import 'dart:io'; 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 backend = MethodChannel('com.zarz.spotiflac/backend'); const paths = MethodChannel('plugins.flutter.io/path_provider'); final messenger = TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger; late Directory root; setUp(() async { root = await Directory.systemTemp.createTemp('library-full-scan-'); messenger.setMockMethodCallHandler(paths, (_) async => root.path); }); tearDown(() async { messenger.setMockMethodCallHandler(backend, null); messenger.setMockMethodCallHandler(paths, null); await root.delete(recursive: true); }); for (final saf in [false, true]) { Future scan({ bool force = false, bool Function()? isCancelled, }) => saf ? PlatformBridge.scanSafTreeToNDJSONFile( 'content://library/tree/music', forceFullScan: force, isCancelled: isCancelled, ) : PlatformBridge.scanLibraryFolderToNDJSONFile( '/music', forceFullScan: force, isCancelled: isCancelled, ); test('forced scan resets checkpoints and cover paths (SAF=$saf)', () async { final oldCovers = await Directory('${root.path}/library_covers').create(); final oldCover = await File( '${oldCovers.path}/cover_same-key.jpg', ).writeAsBytes([1]); final unrelated = await File( '${root.path}/library_scan_other.ndjson.state', ).writeAsString('other source'); final coverDirectories = []; final resumed = []; messenger.setMockMethodCallHandler(backend, (call) async { final args = call.arguments as Map; if (call.method == 'setLibraryCoverCacheDir') { final directory = args['cache_dir'] as String; expect(await Directory(directory).exists(), isTrue); expect(await File('$directory/cover_same-key.jpg').exists(), isFalse); coverDirectories.add(directory); return null; } expect( call.method, saf ? 'scanSafTreeToNDJSONFile' : 'scanLibraryFolderToNDJSONFile', ); final output = File(args['output_path'] as String); final checkpoint = File('${output.path}.state'); final hasCheckpoint = await checkpoint.exists(); expect(await output.exists(), hasCheckpoint); resumed.add(hasCheckpoint); await output.writeAsString('{"id":"track"}\n'); await checkpoint.writeAsString('track\t123\n'); return {'path': output.path, 'count': 1}; }); final first = await scan(); final resumedScan = await scan(); expect(resumedScan.file.path, first.file.path); expect(coverDirectories, isEmpty); final forced = await scan(force: true); expect(await forced.rows().toList(), [ {'id': 'track'}, ]); await scan(force: true); expect(resumed, [false, true, false, false]); expect(coverDirectories.toSet(), hasLength(2)); expect(await oldCover.readAsBytes(), [1]); expect(await unrelated.readAsString(), 'other source'); await forced.delete(); expect(await forced.file.exists(), isFalse); expect(await File('${forced.file.path}.state').exists(), isFalse); }); test( 'cancel before forced scan preserves resumable output (SAF=$saf)', () async { var calls = 0; messenger.setMockMethodCallHandler(backend, (call) async { calls++; final output = File((call.arguments as Map)['output_path'] as String); await output.writeAsString('{"id":"old"}\n'); await File('${output.path}.state').writeAsString('old\t123\n'); return {'path': output.path, 'count': 1}; }); final previous = await scan(); await expectLater( scan(force: true, isCancelled: () => true), throwsStateError, ); expect(calls, 1); expect(await previous.file.readAsString(), '{"id":"old"}\n'); expect( await File('${previous.file.path}.state').readAsString(), 'old\t123\n', ); }, ); } test('cache setup failure stops forced scan before native reads', () async { final calls = []; messenger.setMockMethodCallHandler(backend, (call) async { calls.add(call.method); throw PlatformException(code: 'cache-unavailable'); }); await expectLater( PlatformBridge.scanSafTreeToNDJSONFile( 'content://library/tree/music', forceFullScan: true, ), throwsA(isA()), ); expect(calls, ['setLibraryCoverCacheDir']); }); }