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_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/playlists/playlist_detail_screen.dart'; import 'package:melo/services/baka_auth.dart'; import 'package:melo/services/download_service.dart'; import 'package:melo/services/gast_zugang.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'; 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); } /// Ersetzt in [MeloAudioHandler.loadPlaylist] den echten just_audio-Zugriff — /// dieselbe Klasse wie in playlist_detail_navigation_test.dart. class _FakeAudioHandler extends MeloAudioHandler { _FakeAudioHandler(MeloDb db) : super(db: db); @override Future loadPlaylist(List items, {int startIndex = 0}) async { queue.add(items); if (items.isNotEmpty) mediaItem.add(items[startIndex]); return 0; } } void main() { setUp(() => SharedPreferences.setMockInitialValues({})); testWidgets( 'Per Navigator.push geöffneter PlaylistDetailScreen (wie aus ' 'playlists_screen.dart:120) sieht PlayerExpansionController — echtes ' 'HomeShell, echter Navigator.push, kein direkt daneben geklebter ' 'Test-Provider (Review-Fund: Provider war bisher nur INNERHALB von ' 'HomeShell.build() bereitgestellt, eine gepushte Route ist im Baum ' 'aber ein Geschwister von HomeShell, kein Nachfahre)', (tester) async { driftRuntimeOptions.dontWarnAboutMultipleDatabases = true; final db = MeloDb(NativeDatabase.memory()); 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, )); final playlistId = await db.createPlaylist('Road Trip'); await db.addSongToPlaylist(playlistId, 'song-1', 0); final playlist = Playlist( id: playlistId, name: 'Road Trip', createdAtMs: 0, updatedAtMs: 0, deleted: false, ); final lib = LibraryService(db); final playlists = PlaylistService(db); final handler = _FakeAudioHandler(db); final auth = BakaAuth(speicher: _MemorySpeicher()); final expansion = PlayerExpansionController(vsync: tester); addTearDown(expansion.dispose); // Dieselbe Provider-Verdrahtung wie home_shell_real_expansion_test.dart, // PlayerExpansionController aber jetzt OBERHALB von MaterialApp/HomeShell // — genau wie MeloApps MultiProvider nach dem Fix (main.dart), nicht mehr // nur INNERHALB von HomeShell.build() bereitgestellt. 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: (_) => GastZugang()), 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))), ChangeNotifierProvider.value( value: expansion), ], child: const MaterialApp(home: HomeShell()), )); await tester.pump(); // Navigator.push GENAU wie playlists_screen.dart:120 — von einem Kontext // INNERHALB des echten HomeShell-Baums aus (MiniPlayer steckt immer im // Stack, auch ohne laufenden Titel), nicht von einem manuell daneben // gebauten Test-Baum. final homeShellContext = tester.element(find.byType(MiniPlayer)); Navigator.of(homeShellContext).push(MaterialPageRoute( builder: (_) => PlaylistDetailScreen(playlist: playlist), )); await tester.pumpAndSettle(); expect(find.text('Roses'), findsOneWidget); // Tap wie ein echter Nutzer — löst intern playSongs() und danach // context.read().open(context) aus. await tester.tap(find.text('Roses')); await tester.pumpAndSettle(); // Bug-Symptom: die ProviderNotFoundException landet im try/catch von // PlaylistDetailScreen und zeigt eine "Wiedergabe fehlgeschlagen"- // Snackbar, statt dass der Player öffnet. expect(find.textContaining('Wiedergabe fehlgeschlagen'), findsNothing, reason: 'context.read() darf hier keine ' 'ProviderNotFoundException werfen.'); // Beweis, dass .open() tatsächlich denselben (einzigen) Controller // erreicht hat, nicht nur, dass keine Exception flog. expect(expansion.progress, 1.0); // Aufräum-Reihenfolge wie home_shell_real_expansion_test.dart: erst den // Baum abbauen, dann den Handler disposen (offener Positions-Timer). await tester.pumpWidget(const SizedBox()); handler.dispose(); await tester.pump(const Duration(milliseconds: 1)); }); }