import 'dart:async'; import 'dart:convert'; import 'package:flutter/services.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:spotiflac_android/services/platform_bridge.dart'; void main() { TestWidgetsFlutterBinding.ensureInitialized(); const channel = MethodChannel('com.zarz.spotiflac/backend'); final messenger = TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger; tearDown(() => messenger.setMockMethodCallHandler(channel, null)); for (final format in ['small-json', 'large-json', 'native-list']) { test('custom search decodes and caches $format results', () async { final rows = List.generate( format == 'large-json' ? 3000 : 2, (index) => { 'id': '$index', 'name': 'Lagu $index — 日本語', 'artist_name': 'Artist', 'album_name': 'Album', 'duration': 123000, }, ); final json = jsonEncode(rows); if (format == 'large-json') { expect(json.length, greaterThan(128 * 1024)); } var calls = 0; messenger.setMockMethodCallHandler(channel, (call) async { expect(call.method, 'customSearchWithExtension'); calls++; return format == 'native-list' ? rows : json; }); final result = await PlatformBridge.customSearchWithExtension( 'decode-test-$format', 'query', ); expect(result, rows); result.first['name'] = 'Changed by caller'; final cached = await PlatformBridge.customSearchWithExtension( 'decode-test-$format', 'query', ); expect(cached, rows); expect(calls, 1); }); } test( 'large cancelled response cannot replace the latest cached search', () async { final pending = >[]; messenger.setMockMethodCallHandler(channel, (call) async { if (call.method == 'cancelExtensionRequest') return null; expect(call.method, 'customSearchWithExtension'); final completer = Completer(); pending.add(completer); return completer.future; }); Future>> search(String query) => PlatformBridge.customSearchWithExtension( 'large-cancel-test', query, cancelPrevious: true, ); final oldA = search('A'); await Future.delayed(Duration.zero); final b = search('B'); await Future.delayed(Duration.zero); final latestA = search('A'); await Future.delayed(Duration.zero); expect(pending, hasLength(3)); pending[2].complete('[{"id":"latest"}]'); expect((await latestA).single['id'], 'latest'); final oldRows = List.generate( 3000, (index) => { 'id': 'old-$index', 'name': 'Previous search result that must not replace the new cache', }, ); final oldJson = jsonEncode(oldRows); expect(oldJson.length, greaterThan(128 * 1024)); pending[0].complete(oldJson); pending[1].complete('[]'); await Future.wait([oldA, b]); expect((await search('A')).single['id'], 'latest'); expect(pending, hasLength(3)); }, ); test('invalid results fail without poisoning the retry cache', () async { var calls = 0; messenger.setMockMethodCallHandler(channel, (_) async { return ++calls == 1 ? '[42]' : '[{"id":"valid"}]'; }); Future>> search() => PlatformBridge.customSearchWithExtension( 'invalid-decode-test', 'query', ); await expectLater(search(), throwsFormatException); expect((await search()).single['id'], 'valid'); expect(calls, 2); }); }