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.
199 lines
6.4 KiB
Dart
199 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:pluriwave/widgets/pluri_glass_surface.dart';
|
|
import 'package:pluriwave/widgets/pluri_wave_scaffold.dart';
|
|
import 'package:provider/provider.dart';
|
|
import 'package:shared_preferences/shared_preferences.dart';
|
|
|
|
import '../helpers/fakes.dart';
|
|
import '../helpers/fakes_alarmas.dart';
|
|
|
|
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: 'scaffold1',
|
|
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,
|
|
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.pumpAndSettle();
|
|
}
|
|
|
|
void main() {
|
|
setUp(() {
|
|
SharedPreferences.setMockInitialValues({});
|
|
});
|
|
|
|
testWidgets(
|
|
'usa PluriWaveScaffold sin colores hardcodeados y anima la entrada '
|
|
'(S2-R7)',
|
|
(tester) async {
|
|
await _montarPantalla(tester);
|
|
|
|
expect(find.byType(PluriWaveScaffold), findsOneWidget);
|
|
for (final scaffold in tester.widgetList<Scaffold>(
|
|
find.byType(Scaffold),
|
|
)) {
|
|
expect(
|
|
scaffold.backgroundColor,
|
|
isNot(const Color(0xFF061722)),
|
|
reason: 'el Scaffold crudo con color hardcodeado debe desaparecer',
|
|
);
|
|
}
|
|
expect(
|
|
find.byType(Animate),
|
|
findsWidgets,
|
|
reason: 'la entrada debe animarse cuando las animaciones estan activas',
|
|
);
|
|
},
|
|
);
|
|
|
|
testWidgets(
|
|
'omite la animacion de entrada con disableAnimations=true (S5-R3)',
|
|
(tester) async {
|
|
await _montarPantalla(tester, disableAnimations: true);
|
|
|
|
expect(find.byType(PluriWaveScaffold), findsOneWidget);
|
|
expect(
|
|
find.byType(Animate),
|
|
findsNothing,
|
|
reason: 'reduced motion debe omitir la animacion de entrada',
|
|
);
|
|
final l10n = lookupAppLocalizations(const Locale('es'));
|
|
expect(find.text(l10n.stopAlarmAction), findsOneWidget);
|
|
},
|
|
);
|
|
|
|
group('WU11 — restyle a pantalla completa', () {
|
|
testWidgets(
|
|
'el tiempo gigante usa PluriWaveTypography.heroTime (con un ajuste '
|
|
'local de letter-spacing/height, audit 9.5) envuelto en '
|
|
'FittedBox(scaleDown)',
|
|
(tester) async {
|
|
await _montarPantalla(tester);
|
|
|
|
final heroFinder = find.byKey(const ValueKey('ringing-hero-time'));
|
|
expect(heroFinder, findsOneWidget);
|
|
final texto = tester.widget<Text>(heroFinder);
|
|
final contexto = tester.element(heroFinder);
|
|
final base = contexto.pluriType.heroTime;
|
|
// Audit 9.5 (t4 line 417): the prototype wants 88px/w800/ls-4/lh.95
|
|
// on THIS screen only. heroTime itself must stay ls-2.0/height 1.0
|
|
// — EditorHoraInline (the alarm editor's hour block) is the other
|
|
// consumer and wants height:1 (t4 line 379) — so this is a LOCAL
|
|
// copyWith override, not a change to the shared token.
|
|
expect(texto.style?.fontSize, base.fontSize);
|
|
expect(texto.style?.fontWeight, base.fontWeight);
|
|
expect(texto.style?.letterSpacing, -4);
|
|
expect(texto.style?.height, 0.95);
|
|
expect(
|
|
base.letterSpacing,
|
|
-2.0,
|
|
reason: 'the shared token is unchanged',
|
|
);
|
|
expect(base.height, 1.0, reason: 'the shared token is unchanged');
|
|
expect(
|
|
find.ancestor(of: heroFinder, matching: find.byType(FittedBox)),
|
|
findsOneWidget,
|
|
);
|
|
},
|
|
);
|
|
|
|
testWidgets(
|
|
'el fondo es arte difuminado a pantalla completa; el contenedor '
|
|
'glass-card anterior desaparece',
|
|
(tester) async {
|
|
await _montarPantalla(tester);
|
|
|
|
expect(
|
|
find.byKey(const ValueKey('ringing-background-art')),
|
|
findsOneWidget,
|
|
);
|
|
expect(find.byType(PluriGlassSurface), findsNothing);
|
|
},
|
|
);
|
|
});
|
|
}
|