import 'dart:ui'; import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:provider/provider.dart'; import 'package:melo/player/player_expansion_controller.dart'; // Minimaler Host, der dieselbe Provider/PopScope-Verdrahtung wie HomeShell // nachstellt, ohne die volle App (Audio-Handler etc.) hochfahren zu müssen. // Der eigentliche HomeShell-Umbau wird direkt gegen main.dart getestet, // sobald Task 4/5 die Stack-Struktur eingeführt haben; dieser erste Test // sichert nur die PopScope-Erweiterung ab, isoliert an einem Test-Double. class _TestShell extends StatefulWidget { const _TestShell({this.tabContent = const SizedBox.shrink()}); final Widget tabContent; @override State<_TestShell> createState() => _TestShellState(); } class _TestShellState extends State<_TestShell> with TickerProviderStateMixin { late final PlayerExpansionController _controller; @override void initState() { super.initState(); _controller = PlayerExpansionController(vsync: this); } @override void dispose() { _controller.dispose(); super.dispose(); } @override Widget build(BuildContext context) { // Eigener AnimatedBuilder um PopScope, damit canPop bei jedem // _controller-Tick neu ausgewertet wird (Reaktivitäts-Regel aus dem // Plan) — ohne ihn bliebe canPop auf dem Wert des allerersten Builds // eingefroren, weil sonst nichts hier ein Rebuild auslöst. return ChangeNotifierProvider.value( value: _controller, child: AnimatedBuilder( animation: _controller, builder: (context, child) => PopScope( canPop: _controller.progress == 0, onPopInvokedWithResult: (didPop, _) { if (!didPop) _controller.close(context); }, child: child!, ), // Derselbe Stack-Aufbau wie HomeShell (Task 4): Blur-Layer und // Tab-Blocker bekommen jeweils ihren eigenen AnimatedBuilder, NICHT // nur den äußeren aus dem PopScope-Consumer oben (Reaktivitäts-Regel // aus dem Plan) — sonst würde dieser Test den genau davor gefundenen // Bug (Review-Panel) nicht abdecken. child: Scaffold( body: Stack( children: [ AnimatedBuilder( animation: _controller, builder: (context, child) => IgnorePointer( ignoring: _controller.progress > 0, child: child, ), child: widget.tabContent, ), AnimatedBuilder( animation: _controller, builder: (context, child) { final p = _controller.progress; if (p == 0) return const SizedBox.shrink(); return Positioned.fill( child: BackdropFilter( filter: ImageFilter.blur(sigmaX: p * 20.0, sigmaY: p * 20.0), child: Container(color: Colors.transparent), ), ); }, ), ], ), ), ), ); } } void main() { testWidgets('Zurück-Taste schließt den Player statt zu poppen, wenn offen', (tester) async { await tester.pumpWidget(const MaterialApp(home: _TestShell())); final controller = tester.state<_TestShellState>(find.byType(_TestShell))._controller; controller.open(tester.element(find.byType(_TestShell))); await tester.pumpAndSettle(); expect(controller.progress, 1.0); // pageBack() sucht einen sichtbaren Zurück-Button, den es hier ohne // zweite Route nicht gibt (siehe Plan-Hinweis für den Umsetzer) — // Navigator.maybePop() löst denselben PopScope-Callback aus. await Navigator.maybePop(tester.element(find.byType(_TestShell))); await tester.pumpAndSettle(); expect(controller.progress, 0.0); }); testWidgets('Blur-Layer erscheint erst, wenn progress > 0', (tester) async { await tester.pumpWidget(const MaterialApp(home: _TestShell())); expect(find.byType(BackdropFilter), findsNothing); final controller = tester.state<_TestShellState>(find.byType(_TestShell))._controller; controller.open(tester.element(find.byType(_TestShell))); // Ein zusätzlicher, leerer pump() startet den Ticker — sein erster Tick // liefert laut Flutter-Konvention elapsed=0 (Baseline), erst der // folgende pump(duration) rückt die Animation tatsächlich vorwärts. // Ohne diesen Zwischenschritt bliebe progress nach nur einem // pump(duration) fälschlich bei 0.0 (per Debug-Ausgabe verifiziert). await tester.pump(); await tester.pump(const Duration(milliseconds: 50)); // mitten in der Animation expect(find.byType(BackdropFilter), findsOneWidget); await tester.pumpAndSettle(); }); testWidgets('Tab-Inhalt ist bei offenem Player nicht mehr antippbar', (tester) async { var getappt = false; await tester.pumpWidget(MaterialApp( home: _TestShell( tabContent: GestureDetector( onTap: () => getappt = true, child: const SizedBox(width: 200, height: 200), ), ), )); final controller = tester.state<_TestShellState>(find.byType(_TestShell))._controller; controller.open(tester.element(find.byType(_TestShell))); await tester.pumpAndSettle(); await tester.tap(find.byType(GestureDetector), warnIfMissed: false); expect(getappt, isFalse); }); }