Files
pluriwave/test/pantallas/pantalla_alarma_sonando_tier4_test.dart
T
FreeTLab d81fabbe27 refactor(iap): make esPremium a required constructor parameter
EstadoAlarmas, EstadoGrabacion and EstadoRadio defaulted `esPremium` to
`() => true`, so any construction site that forgot to wire entitlement
compiled fine and silently ran ungated — failing OPEN to premium and
disabling the paywall with no test able to catch it.

The parameter is now required with no default. Production wiring in
app.dart was already correct and is unchanged; the 184 pre-existing test
call sites now pass `() => true` explicitly, which is exactly the old
implicit default, so every assertion is untouched.

EstadoRadio has no gate of its own but constructs EstadoGrabacion, so it
inherits the same contract.

The one test that existed to pin the old default is renamed to describe
what it still covers (the premium path through iniciar() with no
duracion); its assertions are unchanged.
2026-08-10 22:06:36 +02:00

192 lines
6.4 KiB
Dart

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<void> _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<EstadoRadio>.value(value: radio),
ChangeNotifierProvider<EstadoAlarmas>.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<NavigatorState>(find.byType(Navigator));
unawaited(
navigator.push(
MaterialPageRoute<void>(
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<Image>(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<ClipRRect>(
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<Icon>(find.byIcon(Icons.snooze_rounded));
expect(icon.size, 19);
expect(icon.color, const Color(0xFFF4B860));
},
);
}