import 'package:audio_service/audio_service.dart'; import 'package:drift/drift.dart' show Value, driftRuntimeOptions; import 'package:drift/native.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:provider/provider.dart'; import 'package:shared_preferences/shared_preferences.dart'; import 'package:melo/library/category_service.dart'; import 'package:melo/library/database.dart'; import 'package:melo/library/playlist_service.dart'; import 'package:melo/library/song_list.dart'; import 'package:melo/library/song_media.dart'; import 'package:melo/player/audio_handler.dart'; import 'package:melo/settings/app_settings.dart'; import 'package:melo/shared/lauf_balken.dart'; /// Die Verdrahtung zwischen Player und Songzeile — der Teil zwischen der /// reinen Regel [istAmLaufen] und dem isolierten [LaufMarkierung]. Genau /// hier saßen die Fehler: fehlender Startwert (die Markierung erschien einen /// Frame zu spät) und ein Formwechsel des Widget-Baums, der das Herz daneben /// sein Abo verlieren ließ. void main() { late MeloDb db; late CategoryService kategorien; late PlaylistService playlists; late AppSettings einstellungen; late MeloAudioHandler handler; /// Die Plugin-Kanäle, die `MeloAudioHandler` beim Bauen anfasst. Ohne diese /// Antworten werfen sie asynchron und der Testlauf meldet Fremdfehler. void stelleKanaeleRuhig() { final messenger = TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger; messenger.setMockMethodCallHandler( const MethodChannel('plugins.flutter.io/path_provider'), (call) async => '/tmp/melo-test', ); messenger.setMockMethodCallHandler( const MethodChannel('plugins.it_nomads.com/flutter_secure_storage'), (call) async => null, ); } setUp(() async { TestWidgetsFlutterBinding.ensureInitialized(); driftRuntimeOptions.dontWarnAboutMultipleDatabases = true; SharedPreferences.setMockInitialValues({}); stelleKanaeleRuhig(); db = MeloDb(NativeDatabase.memory()); kategorien = CategoryService(db); playlists = PlaylistService(db); einstellungen = AppSettings(); await einstellungen.init(); handler = MeloAudioHandler(db: db); await db.into(db.songs).insert(SongsCompanion.insert( id: 'song-1', path: '/music/roses.mp3', title: 'Roses', artist: const Value('Neoni'), dateAddedMs: 0, updatedAtMs: 0, )); }); tearDown(() { handler.dispose(); kategorien.dispose(); playlists.dispose(); }); Song derSong() => Song( id: 'song-1', path: '/music/roses.mp3', title: 'Roses', artist: 'Neoni', dateAddedMs: 0, updatedAtMs: 0, deleted: false, playCount: 0, categoriesEdited: false, metadataEdited: false, ); Future pumpe(WidgetTester tester) async { await tester.pumpWidget( MultiProvider( providers: [ Provider.value(value: db), Provider.value(value: handler), ChangeNotifierProvider.value(value: kategorien), ChangeNotifierProvider.value(value: playlists), ChangeNotifierProvider.value(value: einstellungen), ], child: MaterialApp( home: Scaffold( body: SongZeile( song: derSong(), warteschlange: [derSong()], index: 0, ), ), ), ), ); } Future aufraeumen(WidgetTester tester) async { await tester.pumpWidget(const SizedBox()); await tester.pump(const Duration(milliseconds: 1)); } testWidgets('ohne Wiedergabe bleibt die Zeile unmarkiert', (tester) async { await pumpe(tester); await tester.pump(); expect(find.byType(LaufBalken), findsNothing); await aufraeumen(tester); }); testWidgets('läuft der Titel, ist die Zeile markiert', (tester) async { handler.mediaItem.add(songToMediaItem(derSong())); await pumpe(tester); await tester.pump(); expect(find.byType(LaufBalken), findsOneWidget); await aufraeumen(tester); }); testWidgets('die Markierung steht schon im ersten Frame', (tester) async { // Ohne initialData liefert der BehaviorSubject erst im nächsten Frame — // die Balken ploppten beim Zurückscrollen sichtbar nach. handler.mediaItem.add(songToMediaItem(derSong())); await tester.pump(); await pumpe(tester); // Genau ein Frame, kein zweiter. expect(find.byType(LaufBalken), findsOneWidget); await aufraeumen(tester); }); testWidgets('ein anderer Titel markiert diese Zeile nicht', (tester) async { handler.mediaItem.add(const MediaItem( id: 'file:///music/anderes.mp3', title: 'Anderes', extras: {'songId': 'song-2'}, )); await pumpe(tester); await tester.pump(); expect(find.byType(LaufBalken), findsNothing); await aufraeumen(tester); }); testWidgets('der Wechsel zu markiert wirft die Zeile nicht weg', (tester) async { // Ein Formwechsel des Widget-Baums hätte das Herz daneben sein Abo // verlieren lassen; es sähe für einen Frame leer aus. await pumpe(tester); await tester.pump(); final vorher = tester.element(find.byType(ListTile)); handler.mediaItem.add(songToMediaItem(derSong())); // Zwei Frames: der Strom liefert erst nach einem Microtask-Durchlauf. await tester.pump(); await tester.pump(); expect(find.byType(LaufBalken), findsOneWidget); // Dasselbe Element wie vorher — der Baum wurde nicht verworfen. expect(tester.element(find.byType(ListTile)), same(vorher)); await aufraeumen(tester); }); }