Files
pluriwave/test/pantallas/pantalla_alarmas_editor_test.dart
T
FreeTLab 9a2eb57a0e feat(alarmas): simplify alarm cards and add vacation summary row
Restyle the Alarmas root per the functional redesign: alarm cards drop
the always-visible edit/skip/delete button row for a minimal giant
time + station + switch layout. Tap opens the editor, swipe deletes
(with an AlertDialog confirmation), and the hero banner gains an
inline "Saltar" pill for the featured (soonest-firing) alarm's skip
action. No capability from the old button row is lost, only the
trigger location moved; estado_alarmas.dart and its scheduling/
snooze/dismiss-guard tests are untouched.

The vacation inline panel becomes a tappable summary row (range count
+ next-range countdown, computed over the existing estado.vacaciones)
that pushes a Vacaciones manager screen. That destination is a
placeholder for now (_PantallaVacacionesTemporal, holding the old
panel's body verbatim so add/delete-range capability is preserved) --
WU9 replaces it with the real PantallaVacaciones per design ADR-6.

New ARB keys (en/es only; other 11 locales are WU18's job):
alarmHeroSkipAction, alarmDeleteConfirmTitle/Message,
vacationRangesCount, vacationSummary{Active,Upcoming}Countdown.
2026-07-29 10:20:25 +02:00

394 lines
13 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/pantallas/pantalla_alarmas.dart';
import 'package:pluriwave/servicios/servicio_alarmas.dart';
import 'package:pluriwave/widgets/pluri_push_scaffold.dart';
import 'package:provider/provider.dart';
import 'package:shared_preferences/shared_preferences.dart';
import '../helpers/fakes.dart';
import '../helpers/fakes_alarmas.dart';
class _Entorno {
_Entorno({required this.estadoAlarmas});
final EstadoAlarmas estadoAlarmas;
}
class _EntornoDosAlarmas {
_EntornoDosAlarmas({required this.estadoAlarmas, required this.android});
final EstadoAlarmas estadoAlarmas;
final FakePuertoAlarmasAndroid android;
}
/// WU8 fixture: two DAILY alarms ('r1' 07:00, 'r2' 08:00) under a FIXED clock
/// (06:00, before both) so 'r1' is deterministically the featured/soonest
/// alarm regardless of the wall-clock time the suite happens to run at.
Future<_EntornoDosAlarmas> _montarDosAlarmas(WidgetTester tester) async {
tester.view.physicalSize = const Size(1440, 3200);
tester.view.devicePixelRatio = 1.0;
addTearDown(tester.view.resetPhysicalSize);
addTearDown(tester.view.resetDevicePixelRatio);
final radio = EstadoRadio(
audio: FakeServicioAudio(),
favoritos: FakeServicioFavoritos(),
radio: FakeServicioRadio(),
servicioEcualizador: FakeServicioEcualizador(),
servicioGrabacion: FakeServicioGrabacionRadioInactiva(),
iniciarAutomaticamente: false,
);
addTearDown(radio.dispose);
final android = FakePuertoAlarmasAndroid();
final estadoAlarmas = EstadoAlarmas(
servicio: ServicioAlarmas(reloj: () => DateTime(2026, 6, 1, 6, 0)),
android: android,
iniciarAutomaticamente: false,
);
addTearDown(estadoAlarmas.dispose);
addTearDown(android.dispose);
await estadoAlarmas.guardarAlarma(
const AlarmaMusical(
id: 'r1',
nombre: 'Primera',
hora: 7,
minuto: 0,
tipoProgramacion: TipoProgramacionAlarma.diaria,
diasSemana: [],
),
);
await estadoAlarmas.guardarAlarma(
const AlarmaMusical(
id: 'r2',
nombre: 'Segunda',
hora: 8,
minuto: 0,
tipoProgramacion: TipoProgramacionAlarma.diaria,
diasSemana: [],
),
);
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,
home: const Scaffold(body: PantallaAlarmas()),
),
),
);
await tester.pumpAndSettle();
return _EntornoDosAlarmas(estadoAlarmas: estadoAlarmas, android: android);
}
Future<_Entorno> _abrirEditor(WidgetTester tester) async {
tester.view.physicalSize = const Size(1440, 3200);
tester.view.devicePixelRatio = 1.0;
addTearDown(tester.view.resetPhysicalSize);
addTearDown(tester.view.resetDevicePixelRatio);
final favoritos = FakeServicioFavoritos();
await favoritos.agregar(emisoraDemo(uuid: 'alfa', nombre: 'Alfa FM'));
await favoritos.agregar(emisoraDemo(uuid: 'beta', nombre: 'Beta FM'));
final radio = EstadoRadio(
audio: FakeServicioAudio(),
favoritos: favoritos,
radio: FakeServicioRadio(),
servicioEcualizador: FakeServicioEcualizador(),
servicioGrabacion: FakeServicioGrabacionRadioInactiva(),
iniciarAutomaticamente: false,
);
addTearDown(radio.dispose);
await radio.cargarFavoritos();
final android = FakePuertoAlarmasAndroid();
final estadoAlarmas = EstadoAlarmas(
servicio: ServicioAlarmas(reloj: DateTime.now),
android: android,
iniciarAutomaticamente: false,
);
addTearDown(estadoAlarmas.dispose);
addTearDown(android.dispose);
await estadoAlarmas.guardarAlarma(
const AlarmaMusical(
id: 'ed1',
nombre: 'Semanal',
hora: 7,
minuto: 30,
tipoProgramacion: TipoProgramacionAlarma.diasSemana,
diasSemana: [DateTime.monday],
),
);
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,
home: const Scaffold(body: PantallaAlarmas()),
),
),
);
await tester.pumpAndSettle();
// WU8: the always-visible "Edit" button is gone — tapping the simplified
// card itself now opens the editor.
final tarjeta = find.byKey(const ValueKey('tarjeta-alarma-ed1'));
await tester.ensureVisible(tarjeta);
await tester.pumpAndSettle();
await tester.tap(tarjeta);
await tester.pumpAndSettle();
return _Entorno(estadoAlarmas: estadoAlarmas);
}
String _textoPreview(WidgetTester tester) {
final texto = tester.widget<Text>(
find.descendant(
of: find.byKey(const ValueKey('next-trigger-preview')),
matching: find.byType(Text),
),
);
return texto.data ?? '';
}
void main() {
final l10n = lookupAppLocalizations(const Locale('es'));
setUp(() {
SharedPreferences.setMockInitialValues({});
});
testWidgets(
'muestra la proxima ejecucion y la actualiza al cambiar la recurrencia '
'(S2-R8)',
(tester) async {
await _abrirEditor(tester);
expect(
find.byKey(const ValueKey('next-trigger-preview')),
findsOneWidget,
);
final antes = _textoPreview(tester);
expect(antes, isNot(l10n.alarmNoNextExecution));
// Lunes -> Martes: la fecha calculada SIEMPRE cambia, sea cual sea hoy.
await tester.tap(find.text(l10n.weekdayShortTuesday));
await tester.pumpAndSettle();
await tester.tap(find.text(l10n.weekdayShortMonday));
await tester.pumpAndSettle();
final despues = _textoPreview(tester);
expect(despues, isNot(l10n.alarmNoNextExecution));
expect(despues, isNot(antes));
},
);
testWidgets(
'el selector de emisora abre un bottom sheet con buscador (S2-R9)',
(tester) async {
await _abrirEditor(tester);
await tester.ensureVisible(
find.byKey(const ValueKey('alarm-station-field')),
);
await tester.pumpAndSettle();
await tester.tap(find.byKey(const ValueKey('alarm-station-field')));
await tester.pumpAndSettle();
expect(find.byType(SearchBar), findsOneWidget);
final lista = find.byType(ListView).last;
expect(
find.descendant(of: lista, matching: find.text('Alfa FM')),
findsOneWidget,
);
expect(
find.descendant(of: lista, matching: find.text('Beta FM')),
findsOneWidget,
);
await tester.enterText(find.byType(TextField).last, 'beta');
await tester.pumpAndSettle();
expect(
find.descendant(of: lista, matching: find.text('Alfa FM')),
findsNothing,
);
expect(
find.descendant(of: lista, matching: find.text('Beta FM')),
findsOneWidget,
);
},
);
testWidgets(
'tambien existe un selector para la emisora de respaldo (S2-R9)',
(tester) async {
await _abrirEditor(tester);
await tester.ensureVisible(
find.byKey(const ValueKey('alarm-fallback-station-field')),
);
await tester.pumpAndSettle();
await tester.tap(
find.byKey(const ValueKey('alarm-fallback-station-field')),
);
await tester.pumpAndSettle();
expect(find.byType(SearchBar), findsOneWidget);
},
);
testWidgets('permite configurar la duracion del snooze (S2-R10)', (
tester,
) async {
final entorno = await _abrirEditor(tester);
await tester.ensureVisible(find.text(l10n.alarmSnoozeDurationTitle));
await tester.pumpAndSettle();
expect(find.byType(SegmentedButton<int>), findsWidgets);
await tester.tap(find.text(l10n.alarmSnoozeOptionLabel(10)));
await tester.pumpAndSettle();
await tester.ensureVisible(find.text(l10n.saveAlarmAction));
await tester.pumpAndSettle();
await tester.tap(find.text(l10n.saveAlarmAction));
await tester.pumpAndSettle();
expect(entorno.estadoAlarmas.alarmas.single.snoozeMinutos, 10);
});
testWidgets('el slider de volumen permite bajar hasta 0.0 (S2-R11)', (
tester,
) async {
await _abrirEditor(tester);
final sliders = tester.widgetList<Slider>(find.byType(Slider));
final volumen = sliders.firstWhere((slider) => slider.max == 1.0);
expect(volumen.min, 0.0);
});
group('WU8 — tarjeta de alarma simplificada', () {
testWidgets(
'tocar la tarjeta abre el editor precargado con los datos de esa '
'alarma',
(tester) async {
await _montarDosAlarmas(tester);
await tester.tap(find.byKey(const ValueKey('tarjeta-alarma-r2')));
await tester.pumpAndSettle();
expect(find.text(l10n.editAlarmTitle), findsOneWidget);
expect(find.text('Segunda'), findsOneWidget);
},
);
testWidgets('deslizar la tarjeta pide confirmacion; cancelar la conserva y '
'confirmar la elimina', (tester) async {
final entorno = await _montarDosAlarmas(tester);
// Cancelar: la tarjeta se conserva.
await tester.drag(
find.byKey(const ValueKey('tarjeta-alarma-r2')),
const Offset(-600, 0),
);
await tester.pumpAndSettle();
expect(find.text(l10n.alarmDeleteConfirmTitle), findsOneWidget);
await tester.tap(find.text(l10n.cancelAction));
await tester.pumpAndSettle();
expect(entorno.estadoAlarmas.alarmas.length, 2);
expect(find.byKey(const ValueKey('tarjeta-alarma-r2')), findsOneWidget);
// Confirmar: la tarjeta se elimina.
await tester.drag(
find.byKey(const ValueKey('tarjeta-alarma-r2')),
const Offset(-600, 0),
);
await tester.pumpAndSettle();
expect(find.text(l10n.alarmDeleteConfirmTitle), findsOneWidget);
await tester.tap(find.text(l10n.deleteAction));
await tester.pumpAndSettle();
expect(entorno.estadoAlarmas.alarmas.length, 1);
expect(find.byKey(const ValueKey('tarjeta-alarma-r2')), findsNothing);
});
testWidgets(
'el banner destacado permite saltar la alarma mas proxima (accion '
'"Saltar")',
(tester) async {
final entorno = await _montarDosAlarmas(tester);
final proximaAntes = entorno.estadoAlarmas.proximaAlarma;
expect(proximaAntes?.id, 'r1');
await tester.tap(find.byKey(const ValueKey('hero-skip-next')));
await tester.pumpAndSettle();
expect(find.byType(SnackBar), findsOneWidget);
final r1 = entorno.estadoAlarmas.alarmas.firstWhere(
(a) => a.id == 'r1',
);
expect(r1.proximaEjecucion, isNot(proximaAntes!.proximaEjecucion));
},
);
testWidgets('la fila resumen de vacaciones empuja una pantalla al tocarla '
'(destino provisorio; WU9 lo reemplaza)', (tester) async {
await _montarDosAlarmas(tester);
await tester.tap(find.byKey(const ValueKey('vacaciones-resumen')));
await tester.pumpAndSettle();
expect(find.byType(PluriPushScaffold), findsOneWidget);
});
testWidgets(
'guardia de capacidad: editar (tap), saltar (hero) y eliminar (swipe) '
'siguen siendo alcanzables tras la simplificacion de la tarjeta',
(tester) async {
final entorno = await _montarDosAlarmas(tester);
// 1) Saltar, desde el banner destacado.
await tester.tap(find.byKey(const ValueKey('hero-skip-next')));
await tester.pumpAndSettle();
expect(find.byType(SnackBar), findsOneWidget);
// 2) Editar, tocando la tarjeta.
await tester.tap(find.byKey(const ValueKey('tarjeta-alarma-r2')));
await tester.pumpAndSettle();
expect(find.text(l10n.editAlarmTitle), findsOneWidget);
await tester.tap(find.byIcon(Icons.close_rounded));
await tester.pumpAndSettle();
// 3) Eliminar, deslizando y confirmando.
await tester.drag(
find.byKey(const ValueKey('tarjeta-alarma-r2')),
const Offset(-600, 0),
);
await tester.pumpAndSettle();
await tester.tap(find.text(l10n.deleteAction));
await tester.pumpAndSettle();
expect(entorno.estadoAlarmas.alarmas.length, 1);
},
);
});
}