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, // Slice 3 (fade-in dedup): existing callers rely on the radio already // being "reproduciendo" by mount time, which cancels the fallback timer // synchronously and leaves nothing to observe mid-handoff. Fade-in-gate // tests need a live `_estadoSub` subscriber instead, so they set this to // false and emit `reproduciendo` themselves after the widget mounts. bool audioYaReproduciendo = true, }) async { tester.view.physicalSize = const Size(1440, 3200); tester.view.devicePixelRatio = 1.0; addTearDown(tester.view.resetPhysicalSize); addTearDown(tester.view.resetDevicePixelRatio); final audio = FakeServicioAudio(); if (audioYaReproduciendo) { 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(); final 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, emisora: const Emisora( uuid: 'e1', nombre: 'Radio Uno', url: 'https://radio.example/stream', ), ), ); 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, audioPrearrancado: true, ), 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 detiene el audio local, pospone y cierra (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(entorno.audio.pausas, greaterThanOrEqualTo(1)); 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('fade-in dedup en el handoff (Slice 3)', () { testWidgets('el fade-in de Dart se retiene hasta que el nativo confirma el ' 'handoff, y arranca justo despues (camino radio)', (tester) async { final entorno = await _montarPantalla( tester, audioYaReproduciendo: false, ); entorno.android.puertaConfirmarAudioFlutter = Completer(); // Antes de que la radio confirme "reproduciendo", solo debe existir // el volumen de arranque previo (0.05): el ramp real hacia // alarma.volumen todavia NO debe haber arrancado. expect(entorno.android.detenidas, isEmpty); expect(entorno.audio.volumenesAplicados, [0.05]); // La radio confirma que esta reproduciendo -> dispara // _confirmarAudioFlutterListo(), que queda bloqueado en la puerta // (todavia no hay confirmacion nativa real). entorno.audio.emitirEstado(EstadoReproduccion.reproduciendo); await tester.pumpAndSettle(); expect( entorno.android.detenidas, isEmpty, reason: 'confirmarAudioFlutter sigue bloqueado en la puerta de prueba', ); expect( entorno.audio.volumenesAplicados, [0.05], reason: 'el fade-in de Dart no debe arrancar antes del handoff', ); // Se libera la puerta: recien ahi "confirma" el nativo, y solo // entonces debe arrancar el fade-in de Dart (una unica rampa // audible a la vez). entorno.android.puertaConfirmarAudioFlutter!.complete(); await tester.pumpAndSettle(); expect(entorno.android.detenidas, contains('ring1')); expect(entorno.audio.volumenesAplicados, [0.05, 0.85]); }); testWidgets('si confirmar el audio con el nativo falla, el fade-in de Dart ' 'arranca igual (el nativo esta muerto o nunca corrio)', (tester) async { final entorno = await _montarPantalla( tester, audioYaReproduciendo: false, ); entorno.android.fallaConfirmarAudioFlutter = true; entorno.audio.emitirEstado(EstadoReproduccion.reproduciendo); await tester.pumpAndSettle(); // La confirmacion nativa fallo (el fake lanza antes de registrar en // `detenidas`), pero el fade-in de Dart debe arrancar de todas // formas: si el lado nativo esta muerto o nunca corrio, Dart es la // unica fuente audible, y el ring no debe quedar pegado en // _volumenInicialFadeIn para siempre. expect(entorno.android.detenidas, isEmpty); expect(entorno.audio.volumenesAplicados, [0.05, 0.85]); }); }); }