import 'package:drift/native.dart'; import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:melo/library/category_service.dart'; import 'package:melo/library/database.dart'; import 'package:melo/library/library_service.dart'; import 'package:melo/library/playlist_service.dart'; import 'package:melo/main.dart'; import 'package:melo/player/audio_handler.dart'; import 'package:melo/player/mini_player.dart'; import 'package:melo/player/player_expansion_controller.dart'; import 'package:melo/services/baka_auth.dart'; import 'package:melo/services/download_service.dart'; import 'package:melo/services/melo_cloud_service.dart'; import 'package:melo/services/navidrome_service.dart'; import 'package:melo/services/offline_mode.dart'; import 'package:melo/services/sync_service.dart'; import 'package:melo/services/yt_download_service.dart'; import 'package:melo/services/yt_search_service.dart'; import 'package:melo/settings/app_settings.dart'; import 'package:provider/provider.dart'; import 'package:shared_preferences/shared_preferences.dart'; // Dieselbe _MemorySpeicher/Provider-Verdrahtung wie test/home_shell_test.dart // — von dort übernommen, nicht neu erfunden. class _MemorySpeicher implements TokenSpeicher { final Map werte = {}; @override Future lesen(String key) async => werte[key]; @override Future schreiben(String key, String wert) async => werte[key] = wert; @override Future loeschen(String key) async => werte.remove(key); } void main() { setUp(() => SharedPreferences.setMockInitialValues({})); testWidgets( 'Echtes HomeShell: Blur-Layer reagiert live auf progress, nicht erst ' 'beim nächsten fremden Rebuild', (tester) async { final db = MeloDb(NativeDatabase.memory()); final lib = LibraryService(db); final playlists = PlaylistService(db); final handler = MeloAudioHandler(db: db); final auth = BakaAuth(speicher: _MemorySpeicher()); await tester.pumpWidget(MultiProvider( providers: [ Provider.value(value: db), ChangeNotifierProvider.value(value: lib), ChangeNotifierProvider.value(value: playlists), Provider.value(value: handler), ChangeNotifierProvider(create: (_) => CategoryService(db)), ChangeNotifierProvider(create: (_) => AppSettings()), ChangeNotifierProvider(create: (_) => OfflineMode()), ChangeNotifierProvider(create: (_) => auth), ChangeNotifierProvider( create: (_) => YtDownloadService(auth: auth)), ChangeNotifierProvider( create: (_) => YtSearchService(auth: auth)), ChangeNotifierProvider( create: (_) => DownloadService(db: db, navidrome: NavidromeService())), ChangeNotifierProvider( create: (_) => SyncService(db: db, cloud: MeloCloudService(auth: auth))), ], child: const MaterialApp(home: HomeShell()), )); await tester.pump(); expect(find.byType(BackdropFilter), findsNothing); // PlayerExpansionController wird von HomeShell selbst per // ChangeNotifierProvider INNERHALB seines eigenen build() bereitgestellt // — der Provider sitzt also unterhalb (nicht oberhalb) des HomeShell- // Elements im Baum. Provider.of von einem HomeShell-Element aus würde // deshalb ins Leere laufen (Ancestor-Lookup findet nur Vorfahren, nicht // die eigenen Nachfahren) — stattdessen ein Element unterhalb nehmen, // z.B. MiniPlayer, das immer im Stack steckt, auch ohne laufenden Titel. final descendantContext = tester.element(find.byType(MiniPlayer)); final controller = Provider.of( descendantContext, listen: false); controller.open(descendantContext); // Absichtlich NUR EINEN kurzen Zwischenschritt pumpen (nicht // pumpAndSettle) — das ist der entscheidende Unterschied zum // `_TestShell`-Test: er beweist, dass der Blur-Layer OHNE einen // zusätzlichen, fremden setState()-Trigger (z.B. Tab-Wechsel) live // erscheint. await tester.pump(); await tester.pump(const Duration(milliseconds: 50)); expect(find.byType(BackdropFilter), findsOneWidget); await tester.pumpAndSettle(); // Baum abbauen, bevor der Test endet — derselbe Grund wie in // test/home_shell_test.dart (offener Positions-Timer des Audio-Handlers). await tester.pumpWidget(const SizedBox()); handler.dispose(); await tester.pump(const Duration(milliseconds: 1)); }); testWidgets( 'State-Persistenz: _CoverGrundState bleibt über einen ' 'open()/close()/open()-Zyklus erhalten (Spec-Vorteil ggü. dem früheren ' 'Navigator.push, das die Route bei jedem Schließen disposte)', (tester) async { final db = MeloDb(NativeDatabase.memory()); final lib = LibraryService(db); final playlists = PlaylistService(db); final handler = MeloAudioHandler(db: db); final auth = BakaAuth(speicher: _MemorySpeicher()); await tester.pumpWidget(MultiProvider( providers: [ Provider.value(value: db), ChangeNotifierProvider.value(value: lib), ChangeNotifierProvider.value(value: playlists), Provider.value(value: handler), ChangeNotifierProvider(create: (_) => CategoryService(db)), ChangeNotifierProvider(create: (_) => AppSettings()), ChangeNotifierProvider(create: (_) => OfflineMode()), ChangeNotifierProvider(create: (_) => auth), ChangeNotifierProvider( create: (_) => YtDownloadService(auth: auth)), ChangeNotifierProvider( create: (_) => YtSearchService(auth: auth)), ChangeNotifierProvider( create: (_) => DownloadService(db: db, navidrome: NavidromeService())), ChangeNotifierProvider( create: (_) => SyncService(db: db, cloud: MeloCloudService(auth: auth))), ], child: const MaterialApp(home: HomeShell()), )); await tester.pump(); // _CoverGrund ist privat (now_playing_screen.dart) — von außen per // runtimeType-Namen gefunden statt per find.byType, das den Typ // importierbar/öffentlich voraussetzen würde. skipOffstage: false ist // hier nötig: bei progress == 0 steckt NowPlayingScreen in einem // Visibility(visible: false, maintainState: true) — genau das soll // dieser Test ja beweisen —, und Flutters Standard-Finder überspringen // Offstage-Elemente per Default (per Debug-Ausgabe verifiziert). final coverGrundFinder = find.byWidgetPredicate( (w) => w.runtimeType.toString() == '_CoverGrund', skipOffstage: false); expect(coverGrundFinder, findsOneWidget, reason: 'NowPlayingScreen bleibt dank Visibility(maintainState: ' 'true) (Task 5) von Anfang an im Baum, auch bei progress == 0.'); final zustandVorher = tester.state(coverGrundFinder); final descendantContext = tester.element(find.byType(MiniPlayer)); final controller = Provider.of( descendantContext, listen: false); controller.open(descendantContext); await tester.pumpAndSettle(); controller.close(descendantContext); await tester.pumpAndSettle(); controller.open(descendantContext); await tester.pumpAndSettle(); final zustandNachher = tester.state(coverGrundFinder); expect(identical(zustandVorher, zustandNachher), isTrue, reason: '_CoverGrundState wäre beim früheren Navigator.push-Ansatz ' 'bei jedem Schließen disposed und neu erzeugt worden.'); await tester.pumpWidget(const SizedBox()); handler.dispose(); await tester.pump(const Duration(milliseconds: 1)); }); }