import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:pluriwave/estado/estado_radio.dart'; import 'package:pluriwave/l10n/gen/app_localizations.dart'; import 'package:pluriwave/widgets/pluri_sleep_timer_sheet.dart'; import 'package:provider/provider.dart'; import 'package:shared_preferences/shared_preferences.dart'; import '../helpers/fakes.dart'; import '../helpers/fakes_alarmas.dart'; /// Issue 2 (feedback-pruebas): "Timer de sueño" opened from Escuchar (and /// every other root header) showed a bottom sheet with no visible /// countdown. `ServicioTimer.tiempoRestanteStream`/`tiempoRestante` already /// existed and this sheet already had a `StreamBuilder` countdown branch — /// but every preset/custom-duration action popped the sheet immediately /// after starting the timer, so the countdown never had a chance to render /// in the primary flow. Zero test coverage existed for this file before. EstadoRadio _estado() => EstadoRadio( esPremium: () => true, audio: FakeServicioAudio(), favoritos: FakeServicioFavoritos(), radio: FakeServicioRadio(), servicioEcualizador: FakeServicioEcualizador(), servicioGrabacion: FakeServicioGrabacionRadioInactiva(), iniciarAutomaticamente: false, ); Widget _host(EstadoRadio estado) { return ChangeNotifierProvider.value( value: estado, child: MaterialApp( locale: const Locale('es'), localizationsDelegates: AppLocalizations.localizationsDelegates, supportedLocales: AppLocalizations.supportedLocales, home: Scaffold( body: Builder( builder: (context) => TextButton( onPressed: () => showPluriSleepTimerSheet(context), child: const Text('abrir'), ), ), ), ), ); } void main() { setUp(() { SharedPreferences.setMockInitialValues({}); }); // The default 800x600 test viewport is shorter than a typical phone — // same fix `pantalla_reproductor_test.dart`/`pantalla_inicio_test.dart` // already established for other bottom-sheet/full-bleed content. void ajustarSuperficieRealista(WidgetTester tester) { tester.view.physicalSize = const Size(390, 844); tester.view.devicePixelRatio = 1.0; addTearDown(tester.view.resetPhysicalSize); addTearDown(tester.view.resetDevicePixelRatio); } testWidgets( 'selecting a preset keeps the sheet open and switches it to the live ' 'remaining-time countdown, instead of closing with no feedback', (tester) async { ajustarSuperficieRealista(tester); final estado = _estado(); addTearDown(estado.dispose); await tester.pumpWidget(_host(estado)); await tester.tap(find.text('abrir')); await tester.pumpAndSettle(); final l10n = AppLocalizations.of(tester.element(find.text('abrir'))); expect(estado.timer.activo, isFalse); expect(find.byType(ActionChip), findsWidgets); final chip = tester.widget( find.widgetWithText(ActionChip, l10n.durationMinutesOnly(5)), ); chip.onPressed!(); await tester.pump(); expect(estado.timer.activo, isTrue); expect( find.byType(ActionChip), findsNothing, reason: 'issue 2: the picker is replaced by the countdown view', ); expect(find.text(l10n.cancelTimer), findsOneWidget); expect( find.text(l10n.durationMinutesOnly(5)), findsOneWidget, reason: 'issue 2: the remaining time is now surfaced live', ); // `ServicioTimer` starts a REAL periodic Timer -- `addTearDown` // callbacks run too late to satisfy the "no pending timers" check // (a well-known flutter_test ordering quirk), so it must be // cancelled here, inside the test body, before it ends. await estado.timer.cancelar(); }, ); testWidgets('reopening the sheet while a timer is already active shows the ' 'countdown immediately, not the duration picker', (tester) async { ajustarSuperficieRealista(tester); final estado = _estado(); addTearDown(estado.dispose); estado.iniciarTimerDuracion(const Duration(minutes: 10)); await tester.pumpWidget(_host(estado)); await tester.tap(find.text('abrir')); await tester.pumpAndSettle(); final l10n = AppLocalizations.of(tester.element(find.text('abrir'))); expect(find.byType(ActionChip), findsNothing); expect(find.text(l10n.cancelTimer), findsOneWidget); expect(find.text(l10n.durationMinutesOnly(10)), findsOneWidget); // See the comment in the previous test — cancel before the body ends. await estado.timer.cancelar(); }); testWidgets( 'cancelling from the countdown view stops the timer and closes the ' 'sheet', (tester) async { ajustarSuperficieRealista(tester); final estado = _estado(); addTearDown(estado.dispose); estado.iniciarTimerDuracion(const Duration(minutes: 10)); await tester.pumpWidget(_host(estado)); await tester.tap(find.text('abrir')); await tester.pumpAndSettle(); final l10n = AppLocalizations.of(tester.element(find.text('abrir'))); final boton = tester.widget( find.widgetWithText(FilledButton, l10n.cancelTimer), ); boton.onPressed!(); await tester.pumpAndSettle(); expect(estado.timer.activo, isFalse); expect(find.text(l10n.cancelTimer), findsNothing); }, ); testWidgets( 'starting a custom duration ALSO keeps the sheet open on the countdown ' 'view, not just the presets', (tester) async { ajustarSuperficieRealista(tester); final estado = _estado(); addTearDown(estado.dispose); await tester.pumpWidget(_host(estado)); await tester.tap(find.text('abrir')); await tester.pumpAndSettle(); final l10n = AppLocalizations.of(tester.element(find.text('abrir'))); await tester.tap(find.text(l10n.optionOther)); await tester.pumpAndSettle(); // Confirm the custom-duration sub-sheet directly (default prefilled // value is already "15" minutes -- see _TimerPersonalizadoSheetState). final confirmar = tester.widget( find.widgetWithText(FilledButton, l10n.startTimer), ); confirmar.onPressed!(); await tester.pumpAndSettle(); expect(estado.timer.activo, isTrue); expect(find.byType(ActionChip), findsNothing); expect(find.text(l10n.cancelTimer), findsOneWidget); // See the comment in the first test — cancel before the body ends. await estado.timer.cancelar(); }, ); }