Fix: PlayerExpansionController oberhalb des Navigators bereitstellen
Bug (Code-Review, P0 — blockierte den Push): PlayerExpansionController wurde bisher nur INNERHALB von HomeShell.build() bereitgestellt. Per Navigator.push geöffnete Screens (PlaylistDetailScreen, ServerTitelScreen) sind im Widget-Baum aber Geschwister von HomeShell im selben Navigator/Overlay, keine Nachfahren — sie sahen den Provider nicht. context.read<PlayerExpansionController>().open(context) warf dort zur Laufzeit ProviderNotFoundException, vom bestehenden try/catch abgefangen zu einer "Wiedergabe fehlgeschlagen"-Snackbar — der Player öffnete sich nie. MeloApp ist jetzt ein StatefulWidget (TickerProviderStateMixin), erzeugt PlayerExpansionController in initState() und stellt ihn in seiner MultiProvider-Liste bereit — oberhalb von MaterialApp/Navigator, wie alle anderen App-weiten Provider. HomeShell erzeugt keinen eigenen mehr, sondern liest die Referenz per context.read<PlayerExpansionController>() einmalig (keine Subscription) und reicht sie an seine AnimatedBuilder weiter, die weiterhin direkt am Controller lauschen — die bewusste "Consumer nur um PopScope"-Optimierung (Scaffold wird nicht bei jedem progress-Tick neu gebaut) bleibt dabei erhalten. Test zuerst (RED mit der heutigen, echten ProviderNotFoundException reproduziert — via echtem HomeShell + echtem Navigator.push, nicht dem bisherigen Kurzschluss-Testaufbau, der den Provider manuell direkt neben das Test-Widget klebt): test/player/provider_scope_navigator_push_test.dart. Bestehende Tests, die HomeShell direkt aufbauen (hauptmenue_test.dart, home_shell_test.dart, home_shell_real_expansion_test.dart), an die neue Verdrahtung angepasst — der Controller kommt jetzt aus ihrer eigenen MultiProvider-Liste statt aus HomeShell selbst. Volle Suite (625 Tests) grün. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SM6e73V9ZethPiqHSocxUA
This commit is contained in:
co-authored by
Claude Sonnet 5
parent
6687f2f67c
commit
0fe624d1e8
@@ -7,6 +7,7 @@ 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/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';
|
||||
@@ -52,6 +53,7 @@ void main() {
|
||||
late DownloadService downloads;
|
||||
late SyncService sync;
|
||||
MeloAudioHandler? handler;
|
||||
PlayerExpansionController? expansion;
|
||||
|
||||
setUp(() {
|
||||
SharedPreferences.setMockInitialValues({});
|
||||
@@ -81,10 +83,14 @@ void main() {
|
||||
// Widget-Baum sie beim Abbauen selbst schließen, bliebe ein Aufräum-Timer
|
||||
// von drift im Test-Rahmen hängen — und ein anschließendes db.close()
|
||||
// käme gar nicht mehr zurück (der Testlauf hinge unbegrenzt).
|
||||
Widget bauen() {
|
||||
Widget bauen(WidgetTester tester) {
|
||||
// Der Audio-Handler erst hier: sein Zwischenspeicher greift auf einen
|
||||
// Plattform-Kanal zu, den es im setUp noch nicht gibt.
|
||||
handler ??= MeloAudioHandler(db: db);
|
||||
// PlayerExpansionController sitzt seit dem Provider-Scope-Fix (main.dart)
|
||||
// oberhalb von MaterialApp/HomeShell, nicht mehr innerhalb von
|
||||
// HomeShell.build() — hier deshalb dieselbe Verdrahtung nachgebaut.
|
||||
expansion ??= PlayerExpansionController(vsync: tester);
|
||||
return MultiProvider(
|
||||
providers: [
|
||||
Provider<MeloDb>.value(value: db),
|
||||
@@ -99,6 +105,8 @@ void main() {
|
||||
ChangeNotifierProvider<YtSearchService>.value(value: ytSearch),
|
||||
ChangeNotifierProvider<DownloadService>.value(value: downloads),
|
||||
ChangeNotifierProvider<SyncService>.value(value: sync),
|
||||
ChangeNotifierProvider<PlayerExpansionController>.value(
|
||||
value: expansion!),
|
||||
],
|
||||
child: MaterialApp(theme: MeloTheme.dark, home: const HomeShell()),
|
||||
);
|
||||
@@ -113,6 +121,8 @@ void main() {
|
||||
await tester.pumpWidget(const SizedBox());
|
||||
handler?.dispose();
|
||||
handler = null;
|
||||
expansion?.dispose();
|
||||
expansion = null;
|
||||
// Die Zeit ein Stück vorstellen: beim Abbauen melden sich die
|
||||
// Datenbank-Ströme ab und planen dafür eine Aufräum-Aufgabe ein. Ein
|
||||
// pump() ohne Dauer lässt sie liegen, und der Test-Rahmen meldet dann
|
||||
@@ -126,7 +136,7 @@ void main() {
|
||||
);
|
||||
|
||||
testWidgets('alle vier Bereiche stehen im Hauptmenü', (tester) async {
|
||||
await tester.pumpWidget(bauen());
|
||||
await tester.pumpWidget(bauen(tester));
|
||||
await tester.pump();
|
||||
|
||||
final leiste =
|
||||
@@ -138,7 +148,7 @@ void main() {
|
||||
|
||||
testWidgets('jeder Bereich ist beschriftet und liegt auf dem Bildschirm',
|
||||
(tester) async {
|
||||
await tester.pumpWidget(bauen());
|
||||
await tester.pumpWidget(bauen(tester));
|
||||
await tester.pump();
|
||||
|
||||
final bildschirm = tester.view.physicalSize / tester.view.devicePixelRatio;
|
||||
@@ -163,7 +173,7 @@ void main() {
|
||||
final ziel = i;
|
||||
testWidgets('Tipp auf "${hauptmenue[ziel]}" wechselt dorthin',
|
||||
(tester) async {
|
||||
await tester.pumpWidget(bauen());
|
||||
await tester.pumpWidget(bauen(tester));
|
||||
await tester.pump();
|
||||
|
||||
await tester.tap(eintrag(hauptmenue[ziel]));
|
||||
@@ -182,7 +192,7 @@ void main() {
|
||||
|
||||
testWidgets('das Hauptmenü klebt am unteren Rand', (tester) async {
|
||||
// Läge es woanders, wäre es kein Hauptmenü mehr.
|
||||
await tester.pumpWidget(bauen());
|
||||
await tester.pumpWidget(bauen(tester));
|
||||
await tester.pump();
|
||||
|
||||
final menue = tester.getRect(find.byType(BottomNavigationBar));
|
||||
|
||||
Reference in New Issue
Block a user