Files
pluriwave/test/helpers/fakes_alarmas.dart
T
FreeTLab f2528c930b fix(alarmas): decode native failures with the real channel key names
The first pass read 'alarmaId'/'tipo' from the channel payload while the
native side sends 'alarmId'/'type'/'atMillis' (AlarmScheduler.kt:1389).
Every entry would have been dropped silently in production.

The tests passed because the fake was seeded with the same guessed keys,
so they confirmed the mistake instead of catching it. Decoding now goes
through FalloProgramacionNativo.fromMap -- the single place native key
names appear -- and the fixtures build through that same constructor.
2026-07-31 23:27:45 +02:00

251 lines
8.7 KiB
Dart

import 'dart:async';
import 'package:pluriwave/l10n/gen/app_localizations.dart';
import 'package:pluriwave/modelos/alarma_musical.dart';
import 'package:pluriwave/servicios/servicio_alarmas_android.dart';
import 'package:pluriwave/servicios/servicio_grabacion_radio.dart';
/// Shared fake of the Android alarm bridge for alarm-related tests.
class FakePuertoAlarmasAndroid implements PuertoAlarmasAndroid {
final programadas = <AlarmaMusical>[];
final canceladas = <String>[];
final detenidas = <String>[];
final ocultadas = <String>[];
final soloOcultadas = <String>[];
final ejecucionesNativas = <EjecucionAlarmaNativa>[];
final snoozesNativos = <EstadoSnoozeNativo>[];
final fallosProgramacionNativos = <FalloProgramacionNativo>[];
final _eventos = StreamController<EventoAlarmaAndroid>.broadcast();
bool ignoraOptimizacionBateria = true;
int solicitudesExencionBateria = 0;
int aperturasConfiguracionNotificaciones = 0;
/// Extra diagnostico() fields (fix/alarmas-fiabilidad diagnostics screen).
/// Default values mirror the previous hardcoded literals in [diagnostico],
/// so every existing test that never sets these keeps seeing the exact
/// same snapshot as before.
bool puedeProgramarExactas = true;
bool notificacionesPermitidas = true;
bool puedeUsarPantallaCompleta = true;
String fabricante = 'test';
int versionSdk = 35;
/// Ids [programar] most recently scheduled as active-with-a-next-run (kept
/// in sync with [cancelar] too), mirroring the real native scheduler's own
/// pending-alarm registry (fix/alarmas-fallos-silenciosos, item 3: "verify
/// the alarm is actually registered"). Backs [alarmasNativasPendientes]'s
/// DEFAULT so a test that never touches that field gets a value that
/// tracks reality instead of a frozen `0` -- a test that explicitly
/// assigns the field (many `pantalla_diagnostico_alarmas_test.dart` cases
/// do, to model a stale/corrupt native count on purpose) keeps getting
/// EXACTLY that value regardless of what programar/cancelar do afterward.
final _idsRegistradosNativamente = <String>{};
int? _alarmasNativasPendientesFijado;
int get alarmasNativasPendientes =>
_alarmasNativasPendientesFijado ?? _idsRegistradosNativamente.length;
set alarmasNativasPendientes(int valor) =>
_alarmasNativasPendientesFijado = valor;
/// Test-only failure switch (diagnostics screen, "intent not resolving"
/// coverage): when true, every `abrir*`/`solicitar*` system-screen action
/// below reports failure (as a real device does when a ROM lacks that
/// settings screen), while still recording the attempt via its counter.
bool fallaAccionSistema = false;
/// Test-only failure switch (Design D7): when true, [programar] throws
/// instead of scheduling, enabling failure-path coverage that the fake
/// could not otherwise produce.
bool fallaProgramar = false;
/// Test-only PER-ALARM failure switch (fix/alarmas-fallos-silenciosos):
/// [programar] throws only for ids in this set, letting a test simulate
/// one alarm failing to schedule while its siblings succeed -- the global
/// [fallaProgramar] switch cannot express that (it fails everything).
final Set<String> idsFallanProgramar = {};
/// Test-only failure switch: when true, [detenerSonidoActivo] reports an
/// unconfirmed/failed stop instead of a confirmed one.
bool fallaDetener = false;
/// Simulates [PluriWaveAlarmService.activeRingingId]: the id the fake
/// reports as currently ringing, or null if nothing rings.
String? alarmaSonandoIdValor;
/// Test-only failure switch (Finding 2, fail-toward-silence): when true,
/// [alarmaSonandoId] throws instead of returning a value, exercising the
/// [detenerSonidoNativo] fallback in `EstadoAlarmas._detenerSiEstaSonando`.
bool fallaConsultaSonando = false;
/// Every alarm id [detenerSonidoActivo] was invoked for, in call order.
final detencionesActivas = <String>[];
/// Test-only gate (RES-2 guard test): when set, [detenerSonidoActivo]
/// awaits it before resolving, letting tests exercise an overlapping
/// in-flight call.
Completer<void>? detenerActivoGate;
/// Simulates a native -> Flutter `alarmFired` MethodChannel event.
void emitirEvento(EventoAlarmaAndroid evento) => _eventos.add(evento);
@override
Stream<EventoAlarmaAndroid> get eventosAlarma => _eventos.stream;
@override
void configurarLocalizaciones(AppLocalizations l10n) {}
@override
Future<void> programar(AlarmaMusical alarma) async {
if (fallaProgramar || idsFallanProgramar.contains(alarma.id)) {
throw StateError('fake programar failure');
}
programadas.add(alarma);
if (alarma.activa && alarma.proximaProgramable != null) {
_idsRegistradosNativamente.add(alarma.id);
} else {
_idsRegistradosNativamente.remove(alarma.id);
}
}
@override
Future<void> cancelar(String alarmaId) async {
canceladas.add(alarmaId);
_idsRegistradosNativamente.remove(alarmaId);
}
@override
Future<void> detenerSonidoNativo(String alarmaId) async {
detenidas.add(alarmaId);
}
@override
Future<String?> alarmaSonandoId() async {
if (fallaConsultaSonando) {
throw StateError('fake alarmaSonandoId failure');
}
return alarmaSonandoIdValor;
}
@override
Future<ResultadoDetencion> detenerSonidoActivo() async {
detencionesActivas.add(alarmaSonandoIdValor ?? '');
final gate = detenerActivoGate;
if (gate != null) await gate.future;
if (fallaDetener) {
return const ResultadoDetencion(detenido: false, estabaSonando: true);
}
return ResultadoDetencion(
detenido: true,
estabaSonando: alarmaSonandoIdValor != null,
alarmaId: alarmaSonandoIdValor,
);
}
@override
Future<void> ocultarNotificacionAlarma(String alarmaId) async {
ocultadas.add(alarmaId);
}
@override
Future<void> ocultarSoloNotificacion(String alarmaId) async {
soloOcultadas.add(alarmaId);
}
@override
Future<DiagnosticoAlarmasAndroid> diagnostico() async =>
DiagnosticoAlarmasAndroid(
puedeProgramarExactas: puedeProgramarExactas,
notificacionesPermitidas: notificacionesPermitidas,
puedeUsarPantallaCompleta: puedeUsarPantallaCompleta,
ignoraOptimizacionBateria: ignoraOptimizacionBateria,
alarmasNativasPendientes: alarmasNativasPendientes,
fabricante: fabricante,
versionSdk: versionSdk,
);
@override
Future<bool> solicitarExencionBateria() async {
solicitudesExencionBateria++;
return !fallaAccionSistema;
}
@override
Future<bool> abrirConfiguracionNotificaciones() async {
aperturasConfiguracionNotificaciones++;
return !fallaAccionSistema;
}
@override
Future<EventoAlarmaAndroid?> obtenerEventoInicial() async => null;
@override
Future<List<EjecucionAlarmaNativa>>
obtenerEjecucionesNativasGestionadas() async => ejecucionesNativas;
@override
Future<List<EstadoSnoozeNativo>> obtenerEstadoSnoozeNativo() async =>
List.of(snoozesNativos);
@override
Future<List<FalloProgramacionNativo>>
obtenerFallosProgramacionNativos() async =>
List.of(fallosProgramacionNativos);
int solicitudesPermisoAlarmasExactas = 0;
int solicitudesPermisoPantallaCompleta = 0;
/// Native-recorded failures the next read should return. Tests seed this
/// to simulate a pre-notice that never armed, a refused foreground-service
/// start, or a per-alarm reschedule that failed after a reboot.
List<FalloProgramacionNativo> fallosNativos = const [];
int lecturasFallosNativos = 0;
/// Simulates an older native build with no such channel method.
bool fallaLecturaFallosNativos = false;
@override
Future<List<FalloProgramacionNativo>> fallosNativosProgramacion() async {
lecturasFallosNativos++;
if (fallaLecturaFallosNativos) {
throw StateError('canal no disponible');
}
return fallosNativos;
}
@override
Future<bool> solicitarPermisoAlarmasExactas() async {
solicitudesPermisoAlarmasExactas++;
return !fallaAccionSistema;
}
@override
Future<bool> solicitarPermisoNotificaciones() async => true;
@override
Future<bool> solicitarPermisoPantallaCompleta() async {
solicitudesPermisoPantallaCompleta++;
return !fallaAccionSistema;
}
Future<void> dispose() => _eventos.close();
}
/// Inactive recording service fake, safe for widget tests.
class FakeServicioGrabacionRadioInactiva extends ServicioGrabacionRadio {
final _controller = StreamController<EstadoGrabacionRadio>.broadcast();
@override
EstadoGrabacionRadio get estado => const EstadoGrabacionRadio.inactiva();
@override
Stream<EstadoGrabacionRadio> get estadoStream => _controller.stream;
@override
Future<void> inicializar() async {}
@override
Future<void> dispose() => _controller.close();
}