Files
pluriwave/test/pantallas/pantalla_alarmas_editor_test.dart
T
FreeTLab a2121d84bd feat(alarmas): rewrite alarm editor with inline time widget
Replace the native showTimePicker dialog in the alarm editor sheet with
a giant inline HH:MM editor (drag/tap to adjust, wraps at 23:59-00:00).
Weekday circles now render unconditionally (disabled outside weekly
mode) instead of being gated behind an `if`.

The date field, fallback-station picker, and sound dropdown are not
dropped: per resolution 3 they move into a collapsed "Advanced" section
so the mockup's weekday-circles-only layout does not lose capability.
Volume/fade-in sliders get a cosmetic type-scale restyle only.

size:exception: 993 changed lines (891+/102-) against the 500-650
forecast - lib/ production code alone is 429 lines, within band; new
test files and 13 regenerated l10n/gen files account for the rest, the
same pattern every prior work unit in this branch has hit.
2026-07-29 11:43:15 +02:00

403 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);
// WU10 correction: the fallback-station field moved into the
// collapsed "Advanced" section (native-alarms delta — Alarm Editor
// Preserves Date, Fallback Station, and Sound Fields). It must be
// expanded first — a collapsed `ExpansionTile` does not build its
// children, so `find.byKey` would otherwise find nothing.
await tester.ensureVisible(find.text(l10n.alarmAdvancedSectionTitle));
await tester.tap(find.text(l10n.alarmAdvancedSectionTitle));
await tester.pumpAndSettle();
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);
},
);
});
}