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.
438 lines
13 KiB
Dart
438 lines
13 KiB
Dart
import 'package:flutter_test/flutter_test.dart';
|
|
import 'package:pluriwave/modelos/alarma_musical.dart';
|
|
import 'package:pluriwave/servicios/servicio_alarmas.dart';
|
|
import 'package:pluriwave/servicios/servicio_programacion_alarmas.dart';
|
|
import 'package:shared_preferences/shared_preferences.dart';
|
|
|
|
void main() {
|
|
group('ServicioProgramacionAlarmas', () {
|
|
final servicio = ServicioProgramacionAlarmas();
|
|
|
|
test('calcula la próxima alarma diaria futura', () {
|
|
final alarma = AlarmaMusical(
|
|
id: 'a1',
|
|
nombre: 'Diaria',
|
|
hora: 7,
|
|
minuto: 30,
|
|
tipoProgramacion: TipoProgramacionAlarma.diaria,
|
|
diasSemana: const [],
|
|
);
|
|
|
|
final proxima = servicio.calcularProxima(
|
|
alarma: alarma,
|
|
desde: DateTime(2026, 5, 21, 7),
|
|
);
|
|
|
|
expect(proxima, DateTime(2026, 5, 21, 7, 30));
|
|
});
|
|
|
|
test('salta vacaciones si la alarma no debe sonar esos días', () {
|
|
final alarma = AlarmaMusical(
|
|
id: 'a2',
|
|
nombre: 'Laboral',
|
|
hora: 8,
|
|
minuto: 0,
|
|
tipoProgramacion: TipoProgramacionAlarma.diaria,
|
|
diasSemana: const [],
|
|
sonarEnVacaciones: false,
|
|
);
|
|
|
|
final proxima = servicio.calcularProxima(
|
|
alarma: alarma,
|
|
desde: DateTime(2026, 8, 1, 7),
|
|
vacaciones: [
|
|
RangoVacaciones(
|
|
id: 'v1',
|
|
nombre: 'Verano',
|
|
inicio: DateTime(2026, 8),
|
|
fin: DateTime(2026, 8, 3),
|
|
),
|
|
],
|
|
);
|
|
|
|
expect(proxima, DateTime(2026, 8, 4, 8));
|
|
});
|
|
|
|
test('saltar próxima solo omite esa ejecución', () {
|
|
final alarma = AlarmaMusical(
|
|
id: 'a3',
|
|
nombre: 'Diaria',
|
|
hora: 9,
|
|
minuto: 0,
|
|
tipoProgramacion: TipoProgramacionAlarma.diaria,
|
|
diasSemana: const [],
|
|
);
|
|
final omitida = DateTime(2026, 5, 22, 9);
|
|
|
|
final proxima = servicio.calcularProxima(
|
|
alarma: alarma,
|
|
desde: DateTime(2026, 5, 22, 8),
|
|
excepciones: [
|
|
ExcepcionAlarma(alarmaId: 'a3', ejecucion: omitida, tipo: 'skipNext'),
|
|
],
|
|
);
|
|
|
|
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);
|
|
|
|
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',
|
|
);
|
|
});
|
|
|
|
test('snooze solo permite 3, 5 o 10 minutos y cae a 5', () {
|
|
expect(
|
|
servicio.calcularSnooze(DateTime(2026, 5, 21, 7), 10),
|
|
DateTime(2026, 5, 21, 7, 10),
|
|
);
|
|
expect(
|
|
servicio.calcularSnooze(DateTime(2026, 5, 21, 7), 99),
|
|
DateTime(2026, 5, 21, 7, 5),
|
|
);
|
|
});
|
|
|
|
test(
|
|
'servicio limpia proxima ejecucion obsoleta al recalcular unica vencida',
|
|
() async {
|
|
SharedPreferences.setMockInitialValues({});
|
|
final servicioAlarmas = ServicioAlarmas(
|
|
reloj: () => DateTime(2026, 5, 22, 10),
|
|
);
|
|
final alarma = AlarmaMusical(
|
|
id: 'a4',
|
|
nombre: 'Unica vencida',
|
|
hora: 9,
|
|
minuto: 0,
|
|
tipoProgramacion: TipoProgramacionAlarma.unica,
|
|
diasSemana: const [],
|
|
fechaUnica: DateTime(2026, 5, 22),
|
|
proximaEjecucion: DateTime(2026, 5, 22, 9),
|
|
);
|
|
|
|
final guardada = await servicioAlarmas.guardarAlarma(alarma);
|
|
|
|
expect(guardada.alarmas.single.proximaEjecucion, isNull);
|
|
},
|
|
);
|
|
|
|
test('mantiene alarma unica creada dentro del mismo minuto', () {
|
|
final alarma = AlarmaMusical(
|
|
id: 'a5',
|
|
nombre: 'Ahora',
|
|
hora: 20,
|
|
minuto: 13,
|
|
tipoProgramacion: TipoProgramacionAlarma.unica,
|
|
diasSemana: const [],
|
|
fechaUnica: DateTime(2026, 5, 23),
|
|
);
|
|
|
|
final proxima = servicio.calcularProxima(
|
|
alarma: alarma,
|
|
desde: DateTime(2026, 5, 23, 20, 13, 45),
|
|
);
|
|
|
|
expect(proxima, DateTime(2026, 5, 23, 20, 13, 47));
|
|
});
|
|
|
|
test('calcula siguiente diaria despues de ejecucion completada', () {
|
|
final alarma = AlarmaMusical(
|
|
id: 'a6',
|
|
nombre: 'Diaria',
|
|
hora: 7,
|
|
minuto: 30,
|
|
tipoProgramacion: TipoProgramacionAlarma.diaria,
|
|
diasSemana: const [],
|
|
proximaEjecucion: DateTime(2026, 5, 25, 7, 30),
|
|
);
|
|
|
|
final siguiente = servicio.calcularSiguienteDespuesDeEjecucion(
|
|
alarma: alarma,
|
|
ejecucion: DateTime(2026, 5, 25, 7, 30),
|
|
);
|
|
|
|
expect(siguiente, DateTime(2026, 5, 26, 7, 30));
|
|
});
|
|
|
|
test('no reutiliza ejecucion gestionada aunque siga en tolerancia', () {
|
|
final alarma = AlarmaMusical(
|
|
id: 'a9',
|
|
nombre: 'Diaria gestionada',
|
|
hora: 7,
|
|
minuto: 30,
|
|
tipoProgramacion: TipoProgramacionAlarma.diaria,
|
|
diasSemana: const [],
|
|
ultimaEjecucionGestionada: DateTime(2026, 5, 25, 7, 30),
|
|
);
|
|
|
|
final proxima = servicio.calcularProxima(
|
|
alarma: alarma,
|
|
desde: DateTime(2026, 5, 25, 7, 30, 20),
|
|
);
|
|
|
|
expect(proxima, DateTime(2026, 5, 26, 7, 30));
|
|
});
|
|
|
|
test('la hora de pared nunca deriva al iterar dias (DST-safe): diaria', () {
|
|
// Property lock for the DST drift bug: advancing candidates with
|
|
// add(Duration(days: 1)) shifts the ABSOLUTE instant by 86400s, so on
|
|
// DST-observing machines (this repo's dev machine runs Europe/Madrid)
|
|
// crossing 2026-03-29 / 2026-10-25 drifted the wall hour +-1h
|
|
// permanently. Iterating a full year of next-occurrences must keep
|
|
// every result at exactly hora:minuto.
|
|
final alarma = AlarmaMusical(
|
|
id: 'dst-diaria',
|
|
nombre: 'Diaria DST',
|
|
hora: 7,
|
|
minuto: 30,
|
|
tipoProgramacion: TipoProgramacionAlarma.diaria,
|
|
diasSemana: const [],
|
|
);
|
|
|
|
var proxima =
|
|
servicio.calcularProxima(
|
|
alarma: alarma,
|
|
desde: DateTime(2026, 1, 5, 6, 0),
|
|
)!;
|
|
for (var i = 0; i < 400; i++) {
|
|
expect(
|
|
proxima.hour,
|
|
7,
|
|
reason: 'deriva de hora de pared en ${proxima.toIso8601String()}',
|
|
);
|
|
expect(proxima.minute, 30);
|
|
proxima =
|
|
servicio.calcularSiguienteDespuesDeEjecucion(
|
|
alarma: alarma,
|
|
ejecucion: proxima,
|
|
)!;
|
|
}
|
|
});
|
|
|
|
test('la hora de pared nunca deriva al iterar dias (DST-safe): '
|
|
'dias de semana', () {
|
|
final alarma = AlarmaMusical(
|
|
id: 'dst-semana',
|
|
nombre: 'Semana DST',
|
|
hora: 7,
|
|
minuto: 30,
|
|
tipoProgramacion: TipoProgramacionAlarma.diasSemana,
|
|
diasSemana: const [1, 3, 5],
|
|
);
|
|
|
|
var proxima =
|
|
servicio.calcularProxima(
|
|
alarma: alarma,
|
|
desde: DateTime(2026, 1, 5, 6, 0),
|
|
)!;
|
|
for (var i = 0; i < 200; i++) {
|
|
expect(
|
|
proxima.hour,
|
|
7,
|
|
reason: 'deriva de hora de pared en ${proxima.toIso8601String()}',
|
|
);
|
|
expect(proxima.minute, 30);
|
|
expect(const [1, 3, 5], contains(proxima.weekday));
|
|
proxima =
|
|
servicio.calcularSiguienteDespuesDeEjecucion(
|
|
alarma: alarma,
|
|
ejecucion: proxima,
|
|
)!;
|
|
}
|
|
});
|
|
|
|
test('mantiene la ocurrencia diaria cuyo disparo acaba de pasar', () {
|
|
// Contract the native AlarmScheduler.computeNextTriggerMillis MUST
|
|
// mirror: a trigger that just passed within toleranciaDisparoInminente
|
|
// (90s) and has NOT been handled is kept (normalized to desde + 2s),
|
|
// never skipped to tomorrow. The native side lacking this window is
|
|
// what recomputed the next occurrence as tomorrow and cancelled the
|
|
// in-flight fire, so the alarm never rang until the screen was turned
|
|
// on. If this expectation changes, the native constant
|
|
// IMMINENT_TOLERANCE_MILLIS must change with it.
|
|
final alarma = AlarmaMusical(
|
|
id: 'a-inminente-diaria',
|
|
nombre: 'Diaria inminente',
|
|
hora: 22,
|
|
minuto: 35,
|
|
tipoProgramacion: TipoProgramacionAlarma.diaria,
|
|
diasSemana: const [],
|
|
);
|
|
|
|
final proxima = servicio.calcularProxima(
|
|
alarma: alarma,
|
|
desde: DateTime(2026, 7, 11, 22, 35, 30),
|
|
);
|
|
|
|
expect(proxima, DateTime(2026, 7, 11, 22, 35, 32));
|
|
});
|
|
|
|
test('mantiene la ocurrencia por dias de semana recien pasada', () {
|
|
final alarma = AlarmaMusical(
|
|
id: 'a-inminente-semana',
|
|
nombre: 'Semana inminente',
|
|
hora: 22,
|
|
minuto: 35,
|
|
tipoProgramacion: TipoProgramacionAlarma.diasSemana,
|
|
diasSemana: const [1, 2, 3, 4, 5, 6, 7],
|
|
);
|
|
|
|
final proxima = servicio.calcularProxima(
|
|
alarma: alarma,
|
|
desde: DateTime(2026, 7, 11, 22, 35, 30),
|
|
);
|
|
|
|
expect(proxima, DateTime(2026, 7, 11, 22, 35, 32));
|
|
});
|
|
|
|
test('calcula siguiente por dias de semana despues de ejecucion', () {
|
|
final alarma = AlarmaMusical(
|
|
id: 'a7',
|
|
nombre: 'Laboral',
|
|
hora: 8,
|
|
minuto: 0,
|
|
tipoProgramacion: TipoProgramacionAlarma.diasSemana,
|
|
diasSemana: const [DateTime.monday, DateTime.wednesday],
|
|
proximaEjecucion: DateTime(2026, 5, 25, 8),
|
|
);
|
|
|
|
final siguiente = servicio.calcularSiguienteDespuesDeEjecucion(
|
|
alarma: alarma,
|
|
ejecucion: DateTime(2026, 5, 25, 8),
|
|
);
|
|
|
|
expect(siguiente, DateTime(2026, 5, 27, 8));
|
|
});
|
|
|
|
test('alarma unica completada no calcula siguiente', () {
|
|
final alarma = AlarmaMusical(
|
|
id: 'a8',
|
|
nombre: 'Unica',
|
|
hora: 8,
|
|
minuto: 0,
|
|
tipoProgramacion: TipoProgramacionAlarma.unica,
|
|
diasSemana: const [],
|
|
fechaUnica: DateTime(2026, 5, 25),
|
|
proximaEjecucion: DateTime(2026, 5, 25, 8),
|
|
);
|
|
|
|
final siguiente = servicio.calcularSiguienteDespuesDeEjecucion(
|
|
alarma: alarma,
|
|
ejecucion: DateTime(2026, 5, 25, 8),
|
|
);
|
|
|
|
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),
|
|
);
|
|
});
|
|
});
|
|
}
|