import 'dart:async'; import 'package:flutter/material.dart'; import 'package:flutter_animate/flutter_animate.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: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'; /// Tier 4 visual fidelity, ringing screen: 9.1 (pulsing amber halo), 9.6 /// (art geometry), 9.8 (snooze eyebrow icon). Item 9.5 (hero time metrics) /// is covered in `pantalla_alarma_sonando_scaffold_test.dart`, which already /// owned the pre-existing `heroTime`-equality assertion that item revises. /// /// 9.1 is the one with real hazard: the prototype's halo is a literal CSS /// `animation: pw-pulse 2.4s ease-in-out infinite`. This screen's dismiss /// guard (`pantalla_alarma_sonando_dismiss_guard_test.dart`, protected, /// byte-identical to `main`) calls `pumpAndSettle()` after every mount and /// every interaction — an actually-infinite `AnimationController.repeat()` /// anywhere in this widget's subtree would hang every one of those calls /// forever, with no way to fix it since that file cannot be touched. The /// halo below is a BOUNDED pulse (one grow/settle cycle, no `.repeat()`) — /// genuine animated motion, but one that always finishes. Future _montarPantalla( WidgetTester tester, { bool disableAnimations = false, }) 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: 'tier4-1', nombre: 'Despertar', hora: 7, minuto: 30, tipoProgramacion: TipoProgramacionAlarma.diaria, diasSemana: [], 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(), builder: (context, child) => MediaQuery( data: MediaQuery.of( context, ).copyWith(disableAnimations: disableAnimations), child: child!, ), 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.1): a pulsing amber halo renders behind the ' 'hero content, and settling it NEVER hangs (protects the dismiss-guard ' "test's repeated pumpAndSettle calls)", (tester) async { await _montarPantalla(tester); expect(find.byKey(const ValueKey('ringing-pulse-halo')), findsOneWidget); // The critical regression guard: this must COMPLETE. An infinite // AnimationController.repeat() here would hang every pumpAndSettle() // call in the protected dismiss-guard test forever. await tester.pumpAndSettle(); }, ); testWidgets( 'visual fidelity (audit 9.1): the halo skips its Animate wrapper under ' 'reduced motion (matches every other entry animation in this app)', (tester) async { await _montarPantalla(tester, disableAnimations: true); expect(find.byType(Animate), findsNothing); expect(find.byKey(const ValueKey('ringing-pulse-halo')), findsOneWidget); }, ); testWidgets( 'visual fidelity (audit 9.6): the ringing art is 180 square with a 36 ' 'corner radius, not 168/30', (tester) async { await _montarPantalla(tester); final art = tester .widgetList(find.byType(Image)) .where((img) => img.width == 180 && img.height == 180); expect( art, isNotEmpty, reason: 'prototype t4 line 421 sizes the ringing-screen art at 180', ); final clip = tester.widget( find.ancestor( of: find.byWidgetPredicate( (w) => w is Image && w.width == 180 && w.height == 180, ), matching: find.byType(ClipRRect), ), ); expect( (clip.borderRadius as BorderRadius).topLeft, const Radius.circular(36), ); }, ); testWidgets( 'visual fidelity (audit 9.8): the POSPONER eyebrow keeps its snooze ' 'icon at 19px warmCoral', (tester) async { await _montarPantalla(tester); final icon = tester.widget(find.byIcon(Icons.snooze_rounded)); expect(icon.size, 19); expect(icon.color, const Color(0xFFF4B860)); }, ); }