fix(alarmas): una alarma de un solo uso a una hora ya pasada suena manana
Build & Deploy PluriWave / Análisis de código (push) Successful in 32s
Build & Deploy PluriWave / Build APK + AAB release (push) Successful in 3m11s

El editor dejaba la fecha clavada en hoy, asi que al escribir 05:30 siendo
las 18:30 el candidato quedaba en el pasado, `calcularProxima` lo rechazaba
con razon y la alarma se guardaba sin proxima ejecucion: nunca sonaba. En la
practica solo se podian poner alarmas unicas para lo que quedaba de dia.

`normalizarFechaUnica` resuelve el dia que el usuario realmente quiere, con
la convencion de cualquier despertador: hora ya pasada -> manana, hora por
llegar -> hoy. Una fecha elegida a proposito en el futuro no se toca nunca,
y una fecha rancia salta a hoy/manana en vez de a `fecha + 1`, que seguiria
en el pasado.

La regla vive en el editor y NO dentro de `calcularProxima`: esa tiene que
seguir siendo literal, porque el recalculo posterior al disparo y el motor
nativo dependen de que una alarma unica vencida resuelva a null en vez de
resucitar al dia siguiente.

De paso el editor pasa a leer el reloj inyectado del servicio en vez de
`DateTime.now()`, para que la vista previa, los limites del selector de
fecha y el ajuste lean el mismo instante y se puedan fijar en las pruebas.
This commit is contained in:
2026-09-22 13:14:32 +02:00
parent 2891a5703e
commit 44af98eac3
4 changed files with 272 additions and 41 deletions
@@ -46,7 +46,10 @@ void main() {
SharedPreferences.setMockInitialValues({});
});
Future<EstadoAlarmas> abrirEditorNuevo(WidgetTester tester) async {
Future<EstadoAlarmas> abrirEditorNuevo(
WidgetTester tester, {
DateTime Function()? reloj,
}) async {
tester.view.physicalSize = const Size(1440, 3200);
tester.view.devicePixelRatio = 1.0;
addTearDown(tester.view.resetPhysicalSize);
@@ -69,7 +72,7 @@ void main() {
final android = FakePuertoAlarmasAndroid();
final estadoAlarmas = EstadoAlarmas(
esPremium: () => true,
servicio: ServicioAlarmas(reloj: DateTime.now),
servicio: ServicioAlarmas(reloj: reloj ?? DateTime.now),
android: android,
iniciarAutomaticamente: false,
);
@@ -218,5 +221,84 @@ void main() {
expect(alarma.sonidoInterno, SonidoInternoAlarma.campanaSuave);
},
);
// The 18:30 -> 05:30 report: with a real clock the suite could only ever
// assert vague invariants, so the editor reads the alarm service's
// injected clock and these two pin it to a fixed wall-clock instant.
DateTime tardeDelMartes() => DateTime(2026, 9, 22, 18, 30);
/// Taps the hour segment [veces] times. Tap increments and wraps, so 11
/// taps move 18h -> 05h. A vertical drag cannot be used here: the sheet
/// scrolls, and the scrollable wins the gesture arena.
Future<void> subirHora(WidgetTester tester, int veces) async {
final hora = find.byKey(const ValueKey('editor-hora-inline-hora'));
await tester.ensureVisible(hora);
for (var i = 0; i < veces; i++) {
await tester.tap(hora);
await tester.pump();
}
await tester.pumpAndSettle();
}
testWidgets('una alarma de un unico uso puesta a una hora ya pasada hoy se '
'programa para manana, no se queda sin proxima ejecucion', (
tester,
) async {
final estadoAlarmas = await abrirEditorNuevo(
tester,
reloj: tardeDelMartes,
);
// Opens at 18:35 (now + 5 min); 11 taps wrap the hour to 05:35,
// which already passed today.
await subirHora(tester, 11);
await tester.ensureVisible(find.text(l10n.saveAlarmAction));
await tester.tap(find.text(l10n.saveAlarmAction));
await tester.pumpAndSettle();
final alarma = estadoAlarmas.alarmas.single;
expect(alarma.tipoProgramacion, TipoProgramacionAlarma.unica);
expect(alarma.hora, 5);
expect(alarma.minuto, 35);
expect(alarma.proximaProgramable, DateTime(2026, 9, 23, 5, 35));
});
testWidgets(
'una hora todavia por llegar sigue sonando hoy mismo, no manana',
(tester) async {
final estadoAlarmas = await abrirEditorNuevo(
tester,
reloj: tardeDelMartes,
);
// 18:35 + 3 taps = 21:35, still ahead of 18:30.
await subirHora(tester, 3);
await tester.ensureVisible(find.text(l10n.saveAlarmAction));
await tester.tap(find.text(l10n.saveAlarmAction));
await tester.pumpAndSettle();
expect(
estadoAlarmas.alarmas.single.proximaProgramable,
DateTime(2026, 9, 22, 21, 35),
);
},
);
testWidgets('la vista previa deja de decir "sin proxima ejecucion" '
'cuando la hora ya paso hoy', (tester) async {
await abrirEditorNuevo(tester, reloj: tardeDelMartes);
await subirHora(tester, 11);
final aviso = tester.widget<Text>(
find.descendant(
of: find.byKey(const ValueKey('next-trigger-preview')),
matching: find.byType(Text),
),
);
expect(aviso.data, isNot(l10n.alarmNoNextExecution));
});
});
}