Files
pluriwave/test/estado/estado_alarmas_test.dart
FreeTLab d81fabbe27 refactor(iap): make esPremium a required constructor parameter
EstadoAlarmas, EstadoGrabacion and EstadoRadio defaulted `esPremium` to
`() => true`, so any construction site that forgot to wire entitlement
compiled fine and silently ran ungated — failing OPEN to premium and
disabling the paywall with no test able to catch it.

The parameter is now required with no default. Production wiring in
app.dart was already correct and is unchanged; the 184 pre-existing test
call sites now pass `() => true` explicitly, which is exactly the old
implicit default, so every assertion is untouched.

EstadoRadio has no gate of its own but constructs EstadoGrabacion, so it
inherits the same contract.

The one test that existed to pin the old default is renamed to describe
what it still covers (the premium path through iniciar() with no
duracion); its assertions are unchanged.
2026-08-10 22:06:36 +02:00

994 lines
29 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(
esPremium: () => true,
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(
esPremium: () => true,
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(
esPremium: () => true,
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(
esPremium: () => true,
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(
esPremium: () => true,
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(
esPremium: () => true,
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(
esPremium: () => true,
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(
esPremium: () => true,
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(
esPremium: () => true,
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(
esPremium: () => true,
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(
esPremium: () => true,
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(
esPremium: () => true,
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(
esPremium: () => true,
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(
esPremium: () => true,
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(
esPremium: () => true,
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(
esPremium: () => true,
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(
esPremium: () => true,
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(
esPremium: () => true,
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(
esPremium: () => true,
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(
esPremium: () => true,
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(
esPremium: () => true,
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(
esPremium: () => true,
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(
esPremium: () => true,
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(
esPremium: () => true,
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(
esPremium: () => true,
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(
esPremium: () => true,
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(
esPremium: () => true,
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);
});
});
}