ExcepcionAlarma._esValida matched ANY exception tipo against an occurrence, treating it as a user skip. Only the 'skipNext' tipo existed until now, but the next commits reuse the same model to record scheduling-reliability failures per alarm (so the alarms list can surface them via ultimaExcepcionPara) -- without this guard, a recorded failure would be silently treated as if the user asked to skip that occurrence, corrupting scheduling. Adds tipo constants to ExcepcionAlarma for the upcoming failure kinds.
358 lines
11 KiB
Dart
358 lines
11 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);
|
|
});
|
|
});
|
|
}
|