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.
196 lines
6.4 KiB
Dart
196 lines
6.4 KiB
Dart
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_alarmas.dart';
|
||
import 'package:pluriwave/servicios/servicio_alarmas.dart';
|
||
import 'package:pluriwave/tema/pluriwave_tokens.dart';
|
||
import 'package:provider/provider.dart';
|
||
import 'package:shared_preferences/shared_preferences.dart';
|
||
|
||
import '../helpers/fakes.dart';
|
||
import '../helpers/fakes_alarmas.dart';
|
||
|
||
/// Tier 5 visual-fidelity closeout (items 7.1-7.4, audit id 2521) --
|
||
/// screens 4-14 batch.
|
||
void main() {
|
||
setUp(() {
|
||
SharedPreferences.setMockInitialValues({});
|
||
});
|
||
|
||
Future<void> montar(
|
||
WidgetTester tester, {
|
||
required EstadoAlarmas estadoAlarmas,
|
||
EstadoRadio? radio,
|
||
}) async {
|
||
tester.view.physicalSize = const Size(1440, 3200);
|
||
tester.view.devicePixelRatio = 1.0;
|
||
addTearDown(tester.view.resetPhysicalSize);
|
||
addTearDown(tester.view.resetDevicePixelRatio);
|
||
|
||
final estadoRadio =
|
||
radio ??
|
||
EstadoRadio(
|
||
esPremium: () => true,
|
||
audio: FakeServicioAudio(),
|
||
favoritos: FakeServicioFavoritos(),
|
||
radio: FakeServicioRadio(),
|
||
servicioEcualizador: FakeServicioEcualizador(),
|
||
servicioGrabacion: FakeServicioGrabacionRadioInactiva(),
|
||
iniciarAutomaticamente: false,
|
||
);
|
||
if (radio == null) addTearDown(estadoRadio.dispose);
|
||
|
||
await tester.pumpWidget(
|
||
MultiProvider(
|
||
providers: [
|
||
ChangeNotifierProvider<EstadoRadio>.value(value: estadoRadio),
|
||
ChangeNotifierProvider<EstadoAlarmas>.value(value: estadoAlarmas),
|
||
],
|
||
child: MaterialApp(
|
||
locale: const Locale('en'),
|
||
localizationsDelegates: AppLocalizations.localizationsDelegates,
|
||
supportedLocales: AppLocalizations.supportedLocales,
|
||
home: const Scaffold(body: PantallaAlarmas()),
|
||
),
|
||
),
|
||
);
|
||
await tester.pump();
|
||
await tester.pump(const Duration(milliseconds: 100));
|
||
}
|
||
|
||
EstadoAlarmas crearEstado() {
|
||
final android = FakePuertoAlarmasAndroid();
|
||
final estado = EstadoAlarmas(
|
||
esPremium: () => true,
|
||
servicio: ServicioAlarmas(reloj: () => DateTime(2026, 6, 11, 6, 0)),
|
||
android: android,
|
||
iniciarAutomaticamente: false,
|
||
);
|
||
return estado;
|
||
}
|
||
|
||
group('visual fidelity (audit 7.1): "New" action', () {
|
||
testWidgets('is a solid brand-teal pill with an add glyph, not the tonal '
|
||
'auto_awesome button (t4:325)', (tester) async {
|
||
final estado = crearEstado();
|
||
addTearDown(estado.dispose);
|
||
await montar(tester, estadoAlarmas: estado);
|
||
|
||
expect(find.byIcon(Icons.auto_awesome_rounded), findsNothing);
|
||
expect(find.byIcon(Icons.add_rounded), findsOneWidget);
|
||
|
||
final l10n = lookupAppLocalizations(const Locale('en'));
|
||
final boton = tester.widget<FilledButton>(find.byType(FilledButton));
|
||
final resuelto = boton.style?.backgroundColor?.resolve(<WidgetState>{});
|
||
expect(resuelto, PluriWaveTokens.brand);
|
||
expect(find.text(l10n.createAlarmAction), findsOneWidget);
|
||
});
|
||
});
|
||
|
||
group('visual fidelity (audit 7.2): next-alarm banner', () {
|
||
testWidgets(
|
||
'is warmCoral-tinted and the Skip chip sits BESIDE the text, not '
|
||
'below it (t4:327-331)',
|
||
(tester) async {
|
||
final estado = crearEstado();
|
||
addTearDown(estado.dispose);
|
||
await estado.guardarAlarma(
|
||
const AlarmaMusical(
|
||
id: 'proxima',
|
||
nombre: 'Despertar',
|
||
hora: 7,
|
||
minuto: 30,
|
||
tipoProgramacion: TipoProgramacionAlarma.diaria,
|
||
diasSemana: [],
|
||
),
|
||
);
|
||
await montar(tester, estadoAlarmas: estado);
|
||
|
||
final l10n = lookupAppLocalizations(const Locale('en'));
|
||
final banner = tester.widget<DecoratedBox>(
|
||
find.byKey(const ValueKey('next-alarm-banner')),
|
||
);
|
||
final decoration = banner.decoration as BoxDecoration;
|
||
expect(
|
||
decoration.color,
|
||
const Color(0xFFF4B860).withValues(alpha: 0.13),
|
||
);
|
||
|
||
final skipY = tester.getCenter(find.text(l10n.alarmHeroSkipAction)).dy;
|
||
final titleY = tester.getCenter(find.byIcon(Icons.alarm_on)).dy;
|
||
expect(
|
||
(skipY - titleY).abs() < 4,
|
||
isTrue,
|
||
reason: 'the Skip chip sits on the SAME row as the icon/text',
|
||
);
|
||
},
|
||
);
|
||
});
|
||
|
||
group('visual fidelity (audit 7.3): vacation row date pill', () {
|
||
testWidgets('shows the upcoming range as a "d-d MON" pill (t4:332)', (
|
||
tester,
|
||
) async {
|
||
final estado = crearEstado();
|
||
addTearDown(estado.dispose);
|
||
await estado.crearRangoVacaciones(
|
||
estado.servicio.crearRangoVacaciones(
|
||
inicio: DateTime(2026, 8, 4),
|
||
fin: DateTime(2026, 8, 18),
|
||
nombre: 'Summer',
|
||
),
|
||
);
|
||
await montar(tester, estadoAlarmas: estado);
|
||
|
||
expect(find.text('4–18 AUG'), findsOneWidget);
|
||
});
|
||
});
|
||
|
||
group('visual fidelity (audit 7.4): alarm card', () {
|
||
testWidgets(
|
||
'shows a recurrence label next to the time and a station icon next '
|
||
'to the station name (t4:337-345)',
|
||
(tester) async {
|
||
final estado = crearEstado();
|
||
addTearDown(estado.dispose);
|
||
await estado.guardarAlarma(
|
||
const AlarmaMusical(
|
||
id: 'tarjeta',
|
||
nombre: 'Despertar',
|
||
hora: 7,
|
||
minuto: 30,
|
||
tipoProgramacion: TipoProgramacionAlarma.diaria,
|
||
diasSemana: [],
|
||
emisora: Emisora(
|
||
uuid: 'e1',
|
||
nombre: 'Radio Uno',
|
||
url: 'https://radio.example/stream',
|
||
),
|
||
),
|
||
);
|
||
await montar(tester, estadoAlarmas: estado);
|
||
|
||
final l10n = lookupAppLocalizations(const Locale('en'));
|
||
expect(
|
||
find.descendant(
|
||
of: find.byKey(const ValueKey('tarjeta-alarma-tarjeta')),
|
||
matching: find.text(l10n.dailyOption),
|
||
),
|
||
findsOneWidget,
|
||
);
|
||
expect(
|
||
find.descendant(
|
||
of: find.byKey(const ValueKey('tarjeta-alarma-tarjeta')),
|
||
matching: find.byKey(const ValueKey('tarjeta-alarma-arte')),
|
||
),
|
||
findsOneWidget,
|
||
);
|
||
},
|
||
);
|
||
});
|
||
}
|