Files
pluriwave/test/estado/estado_alarmas_preaviso_ancla_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

165 lines
6.0 KiB
Dart

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:shared_preferences/shared_preferences.dart';
import '../helpers/fakes_alarmas.dart';
/// Reported twice on-device: "Posponer" on the pre-notice notification left
/// the alarm snoozed 1400+ minutes — a whole day — instead of the configured
/// few minutes.
///
/// The first fix (7054a4c) guarded the NATIVE anchor, and it was not enough,
/// because Dart runs AFTERWARDS on this path: the receiver's `postponeNext`
/// fires, then `startActivity`, then `app.dart` dispatches here, and this
/// method persists and reschedules. Whatever Dart computes is the value that
/// survives. It was the last snooze path with no occurrence guard at all.
///
/// It also cannot simply reuse `_ocurrenciaSonando`: the pre-notice's
/// occurrence legitimately has NOT arrived yet (the reminder is armed 30 min
/// ahead), so the ringing-screen guard would reject a perfectly good anchor.
void main() {
TestWidgetsFlutterBinding.ensureInitialized();
setUp(() {
SharedPreferences.setMockInitialValues({});
});
AlarmaMusical diaria(String id) => AlarmaMusical(
id: id,
nombre: 'Mañana',
hora: 16,
minuto: 20,
tipoProgramacion: TipoProgramacionAlarma.diaria,
diasSemana: const [],
snoozeMinutos: 5,
);
({EstadoAlarmas estado, FakePuertoAlarmasAndroid android}) montar(
DateTime Function() reloj,
) {
final android = FakePuertoAlarmasAndroid();
final estado = EstadoAlarmas(
esPremium: () => true,
servicio: ServicioAlarmas(reloj: reloj),
android: android,
iniciarAutomaticamente: false,
);
addTearDown(estado.dispose);
addTearDown(android.dispose);
return (estado: estado, android: android);
}
test('una ocurrencia del DÍA SIGUIENTE se rechaza: pospone minutos, '
'no 24 horas', () async {
// The exact reported shape. The pre-notice for today's 16:20 is on
// screen at 16:16; the anchor handed in points at TOMORROW (either the
// native spec was already advanced, or app.dart fell back to a
// proximaEjecucion that had moved on).
var ahora = DateTime(2026, 8, 3, 16, 0);
final m = montar(() => ahora);
await m.estado.guardarAlarma(diaria('p1'));
ahora = DateTime(2026, 8, 3, 16, 16);
await m.estado.posponerProximaDesdePreaviso(
m.estado.alarmas.single,
5,
DateTime(2026, 8, 4, 16, 20), // <- tomorrow
);
final snooze = m.estado.alarmas.single.snoozeHasta!;
final minutos = snooze.difference(ahora).inMinutes;
expect(
minutos,
lessThan(60),
reason:
'la alarma quedó a $minutos min ($snooze). El reporte fue "más de '
'1400 minutos"; cualquier cosa por encima de una hora es el mismo bug',
);
expect(
m.estado.alarmas.single.snoozeOrigen,
isNot(DateTime(2026, 8, 4, 16, 20)),
reason:
'el ancla sin validar también se guarda como snoozeOrigen y como '
'ultimaEjecucionGestionada — envenenaría el estado que a9da855 y '
'0430059 existen para mantener limpio',
);
});
test('la ocurrencia REAL del preaviso se respeta aunque esté en el futuro: '
'ancla en la ocurrencia + N, no en ahora + N', () async {
// The whole reason this path needs its own guard instead of reusing
// _ocurrenciaSonando: 30 minutes ahead is legitimate here.
var ahora = DateTime(2026, 8, 3, 16, 0);
final m = montar(() => ahora);
await m.estado.guardarAlarma(diaria('p2'));
// Pre-notice fires at 15:50; the user taps at 15:52, 28 min before.
ahora = DateTime(2026, 8, 3, 15, 52);
await m.estado.posponerProximaDesdePreaviso(
m.estado.alarmas.single,
5,
DateTime(2026, 8, 3, 16, 20),
);
expect(m.estado.alarmas.single.snoozeHasta, DateTime(2026, 8, 3, 16, 25));
expect(m.estado.alarmas.single.snoozeOrigen, DateTime(2026, 8, 3, 16, 20));
});
test('un snooze ya envenenado en disco se cura al recalcular', () async {
// Devices that ran the buggy build carry snoozeHasta = tomorrow in
// SharedPreferences. Without healing it, the alarm keeps reporting
// tomorrow on every tick and the user sees no change after updating.
final ahora = DateTime(2026, 8, 3, 16, 0);
final servicio = ServicioAlarmas(reloj: () => ahora);
await servicio.guardarAlarma(
diaria('p4').copyWith(
snoozeHasta: DateTime(2026, 8, 4, 16, 25),
snoozeOrigen: DateTime(2026, 8, 4, 16, 20),
),
);
final alarma = (await servicio.recalcularTodas()).alarmas.single;
expect(alarma.snoozeHasta, isNull);
expect(alarma.proximaProgramable, DateTime(2026, 8, 3, 16, 20));
});
test('un snooze legítimo de 2 horas NO se toca', () async {
// posponerEjecucion clamps to 120 minutes, so the ceiling has to sit
// above that or the heal would eat real snoozes.
final ahora = DateTime(2026, 8, 3, 16, 0);
final servicio = ServicioAlarmas(reloj: () => ahora);
final hasta = DateTime(2026, 8, 3, 18, 0);
await servicio.guardarAlarma(
diaria('p5').copyWith(snoozeHasta: hasta, snoozeOrigen: ahora),
);
expect(
(await servicio.recalcularTodas()).alarmas.single.snoozeHasta,
hasta,
);
});
test('un ancla absurdamente lejana cae a la ocurrencia propia de la alarma, '
'no a un valor inventado', () async {
var ahora = DateTime(2026, 8, 3, 16, 0);
final m = montar(() => ahora);
await m.estado.guardarAlarma(diaria('p3'));
ahora = DateTime(2026, 8, 3, 16, 10);
await m.estado.posponerProximaDesdePreaviso(
m.estado.alarmas.single,
5,
DateTime(2027, 1, 1, 16, 20), // absurd
);
// proximaEjecucion (today 16:20) is inside the pre-notice window, so it
// is the right fallback and the snooze lands on 16:25.
expect(m.estado.alarmas.single.snoozeHasta, DateTime(2026, 8, 3, 16, 25));
});
}