The test hardcoded 4-18 August 2026, which was in the future when it was written and is now in the past. The pill only renders for the active or next range, so the assertion started failing purely because the calendar moved on — the production code was never wrong. Anchor the range on next month (days 4-18, so it never straddles a month boundary) and assert against rangoFechasCorto, the same pure formatter the widget uses, so the test checks that the pill is rendered rather than restating the formatter's own output.
210 lines
7.4 KiB
Dart
210 lines
7.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/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_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);
|
||
// The pill only renders for the ACTIVE or NEXT range
|
||
// (`EstadoAlarmas.vacacionesProximas` keeps `inicioDia.isAfter(hoy)`), so
|
||
// the range has to be in the future RELATIVE TO THE RUN. Hardcoded
|
||
// calendar dates silently rot into the past and turn this into a
|
||
// date-dependent failure; anchor on next month instead, days 4-18 so the
|
||
// range never straddles a month boundary and the label stays "d–d MON".
|
||
final ahora = DateTime.now();
|
||
final mesSiguiente = DateTime(ahora.year, ahora.month + 1);
|
||
final inicio = DateTime(mesSiguiente.year, mesSiguiente.month, 4);
|
||
final fin = DateTime(mesSiguiente.year, mesSiguiente.month, 18);
|
||
await estado.crearRangoVacaciones(
|
||
estado.servicio.crearRangoVacaciones(
|
||
inicio: inicio,
|
||
fin: fin,
|
||
nombre: 'Summer',
|
||
),
|
||
);
|
||
await montar(tester, estadoAlarmas: estado);
|
||
|
||
// Assert against the same pure formatter the widget uses, so this stays
|
||
// a check that the pill IS rendered in the right shape rather than a
|
||
// duplicate of the formatter's own logic.
|
||
expect(find.text(rangoFechasCorto('en', inicio, fin)), 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,
|
||
);
|
||
},
|
||
);
|
||
});
|
||
}
|