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/formato_fechas.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:pluriwave/tema/pluriwave_theme.dart'; import 'package:provider/provider.dart'; import 'package:shared_preferences/shared_preferences.dart'; import '../helpers/fakes.dart'; import '../helpers/fakes_alarmas.dart'; /// Closing batch (items 9.4, 9.10, audit id 2521) -- the ringing screen. /// Mirrors `pantalla_alarma_sonando_tier4_test.dart`'s own harness (kept as /// a local copy, not a shared import, matching this file's established /// precedent of small helper duplication over cross-test-file coupling). Future _montarPantalla(WidgetTester tester) 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( esPremium: () => true, audio: audio, favoritos: FakeServicioFavoritos(), radio: FakeServicioRadio(), servicioEcualizador: FakeServicioEcualizador(), servicioGrabacion: FakeServicioGrabacionRadioInactiva(), iniciarAutomaticamente: false, ); addTearDown(radio.dispose); final android = FakePuertoAlarmasAndroid(); final estadoAlarmas = EstadoAlarmas( esPremium: () => true, servicio: ServicioAlarmas(reloj: () => DateTime(2026, 6, 11, 7, 0)), android: android, iniciarAutomaticamente: false, ); addTearDown(estadoAlarmas.dispose); addTearDown(android.dispose); await estadoAlarmas.guardarAlarma( const AlarmaMusical( id: 'tier5-1', nombre: 'Despertar', hora: 7, minuto: 30, tipoProgramacion: TipoProgramacionAlarma.diaria, diasSemana: [], snoozeMinutos: 10, emisora: 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, theme: PluriWaveTheme.dark(), 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.pump(); await tester.pump(const Duration(milliseconds: 50)); } void main() { setUp(() { SharedPreferences.setMockInitialValues({}); }); testWidgets('visual fidelity (audit 9.4): the date line renders BELOW the ' 'hero time (t4:415-419: pill, then 7:30, then the date)', (tester) async { await _montarPantalla(tester); final localeTag = Localizations.localeOf( tester.element(find.byType(PantallaAlarmaSonando)), ).toString(); final esperado = fechaLargaConDiaSemana(localeTag, DateTime.now()); expect(find.text(esperado), findsOneWidget); // Order: pill, then the hero time, then the date line. This test used // to assert date-before-time and cited "t4:419" for it — but 419 is // simply the source line the date occupies, and in the prototype it // comes AFTER the 88px time on line 417. The citation refuted the // assertion it was supporting. final pillY = tester .getBottomLeft(find.byKey(const ValueKey('ringing-schedule-pill'))) .dy; final timeY = tester .getBottomLeft(find.byKey(const ValueKey('ringing-hero-time'))) .dy; final dateY = tester.getTopLeft(find.text(esperado)).dy; expect(pillY <= timeY, isTrue, reason: 'pill sits above the time'); expect(timeY <= dateY, isTrue, reason: 'the date sits below the time'); // Regression guard: pumpAndSettle must still complete (purely // additive static text, no new animation). await tester.pumpAndSettle(); }); testWidgets( 'visual fidelity (audit 9.10): the highlighted snooze tile carries a ' '"usual" qualifier ADDITIVELY -- the original flat label the dismiss ' 'guard finds by text stays fully intact', (tester) async { await _montarPantalla(tester); final l10n = AppLocalizations.of( tester.element(find.byType(PantallaAlarmaSonando)), ); // The ORIGINAL flat label must still resolve to exactly one Text -- // this IS the dismiss-guard's own finder contract. expect(find.text(l10n.alarmSnoozeOptionLabel(10)), findsOneWidget); // The additive qualifier renders too, but only on the destacado tile. expect(find.text(l10n.alarmSnoozeUsualLabel), findsOneWidget); expect(find.text(l10n.alarmSnoozeOptionLabel(3)), findsOneWidget); expect(find.text(l10n.alarmSnoozeOptionLabel(5)), findsOneWidget); // Regression guard: the exact hazard 9.9/9.10 share -- pumpAndSettle // must still complete, and the flat label must remain the SAME // widget the dismiss guard taps (still inside a FilledButton). expect( find.ancestor( of: find.text(l10n.alarmSnoozeOptionLabel(10)), matching: find.byType(FilledButton), ), findsOneWidget, ); await tester.pumpAndSettle(); }, ); testWidgets( 'Issue 3 (feedback-pruebas): the gap above the snooze tiles matches the ' 'gap below them (t4:427 draws a uniform gap:12 flex column) -- the ' 'previous 10/14 pair matched neither the prototype nor each other', (tester) async { await _montarPantalla(tester); final l10n = AppLocalizations.of( tester.element(find.byType(PantallaAlarmaSonando)), ); final eyebrowBottom = tester .getBottomLeft( find .ancestor( of: find.byIcon(Icons.snooze_rounded), matching: find.byType(Row), ) .first, ) .dy; final tileDestacado = find.ancestor( of: find.text(l10n.alarmSnoozeOptionLabel(10)), matching: find.byType(FilledButton), ); final tilesTop = tester.getTopLeft(tileDestacado).dy; final tilesBottom = tester.getBottomLeft(tileDestacado).dy; final stopButtonTop = tester .getTopLeft(find.byKey(const ValueKey('ringing-stop-button'))) .dy; expect( tilesTop - eyebrowBottom, 12, reason: 't4:427: gap:12 above the snooze tiles', ); expect( stopButtonTop - tilesBottom, 12, reason: 't4:427: gap:12 below the snooze tiles, same as above', ); }, ); }