Files
pluriwave/test/estado/estado_alarmas_test.dart
T
FreeTLab fd1b91fe9e fix(alarmas): wire scheduling failures into per-alarm exceptions
guardarAlarma/posponerAlarma/posponerProximaDesdePreaviso now record
a scheduling failure via ServicioAlarmas.registrarFalloProgramacion
on catch and clear it on a successful (re)schedule, in addition to
the existing transient EstadoAlarmas.error string. This makes the
failure visible per-alarm via ultimaExcepcionPara instead of only a
generic app-wide message.

Also fixes _sincronizarTodas: a single alarm's android.programar
throw used to abort the whole loop, silently skipping every sibling
alarm scheduled AFTER it on that pass (including on every app launch,
via inicializar). Each alarm's outcome is now independent.
2026-07-31 21:08:21 +02:00

967 lines
28 KiB
Dart

import 'dart:async';
import 'package:flutter_test/flutter_test.dart';
import 'package:pluriwave/estado/estado_alarmas.dart';
import 'package:pluriwave/modelos/alarma_musical.dart';
import 'package:pluriwave/servicios/servicio_alarmas.dart';
import 'package:pluriwave/servicios/servicio_alarmas_android.dart';
import 'package:shared_preferences/shared_preferences.dart';
import '../helpers/fakes_alarmas.dart';
void main() {
TestWidgetsFlutterBinding.ensureInitialized();
setUp(() {
SharedPreferences.setMockInitialValues({});
});
test('posponer persiste snooze y refrescarProgramacion no lo pisa', () async {
var ahora = DateTime(2026, 5, 25, 7, 31);
final android = FakePuertoAlarmasAndroid();
final estado = EstadoAlarmas(
servicio: ServicioAlarmas(reloj: () => ahora),
android: android,
iniciarAutomaticamente: false,
);
addTearDown(estado.dispose);
addTearDown(android.dispose);
await estado.guardarAlarma(
const AlarmaMusical(
id: 'a1',
nombre: 'Diaria',
hora: 7,
minuto: 30,
tipoProgramacion: TipoProgramacionAlarma.diaria,
diasSemana: [],
),
);
final alarma = estado.alarmas.single;
await estado.posponerAlarma(alarma, 5);
// Ancla unificada (S2-R6): proximaEjecucion (7:31:02, normalizada por
// inminencia) + 5 minutos — ya no "ahora + 5".
expect(estado.alarmas.single.snoozeHasta, DateTime(2026, 5, 25, 7, 36, 2));
expect(
android.programadas.last.proximaProgramable,
DateTime(2026, 5, 25, 7, 36, 2),
);
ahora = DateTime(2026, 5, 25, 7, 32);
await estado.refrescarProgramacion();
expect(estado.alarmas.single.snoozeHasta, DateTime(2026, 5, 25, 7, 36, 2));
expect(
android.programadas.last.proximaProgramable,
DateTime(2026, 5, 25, 7, 36, 2),
);
});
test(
'posponer desde preaviso mueve esta ejecucion desde la hora original',
() async {
final android = FakePuertoAlarmasAndroid();
final estado = EstadoAlarmas(
servicio: ServicioAlarmas(reloj: () => DateTime(2026, 5, 25, 7)),
android: android,
iniciarAutomaticamente: false,
);
addTearDown(estado.dispose);
addTearDown(android.dispose);
await estado.guardarAlarma(
AlarmaMusical(
id: 'pre1',
nombre: 'Preaviso',
hora: 7,
minuto: 30,
tipoProgramacion: TipoProgramacionAlarma.diaria,
diasSemana: const [],
proximaEjecucion: DateTime(2026, 5, 25, 7, 30),
),
);
final alarma = estado.alarmas.single;
await estado.posponerProximaDesdePreaviso(
alarma,
10,
DateTime(2026, 5, 25, 7, 30),
);
expect(estado.alarmas.single.snoozeHasta, DateTime(2026, 5, 25, 7, 40));
expect(estado.alarmas.single.snoozeOrigen, DateTime(2026, 5, 25, 7, 30));
expect(
android.programadas.last.proximaProgramable,
DateTime(2026, 5, 25, 7, 40),
);
},
);
test('finalizar diaria calcula siguiente dia y limpia snooze', () async {
final android = FakePuertoAlarmasAndroid();
final estado = EstadoAlarmas(
servicio: ServicioAlarmas(reloj: () => DateTime(2026, 5, 25, 7, 31)),
android: android,
iniciarAutomaticamente: false,
);
addTearDown(estado.dispose);
addTearDown(android.dispose);
await estado.guardarAlarma(
AlarmaMusical(
id: 'a2',
nombre: 'Diaria',
hora: 7,
minuto: 30,
tipoProgramacion: TipoProgramacionAlarma.diaria,
diasSemana: const [],
proximaEjecucion: DateTime(2026, 5, 25, 7, 30),
snoozeHasta: DateTime(2026, 5, 25, 7, 36),
snoozeOrigen: DateTime(2026, 5, 25, 7, 30),
),
);
await estado.finalizarEjecucion('a2');
expect(estado.alarmas.single.snoozeHasta, isNull);
expect(
estado.alarmas.single.proximaEjecucion,
DateTime(2026, 5, 26, 7, 30),
);
});
test('finalizar unica la desactiva y queda sin proxima ejecucion', () async {
final android = FakePuertoAlarmasAndroid();
final estado = EstadoAlarmas(
servicio: ServicioAlarmas(reloj: () => DateTime(2026, 5, 25, 7, 31)),
android: android,
iniciarAutomaticamente: false,
);
addTearDown(estado.dispose);
addTearDown(android.dispose);
await estado.guardarAlarma(
AlarmaMusical(
id: 'a3',
nombre: 'Unica',
hora: 7,
minuto: 30,
tipoProgramacion: TipoProgramacionAlarma.unica,
diasSemana: const [],
fechaUnica: DateTime(2026, 5, 25),
proximaEjecucion: DateTime(2026, 5, 25, 7, 30),
),
);
await estado.finalizarEjecucion('a3');
expect(estado.alarmas.single.activa, isFalse);
expect(estado.alarmas.single.proximaEjecucion, isNull);
});
test(
'solicita exencion de bateria una sola vez cuando no esta exenta',
() async {
final android =
FakePuertoAlarmasAndroid()..ignoraOptimizacionBateria = false;
final estado = EstadoAlarmas(
servicio: ServicioAlarmas(reloj: () => DateTime(2026, 5, 25, 7)),
android: android,
iniciarAutomaticamente: false,
);
addTearDown(estado.dispose);
addTearDown(android.dispose);
await estado.guardarAlarma(
const AlarmaMusical(
id: 'bat1',
nombre: 'Bateria',
hora: 7,
minuto: 30,
tipoProgramacion: TipoProgramacionAlarma.diaria,
diasSemana: [],
),
);
expect(android.solicitudesExencionBateria, 1);
await estado.guardarAlarma(
const AlarmaMusical(
id: 'bat2',
nombre: 'Bateria 2',
hora: 8,
minuto: 0,
tipoProgramacion: TipoProgramacionAlarma.diaria,
diasSemana: [],
),
);
expect(android.solicitudesExencionBateria, 1);
},
);
test('no solicita exencion de bateria cuando ya esta exenta', () async {
final android = FakePuertoAlarmasAndroid();
final estado = EstadoAlarmas(
servicio: ServicioAlarmas(reloj: () => DateTime(2026, 5, 25, 7)),
android: android,
iniciarAutomaticamente: false,
);
addTearDown(estado.dispose);
addTearDown(android.dispose);
await estado.guardarAlarma(
const AlarmaMusical(
id: 'bat3',
nombre: 'Exenta',
hora: 7,
minuto: 30,
tipoProgramacion: TipoProgramacionAlarma.diaria,
diasSemana: [],
),
);
expect(android.solicitudesExencionBateria, 0);
});
test(
'cambiarActiva(false) detiene el audio cuando la alarma esta sonando (SS-1a)',
() async {
final android = FakePuertoAlarmasAndroid();
final estado = EstadoAlarmas(
servicio: ServicioAlarmas(reloj: () => DateTime(2026, 5, 25, 7, 31)),
android: android,
iniciarAutomaticamente: false,
);
addTearDown(estado.dispose);
addTearDown(android.dispose);
await estado.guardarAlarma(
const AlarmaMusical(
id: 'ring1',
nombre: 'Sonando',
hora: 7,
minuto: 30,
tipoProgramacion: TipoProgramacionAlarma.diaria,
diasSemana: [],
),
);
android.alarmaSonandoIdValor = 'ring1';
await estado.cambiarActiva(estado.alarmas.single, false);
expect(android.detencionesActivas, contains('ring1'));
},
);
test(
'guardarAlarma detiene el audio cuando edita la alarma sonando (SS-1b)',
() async {
final android = FakePuertoAlarmasAndroid();
final estado = EstadoAlarmas(
servicio: ServicioAlarmas(reloj: () => DateTime(2026, 5, 25, 7, 31)),
android: android,
iniciarAutomaticamente: false,
);
addTearDown(estado.dispose);
addTearDown(android.dispose);
await estado.guardarAlarma(
const AlarmaMusical(
id: 'ring2',
nombre: 'Sonando',
hora: 7,
minuto: 30,
tipoProgramacion: TipoProgramacionAlarma.diaria,
diasSemana: [],
),
);
android.alarmaSonandoIdValor = 'ring2';
await estado.guardarAlarma(estado.alarmas.single.copyWith(minuto: 45));
expect(android.detencionesActivas, contains('ring2'));
},
);
test('eliminarAlarma detiene el audio antes de cancelar cuando esta sonando '
'(SS-1c, guardia de regresion)', () async {
final android = FakePuertoAlarmasAndroid();
final estado = EstadoAlarmas(
servicio: ServicioAlarmas(reloj: () => DateTime(2026, 5, 25, 7, 31)),
android: android,
iniciarAutomaticamente: false,
);
addTearDown(estado.dispose);
addTearDown(android.dispose);
await estado.guardarAlarma(
const AlarmaMusical(
id: 'ring3',
nombre: 'Sonando',
hora: 7,
minuto: 30,
tipoProgramacion: TipoProgramacionAlarma.diaria,
diasSemana: [],
),
);
android.alarmaSonandoIdValor = 'ring3';
await estado.eliminarAlarma('ring3');
expect(android.detencionesActivas, contains('ring3'));
expect(android.canceladas, contains('ring3'));
});
test('eliminarAlarma usa detenerSonidoNativo cuando la consulta de sonando '
'falla (fail-toward-silence, regresion de eliminarAlarma)', () async {
final android = FakePuertoAlarmasAndroid();
final estado = EstadoAlarmas(
servicio: ServicioAlarmas(reloj: () => DateTime(2026, 5, 25, 7, 31)),
android: android,
iniciarAutomaticamente: false,
);
addTearDown(estado.dispose);
addTearDown(android.dispose);
await estado.guardarAlarma(
const AlarmaMusical(
id: 'ring4',
nombre: 'Sonando',
hora: 7,
minuto: 30,
tipoProgramacion: TipoProgramacionAlarma.diaria,
diasSemana: [],
),
);
android.fallaConsultaSonando = true;
await estado.eliminarAlarma('ring4');
expect(android.detenidas, contains('ring4'));
expect(android.canceladas, contains('ring4'));
});
test(
'guardarAlarma (deshabilitar) usa detenerSonidoNativo cuando la consulta '
'de sonando falla (fail-toward-silence)',
() async {
final android = FakePuertoAlarmasAndroid();
final estado = EstadoAlarmas(
servicio: ServicioAlarmas(reloj: () => DateTime(2026, 5, 25, 7, 31)),
android: android,
iniciarAutomaticamente: false,
);
addTearDown(estado.dispose);
addTearDown(android.dispose);
await estado.guardarAlarma(
const AlarmaMusical(
id: 'ring5',
nombre: 'Sonando',
hora: 7,
minuto: 30,
tipoProgramacion: TipoProgramacionAlarma.diaria,
diasSemana: [],
),
);
android.fallaConsultaSonando = true;
await estado.cambiarActiva(estado.alarmas.single, false);
expect(android.detenidas, contains('ring5'));
},
);
test(
'mutar una alarma distinta a la que suena no dispara el guard (SS-1d)',
() async {
final android = FakePuertoAlarmasAndroid();
final estado = EstadoAlarmas(
servicio: ServicioAlarmas(reloj: () => DateTime(2026, 5, 25, 7, 31)),
android: android,
iniciarAutomaticamente: false,
);
addTearDown(estado.dispose);
addTearDown(android.dispose);
await estado.guardarAlarma(
const AlarmaMusical(
id: 'y1',
nombre: 'Sonando',
hora: 7,
minuto: 30,
tipoProgramacion: TipoProgramacionAlarma.diaria,
diasSemana: [],
),
);
await estado.guardarAlarma(
const AlarmaMusical(
id: 'z1',
nombre: 'Quieta',
hora: 8,
minuto: 0,
tipoProgramacion: TipoProgramacionAlarma.diaria,
diasSemana: [],
),
);
android.alarmaSonandoIdValor = 'y1';
final z1 = estado.alarmas.firstWhere((a) => a.id == 'z1');
await estado.cambiarActiva(z1, false);
expect(android.detencionesActivas, isEmpty);
},
);
test('finalizarEjecucion no registra error cuando el stop nativo se confirma '
'(SS-2a)', () async {
final android = FakePuertoAlarmasAndroid();
final estado = EstadoAlarmas(
servicio: ServicioAlarmas(reloj: () => DateTime(2026, 5, 25, 7, 31)),
android: android,
iniciarAutomaticamente: false,
);
addTearDown(estado.dispose);
addTearDown(android.dispose);
await estado.guardarAlarma(
const AlarmaMusical(
id: 'fin1',
nombre: 'Sonando',
hora: 7,
minuto: 30,
tipoProgramacion: TipoProgramacionAlarma.diaria,
diasSemana: [],
),
);
android.alarmaSonandoIdValor = 'fin1';
await estado.finalizarEjecucion('fin1');
expect(android.detencionesActivas, contains('fin1'));
expect(estado.error, isNull);
});
test('finalizarEjecucion registra error cuando el stop nativo no se confirma '
'(SS-2b)', () async {
final android = FakePuertoAlarmasAndroid()..fallaDetener = true;
final estado = EstadoAlarmas(
servicio: ServicioAlarmas(reloj: () => DateTime(2026, 5, 25, 7, 31)),
android: android,
iniciarAutomaticamente: false,
);
addTearDown(estado.dispose);
addTearDown(android.dispose);
await estado.guardarAlarma(
const AlarmaMusical(
id: 'fin2',
nombre: 'Sonando',
hora: 7,
minuto: 30,
tipoProgramacion: TipoProgramacionAlarma.diaria,
diasSemana: [],
),
);
await estado.finalizarEjecucion('fin2');
expect(estado.error, isNotNull);
});
test('forzarDetencion reintenta el stop nativo y limpia el error si tiene '
'exito (SS-3b)', () async {
final android = FakePuertoAlarmasAndroid()..fallaDetener = true;
final estado = EstadoAlarmas(
servicio: ServicioAlarmas(reloj: () => DateTime(2026, 5, 25, 7, 31)),
android: android,
iniciarAutomaticamente: false,
);
addTearDown(estado.dispose);
addTearDown(android.dispose);
await estado.guardarAlarma(
const AlarmaMusical(
id: 'force1',
nombre: 'Forzada',
hora: 7,
minuto: 30,
tipoProgramacion: TipoProgramacionAlarma.diaria,
diasSemana: [],
),
);
await estado.finalizarEjecucion('force1');
expect(estado.error, isNotNull);
android.fallaDetener = false;
await estado.forzarDetencion('force1');
expect(estado.error, isNull);
expect(android.detencionesActivas.length, 2);
});
test(
'forzarDetencion mantiene el error si el reintento tambien falla (SS-3b)',
() async {
final android = FakePuertoAlarmasAndroid()..fallaDetener = true;
final estado = EstadoAlarmas(
servicio: ServicioAlarmas(reloj: () => DateTime(2026, 5, 25, 7, 31)),
android: android,
iniciarAutomaticamente: false,
);
addTearDown(estado.dispose);
addTearDown(android.dispose);
await estado.guardarAlarma(
const AlarmaMusical(
id: 'force2',
nombre: 'Forzada',
hora: 7,
minuto: 30,
tipoProgramacion: TipoProgramacionAlarma.diaria,
diasSemana: [],
),
);
await estado.finalizarEjecucion('force2');
await estado.forzarDetencion('force2');
expect(estado.error, isNotNull);
},
);
test('evento nativo missed completa la ejecucion (Phase 6)', () async {
final android = FakePuertoAlarmasAndroid();
final estado = EstadoAlarmas(
servicio: ServicioAlarmas(reloj: () => DateTime(2026, 5, 25, 7, 31)),
android: android,
iniciarAutomaticamente: false,
);
addTearDown(estado.dispose);
addTearDown(android.dispose);
await estado.guardarAlarma(
AlarmaMusical(
id: 'miss1',
nombre: 'Perdida',
hora: 7,
minuto: 30,
tipoProgramacion: TipoProgramacionAlarma.diaria,
diasSemana: const [],
proximaEjecucion: DateTime(2026, 5, 25, 7, 30),
),
);
final notificado = Completer<void>();
estado.addListener(() {
if (!notificado.isCompleted) notificado.complete();
});
android.emitirEvento(
EventoAlarmaAndroid(
alarmaId: 'miss1',
titulo: 'Perdida',
accion: EventoAlarmaAndroid.accionMissed,
occurrenceAtMillis: DateTime(2026, 5, 25, 7, 30).millisecondsSinceEpoch,
),
);
await notificado.future;
expect(
estado.alarmas.single.proximaEjecucion,
DateTime(2026, 5, 26, 7, 30),
);
});
test(
'inicializar sincroniza ejecucion nativa y evita reprogramar al instante',
() async {
final android =
FakePuertoAlarmasAndroid()
..ejecucionesNativas.add(
EjecucionAlarmaNativa(
alarmaId: 'native1',
gestionadaEn: DateTime(2026, 5, 25, 7, 30),
),
);
final servicio = ServicioAlarmas(
reloj: () => DateTime(2026, 5, 25, 7, 30, 20),
);
await servicio.guardarAlarma(
AlarmaMusical(
id: 'native1',
nombre: 'Nativa',
hora: 7,
minuto: 30,
tipoProgramacion: TipoProgramacionAlarma.diaria,
diasSemana: const [],
proximaEjecucion: DateTime(2026, 5, 25, 7, 30),
),
);
final estado = EstadoAlarmas(
servicio: servicio,
android: android,
iniciarAutomaticamente: false,
);
addTearDown(estado.dispose);
addTearDown(android.dispose);
await estado.inicializar();
expect(
estado.alarmas.single.ultimaEjecucionGestionada,
DateTime(2026, 5, 25, 7, 30),
);
expect(
estado.alarmas.single.proximaEjecucion,
DateTime(2026, 5, 26, 7, 30),
);
expect(
android.programadas.last.proximaProgramable,
DateTime(2026, 5, 26, 7, 30),
);
},
);
test(
'guardarAlarma: cuando android.programar falla, marca la alarma con una '
'excepcion de fallo visible via ultimaExcepcionPara',
() async {
final android = FakePuertoAlarmasAndroid()..fallaProgramar = true;
final estado = EstadoAlarmas(
servicio: ServicioAlarmas(reloj: () => DateTime(2026, 5, 25, 7)),
android: android,
iniciarAutomaticamente: false,
);
addTearDown(estado.dispose);
addTearDown(android.dispose);
await estado.guardarAlarma(
const AlarmaMusical(
id: 'fallo1',
nombre: 'Diaria',
hora: 7,
minuto: 30,
tipoProgramacion: TipoProgramacionAlarma.diaria,
diasSemana: [],
),
);
final excepcion = estado.ultimaExcepcionPara('fallo1');
expect(excepcion, isNotNull);
expect(excepcion!.tipo, ExcepcionAlarma.tipoFalloProgramacion);
expect(estado.error, isNotNull);
},
);
test(
'guardarAlarma: un reintento exitoso limpia la excepcion de fallo previa',
() async {
final android = FakePuertoAlarmasAndroid()..fallaProgramar = true;
final estado = EstadoAlarmas(
servicio: ServicioAlarmas(reloj: () => DateTime(2026, 5, 25, 7)),
android: android,
iniciarAutomaticamente: false,
);
addTearDown(estado.dispose);
addTearDown(android.dispose);
final alarma = const AlarmaMusical(
id: 'fallo2',
nombre: 'Diaria',
hora: 7,
minuto: 30,
tipoProgramacion: TipoProgramacionAlarma.diaria,
diasSemana: [],
);
await estado.guardarAlarma(alarma);
expect(estado.ultimaExcepcionPara('fallo2'), isNotNull);
android.fallaProgramar = false;
await estado.guardarAlarma(estado.alarmas.single);
expect(estado.ultimaExcepcionPara('fallo2'), isNull);
},
);
test(
'guardarAlarma en el camino feliz nunca registra una excepcion de fallo',
() async {
final android = FakePuertoAlarmasAndroid();
final estado = EstadoAlarmas(
servicio: ServicioAlarmas(reloj: () => DateTime(2026, 5, 25, 7)),
android: android,
iniciarAutomaticamente: false,
);
addTearDown(estado.dispose);
addTearDown(android.dispose);
await estado.guardarAlarma(
const AlarmaMusical(
id: 'ok1',
nombre: 'Diaria',
hora: 7,
minuto: 30,
tipoProgramacion: TipoProgramacionAlarma.diaria,
diasSemana: [],
),
);
expect(estado.ultimaExcepcionPara('ok1'), isNull);
expect(estado.error, isNull);
},
);
test(
'inicializar: un fallo de programacion en UNA alarma no aborta la '
'sincronizacion de las demas (S-sincronizarTodas continua tras error)',
() async {
final android = FakePuertoAlarmasAndroid()
..idsFallanProgramar.add('rota');
final servicio = ServicioAlarmas(
reloj: () => DateTime(2026, 5, 25, 6, 0),
);
await servicio.guardarAlarma(
AlarmaMusical(
id: 'rota',
nombre: 'Rota',
hora: 7,
minuto: 0,
tipoProgramacion: TipoProgramacionAlarma.diaria,
diasSemana: const [],
),
);
await servicio.guardarAlarma(
AlarmaMusical(
id: 'sana',
nombre: 'Sana',
hora: 8,
minuto: 0,
tipoProgramacion: TipoProgramacionAlarma.diaria,
diasSemana: const [],
),
);
final estado = EstadoAlarmas(
servicio: servicio,
android: android,
iniciarAutomaticamente: false,
);
addTearDown(estado.dispose);
addTearDown(android.dispose);
await estado.inicializar();
expect(
android.programadas.map((a) => a.id),
contains('sana'),
reason:
'la alarma sana debe seguir programandose aunque la rota falle',
);
expect(estado.ultimaExcepcionPara('rota'), isNotNull);
expect(estado.ultimaExcepcionPara('sana'), isNull);
},
);
group('EstadoAlarmas — consultas de vacaciones (ADR-6, WU9)', () {
test('rangoVacacionesActivo devuelve el rango cuyo intervalo incluye '
'"ahora" (dias restantes derivables de finDia), o null si ninguno '
'calza (fixed ahora)', () async {
final android = FakePuertoAlarmasAndroid();
final estado = EstadoAlarmas(
servicio: ServicioAlarmas(reloj: () => DateTime(2026, 7, 10)),
android: android,
iniciarAutomaticamente: false,
);
addTearDown(estado.dispose);
addTearDown(android.dispose);
await estado.guardarVacaciones([
RangoVacaciones(
id: 'v1',
nombre: 'Julio',
inicio: DateTime(2026, 7, 5),
fin: DateTime(2026, 7, 15),
),
]);
final activo = estado.rangoVacacionesActivo(ahora: DateTime(2026, 7, 10));
expect(activo?.id, 'v1');
expect(activo!.finDia.difference(DateTime(2026, 7, 10)).inDays, 5);
expect(estado.rangoVacacionesActivo(ahora: DateTime(2026, 8, 1)), isNull);
});
test('vacacionesProximas devuelve los rangos futuros ordenados por inicio '
'ascendente; vacacionesPasadas los finalizados por fin descendente; '
'ambas son disjuntas del rango activo', () async {
final android = FakePuertoAlarmasAndroid();
final estado = EstadoAlarmas(
servicio: ServicioAlarmas(reloj: () => DateTime(2026, 7, 10)),
android: android,
iniciarAutomaticamente: false,
);
addTearDown(estado.dispose);
addTearDown(android.dispose);
await estado.guardarVacaciones([
RangoVacaciones(
id: 'activo',
nombre: 'Activo',
inicio: DateTime(2026, 7, 5),
fin: DateTime(2026, 7, 15),
),
RangoVacaciones(
id: 'lejos',
nombre: 'Lejos',
inicio: DateTime(2026, 12, 1),
fin: DateTime(2026, 12, 10),
),
RangoVacaciones(
id: 'cerca',
nombre: 'Cerca',
inicio: DateTime(2026, 8, 1),
fin: DateTime(2026, 8, 5),
),
RangoVacaciones(
id: 'viejo',
nombre: 'Viejo',
inicio: DateTime(2026, 1, 1),
fin: DateTime(2026, 1, 10),
),
RangoVacaciones(
id: 'reciente',
nombre: 'Reciente',
inicio: DateTime(2026, 6, 1),
fin: DateTime(2026, 6, 5),
),
]);
final ahora = DateTime(2026, 7, 10);
final proximas = estado.vacacionesProximas(ahora: ahora);
expect(proximas.map((r) => r.id).toList(), ['cerca', 'lejos']);
final pasadas = estado.vacacionesPasadas(ahora: ahora);
expect(pasadas.map((r) => r.id).toList(), ['reciente', 'viejo']);
expect(proximas.any((r) => r.id == 'activo'), isFalse);
expect(pasadas.any((r) => r.id == 'activo'), isFalse);
});
test('impactoDeRango separa alarmas activas pausadas de las no afectadas '
'segun sonarEnVacaciones, igual que el predicado del planificador '
'(servicio_programacion_alarmas.dart)', () async {
final android = FakePuertoAlarmasAndroid();
final estado = EstadoAlarmas(
servicio: ServicioAlarmas(reloj: () => DateTime(2026, 7, 10)),
android: android,
iniciarAutomaticamente: false,
);
addTearDown(estado.dispose);
addTearDown(android.dispose);
await estado.guardarAlarma(
const AlarmaMusical(
id: 'p1',
nombre: 'Pausada 1',
hora: 7,
minuto: 30,
tipoProgramacion: TipoProgramacionAlarma.diaria,
diasSemana: [],
sonarEnVacaciones: false,
),
);
await estado.guardarAlarma(
const AlarmaMusical(
id: 'p2',
nombre: 'Pausada 2',
hora: 13,
minuto: 45,
tipoProgramacion: TipoProgramacionAlarma.diaria,
diasSemana: [],
sonarEnVacaciones: false,
),
);
await estado.guardarAlarma(
const AlarmaMusical(
id: 'n1',
nombre: 'Sigue',
hora: 8,
minuto: 15,
tipoProgramacion: TipoProgramacionAlarma.diaria,
diasSemana: [],
),
);
final rango = RangoVacaciones(
id: 'v1',
nombre: 'Julio',
inicio: DateTime(2026, 7, 5),
fin: DateTime(2026, 7, 15),
);
await estado.guardarVacaciones([rango]);
final impacto = estado.impactoDeRango(rango);
expect(impacto.pausadas.map((a) => a.id).toSet(), {'p1', 'p2'});
expect(impacto.noAfectadas.map((a) => a.id).toSet(), {'n1'});
});
test('impactoDeRango no cuenta alarmas inactivas ni como pausadas ni como '
'no afectadas', () async {
final android = FakePuertoAlarmasAndroid();
final estado = EstadoAlarmas(
servicio: ServicioAlarmas(reloj: () => DateTime(2026, 7, 10)),
android: android,
iniciarAutomaticamente: false,
);
addTearDown(estado.dispose);
addTearDown(android.dispose);
await estado.guardarAlarma(
const AlarmaMusical(
id: 'inactiva',
nombre: 'Apagada',
hora: 7,
minuto: 0,
tipoProgramacion: TipoProgramacionAlarma.diaria,
diasSemana: [],
sonarEnVacaciones: false,
activa: false,
),
);
final rango = RangoVacaciones(
id: 'v1',
nombre: 'Julio',
inicio: DateTime(2026, 7, 5),
fin: DateTime(2026, 7, 15),
);
await estado.guardarVacaciones([rango]);
final impacto = estado.impactoDeRango(rango);
expect(impacto.pausadas, isEmpty);
expect(impacto.noAfectadas, isEmpty);
});
test('ninguno de los 4 metodos de consulta reprograma Android (guardia de '
'pureza — son solo lectura sobre _alarmas/_vacaciones)', () async {
final android = FakePuertoAlarmasAndroid();
final estado = EstadoAlarmas(
servicio: ServicioAlarmas(reloj: () => DateTime(2026, 7, 10)),
android: android,
iniciarAutomaticamente: false,
);
addTearDown(estado.dispose);
addTearDown(android.dispose);
await estado.guardarAlarma(
const AlarmaMusical(
id: 'q1',
nombre: 'Consulta',
hora: 7,
minuto: 0,
tipoProgramacion: TipoProgramacionAlarma.diaria,
diasSemana: [],
),
);
final rango = RangoVacaciones(
id: 'v1',
nombre: 'Julio',
inicio: DateTime(2026, 7, 5),
fin: DateTime(2026, 7, 15),
);
await estado.guardarVacaciones([rango]);
final programadasAntes = android.programadas.length;
estado.rangoVacacionesActivo();
estado.vacacionesProximas();
estado.vacacionesPasadas();
estado.impactoDeRango(rango);
expect(android.programadas.length, programadasAntes);
});
});
}