fix(alarmas): una alarma de un solo uso a una hora ya pasada suena manana
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:
@@ -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));
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@@ -75,41 +75,38 @@ void main() {
|
||||
expect(proxima, DateTime(2026, 5, 23, 9));
|
||||
});
|
||||
|
||||
test(
|
||||
'un registro de fallo de programacion NO omite esa ejecucion (solo '
|
||||
'skipNext debe hacerlo)',
|
||||
() {
|
||||
final alarma = AlarmaMusical(
|
||||
id: 'a4',
|
||||
nombre: 'Diaria',
|
||||
hora: 9,
|
||||
minuto: 0,
|
||||
tipoProgramacion: TipoProgramacionAlarma.diaria,
|
||||
diasSemana: const [],
|
||||
);
|
||||
final ocurrencia = DateTime(2026, 5, 22, 9);
|
||||
test('un registro de fallo de programacion NO omite esa ejecucion (solo '
|
||||
'skipNext debe hacerlo)', () {
|
||||
final alarma = AlarmaMusical(
|
||||
id: 'a4',
|
||||
nombre: 'Diaria',
|
||||
hora: 9,
|
||||
minuto: 0,
|
||||
tipoProgramacion: TipoProgramacionAlarma.diaria,
|
||||
diasSemana: const [],
|
||||
);
|
||||
final ocurrencia = DateTime(2026, 5, 22, 9);
|
||||
|
||||
final proxima = servicio.calcularProxima(
|
||||
alarma: alarma,
|
||||
desde: DateTime(2026, 5, 22, 8),
|
||||
excepciones: [
|
||||
ExcepcionAlarma(
|
||||
alarmaId: 'a4',
|
||||
ejecucion: ocurrencia,
|
||||
tipo: ExcepcionAlarma.tipoFalloProgramacion,
|
||||
),
|
||||
],
|
||||
);
|
||||
final proxima = servicio.calcularProxima(
|
||||
alarma: alarma,
|
||||
desde: DateTime(2026, 5, 22, 8),
|
||||
excepciones: [
|
||||
ExcepcionAlarma(
|
||||
alarmaId: 'a4',
|
||||
ejecucion: ocurrencia,
|
||||
tipo: ExcepcionAlarma.tipoFalloProgramacion,
|
||||
),
|
||||
],
|
||||
);
|
||||
|
||||
expect(
|
||||
proxima,
|
||||
ocurrencia,
|
||||
reason:
|
||||
'un fallo de programacion registrado no debe comportarse '
|
||||
'como un salto de usuario',
|
||||
);
|
||||
},
|
||||
);
|
||||
expect(
|
||||
proxima,
|
||||
ocurrencia,
|
||||
reason:
|
||||
'un fallo de programacion registrado no debe comportarse '
|
||||
'como un salto de usuario',
|
||||
);
|
||||
});
|
||||
|
||||
test('snooze solo permite 3, 5 o 10 minutos y cae a 5', () {
|
||||
expect(
|
||||
@@ -354,4 +351,87 @@ void main() {
|
||||
expect(siguiente, isNull);
|
||||
});
|
||||
});
|
||||
|
||||
group('ServicioProgramacionAlarmas.normalizarFechaUnica', () {
|
||||
final servicio = ServicioProgramacionAlarmas();
|
||||
|
||||
test('rolls to tomorrow when the chosen time already passed today', () {
|
||||
final fecha = servicio.normalizarFechaUnica(
|
||||
fecha: DateTime(2026, 9, 22),
|
||||
hora: 5,
|
||||
minuto: 30,
|
||||
ahora: DateTime(2026, 9, 22, 18, 30),
|
||||
);
|
||||
|
||||
expect(fecha, DateTime(2026, 9, 23, 5, 30));
|
||||
});
|
||||
|
||||
test('keeps today when the chosen time is still ahead', () {
|
||||
final fecha = servicio.normalizarFechaUnica(
|
||||
fecha: DateTime(2026, 9, 22),
|
||||
hora: 21,
|
||||
minuto: 45,
|
||||
ahora: DateTime(2026, 9, 22, 18, 30),
|
||||
);
|
||||
|
||||
expect(fecha, DateTime(2026, 9, 22, 21, 45));
|
||||
});
|
||||
|
||||
test('never overrides a date the user deliberately picked ahead', () {
|
||||
final fecha = servicio.normalizarFechaUnica(
|
||||
fecha: DateTime(2026, 9, 27),
|
||||
hora: 5,
|
||||
minuto: 30,
|
||||
ahora: DateTime(2026, 9, 22, 18, 30),
|
||||
);
|
||||
|
||||
expect(fecha, DateTime(2026, 9, 27, 5, 30));
|
||||
});
|
||||
|
||||
test('snaps a long-stale date to today or tomorrow, not to date + 1', () {
|
||||
final fecha = servicio.normalizarFechaUnica(
|
||||
fecha: DateTime(2026, 9, 10),
|
||||
hora: 5,
|
||||
minuto: 30,
|
||||
ahora: DateTime(2026, 9, 22, 18, 30),
|
||||
);
|
||||
|
||||
expect(fecha, DateTime(2026, 9, 23, 5, 30));
|
||||
});
|
||||
|
||||
test('stays today inside the imminent-trigger tolerance', () {
|
||||
final fecha = servicio.normalizarFechaUnica(
|
||||
fecha: DateTime(2026, 9, 22),
|
||||
hora: 18,
|
||||
minuto: 30,
|
||||
ahora: DateTime(2026, 9, 22, 18, 30, 45),
|
||||
);
|
||||
|
||||
expect(fecha, DateTime(2026, 9, 22, 18, 30));
|
||||
});
|
||||
|
||||
test('the rolled date is what calcularProxima can actually schedule', () {
|
||||
final ahora = DateTime(2026, 9, 22, 18, 30);
|
||||
final fecha = servicio.normalizarFechaUnica(
|
||||
fecha: DateTime(2026, 9, 22),
|
||||
hora: 5,
|
||||
minuto: 30,
|
||||
ahora: ahora,
|
||||
);
|
||||
final alarma = AlarmaMusical(
|
||||
id: 'unica-rodada',
|
||||
nombre: 'Unica',
|
||||
hora: 5,
|
||||
minuto: 30,
|
||||
tipoProgramacion: TipoProgramacionAlarma.unica,
|
||||
diasSemana: const [],
|
||||
fechaUnica: fecha,
|
||||
);
|
||||
|
||||
expect(
|
||||
servicio.calcularProxima(alarma: alarma, desde: ahora),
|
||||
DateTime(2026, 9, 23, 5, 30),
|
||||
);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user