import 'dart:async'; import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:pluriwave/estado/estado_alarmas.dart'; import 'package:pluriwave/estado/estado_radio.dart'; import 'package:pluriwave/l10n/gen/app_localizations.dart'; import 'package:pluriwave/modelos/alarma_musical.dart'; import 'package:pluriwave/modelos/emisora.dart'; import 'package:pluriwave/pantallas/pantalla_alarma_sonando.dart'; import 'package:pluriwave/servicios/servicio_alarmas.dart'; import 'package:pluriwave/servicios/servicio_audio.dart'; import 'package:provider/provider.dart'; import 'package:shared_preferences/shared_preferences.dart'; import '../helpers/fakes.dart'; import '../helpers/fakes_alarmas.dart'; class _Entorno { _Entorno({ required this.estadoAlarmas, required this.android, required this.audio, }); final EstadoAlarmas estadoAlarmas; final FakePuertoAlarmasAndroid android; final FakeServicioAudio audio; } Future<_Entorno> _montarPantalla( WidgetTester tester, { int snoozeMinutos = 5, int fadeInSegundos = 0, }) async { tester.view.physicalSize = const Size(1440, 3200); tester.view.devicePixelRatio = 1.0; addTearDown(tester.view.resetPhysicalSize); addTearDown(tester.view.resetDevicePixelRatio); final audio = FakeServicioAudio(); audio.emitirEstado(EstadoReproduccion.reproduciendo); final radio = EstadoRadio( audio: audio, favoritos: FakeServicioFavoritos(), radio: FakeServicioRadio(), servicioEcualizador: FakeServicioEcualizador(), servicioGrabacion: FakeServicioGrabacionRadioInactiva(), iniciarAutomaticamente: false, ); addTearDown(radio.dispose); final android = FakePuertoAlarmasAndroid(); // Mutable clock: the alarm is saved at 7:00 and the screen mounts at ring // time (7:30:10), so snooze anchors to the RINGING occurrence — the only // scenario the ringing screen can exist in. var ahora = DateTime(2026, 6, 11, 7, 0); final estadoAlarmas = EstadoAlarmas( servicio: ServicioAlarmas(reloj: () => ahora), android: android, iniciarAutomaticamente: false, ); addTearDown(estadoAlarmas.dispose); addTearDown(android.dispose); await estadoAlarmas.guardarAlarma( AlarmaMusical( id: 'ring1', nombre: 'Despertar', hora: 7, minuto: 30, tipoProgramacion: TipoProgramacionAlarma.diaria, diasSemana: const [], snoozeMinutos: snoozeMinutos, fadeInSegundos: fadeInSegundos, emisora: const Emisora( uuid: 'e1', nombre: 'Radio Uno', url: 'https://radio.example/stream', ), ), ); // The screen mounts at ring time: 10s after the 7:30 occurrence fired. ahora = DateTime(2026, 6, 11, 7, 30, 10); await tester.pumpWidget( MultiProvider( providers: [ ChangeNotifierProvider.value(value: radio), ChangeNotifierProvider.value(value: estadoAlarmas), ], child: MaterialApp( locale: const Locale('es'), localizationsDelegates: AppLocalizations.localizationsDelegates, supportedLocales: AppLocalizations.supportedLocales, home: const SizedBox.shrink(), ), ), ); final navigator = tester.state(find.byType(Navigator)); unawaited( navigator.push( MaterialPageRoute( builder: (_) => PantallaAlarmaSonando(alarma: estadoAlarmas.alarmas.single), fullscreenDialog: true, ), ), ); await tester.pumpAndSettle(); return _Entorno(estadoAlarmas: estadoAlarmas, android: android, audio: audio); } void main() { final l10n = lookupAppLocalizations(const Locale('es')); setUp(() { SharedPreferences.setMockInitialValues({}); }); testWidgets( 'muestra botones de posponer 3/5/10 mas el personalizado (S2-R1-A/C)', (tester) async { await _montarPantalla(tester, snoozeMinutos: 7); expect(find.text(l10n.alarmSnoozeOptionLabel(3)), findsOneWidget); expect(find.text(l10n.alarmSnoozeOptionLabel(5)), findsOneWidget); expect(find.text(l10n.alarmSnoozeOptionLabel(7)), findsOneWidget); expect(find.text(l10n.alarmSnoozeOptionLabel(10)), findsOneWidget); expect(find.text(l10n.stopAlarmAction), findsOneWidget); }, ); testWidgets( 'no duplica el boton cuando snoozeMinutos coincide con una opcion fija', (tester) async { await _montarPantalla(tester, snoozeMinutos: 5); expect(find.text(l10n.alarmSnoozeOptionLabel(3)), findsOneWidget); expect(find.text(l10n.alarmSnoozeOptionLabel(5)), findsOneWidget); expect(find.text(l10n.alarmSnoozeOptionLabel(10)), findsOneWidget); }, ); testWidgets( 'posponer 5 min pospone la alarma y cierra la pantalla (S2-R1-B)', (tester) async { final entorno = await _montarPantalla(tester, snoozeMinutos: 5); await tester.tap(find.text(l10n.alarmSnoozeOptionLabel(5))); await tester.pumpAndSettle(); final alarma = entorno.estadoAlarmas.alarmas.single; expect(alarma.snoozeHasta, DateTime(2026, 6, 11, 7, 35)); expect(find.byType(PantallaAlarmaSonando), findsNothing); // posponerAlarma oculta la notificacion nativa (mismo stop path que // el boton de detener) y reprograma con el snooze. expect(entorno.android.ocultadas, contains('ring1')); expect( entorno.android.programadas.last.snoozeHasta, DateTime(2026, 6, 11, 7, 35), ); }, ); group('salidas del ring fuera de los botones', () { testWidgets( 'el boton atras del sistema se comporta como Detener: finaliza la ' 'ejecucion y cierra', (tester) async { final entorno = await _montarPantalla(tester); // Simulate the SYSTEM back (routes through PopScope via the // binding), not a direct Navigator.pop: a plain pop only runs // dispose(), which would leave no alarm UI anywhere to stop the // ring (native audio teardown is driven by finalizarEjecucion, the // same path Detener uses). await tester.binding.handlePopRoute(); await tester.pumpAndSettle(); expect(find.byType(PantallaAlarmaSonando), findsNothing); expect( entorno.android.ocultadas, contains('ring1'), reason: 'atras debe finalizar la ejecucion (mismo camino que Detener)', ); }, ); }); }