_cargarEmisorasCustom resolved the file path outside the IO guard, so a throw from the resolver escaped into _init()'s Future.wait and took the sibling loads (populares, favoritos, grupos) down with it — a gap the old catch-all used to cover. Path resolution now gets the same IO-fail treatment as an unreadable file: degraded flag, logged skip, siblings unaffected.
663 lines
23 KiB
Dart
663 lines
23 KiB
Dart
import 'dart:async';
|
|
import 'dart:convert';
|
|
import 'dart:io';
|
|
|
|
import 'package:flutter_test/flutter_test.dart';
|
|
import 'package:pluriwave/estado/estado_radio.dart';
|
|
import 'package:pluriwave/modelos/emisora.dart';
|
|
import 'package:pluriwave/modelos/preset_ecualizador.dart';
|
|
import 'package:pluriwave/servicios/servicio_audio.dart';
|
|
import 'package:shared_preferences/shared_preferences.dart';
|
|
|
|
import '../helpers/fakes.dart';
|
|
|
|
void main() {
|
|
TestWidgetsFlutterBinding.ensureInitialized();
|
|
|
|
setUp(() {
|
|
SharedPreferences.setMockInitialValues({});
|
|
});
|
|
|
|
group('EstadoRadio integración de custom + EQ persistente', () {
|
|
test('incluye emisoras custom en el listado principal de inicio', () async {
|
|
final archivo = await _crearArchivoCustom([
|
|
emisoraDemo(uuid: 'custom-1', nombre: 'Custom Uno'),
|
|
]);
|
|
final estado = EstadoRadio(
|
|
audio: FakeServicioAudio(),
|
|
favoritos: FakeServicioFavoritos(),
|
|
radio: FakeServicioRadio(
|
|
populares: [emisoraDemo(uuid: 'api-1', nombre: 'API Uno')],
|
|
),
|
|
servicioEcualizador: FakeServicioEcualizador(),
|
|
resolverArchivoCustom: () async => archivo,
|
|
iniciarAutomaticamente: false,
|
|
);
|
|
|
|
await estado.inicializar();
|
|
|
|
expect(
|
|
estado.emisorasInicio.map((e) => e.uuid).toList(),
|
|
equals(['custom-1', 'api-1']),
|
|
);
|
|
});
|
|
|
|
test(
|
|
'carga EQ principal persistido antes de decidir EQ de reproducción',
|
|
() async {
|
|
final audio = FakeServicioAudio();
|
|
final principal = PresetEcualizador.rock;
|
|
final emisora = emisoraDemo(uuid: 'api-1', nombre: 'API Uno');
|
|
final estado = EstadoRadio(
|
|
audio: audio,
|
|
favoritos: FakeServicioFavoritos(),
|
|
radio: FakeServicioRadio(populares: [emisora]),
|
|
servicioEcualizador: FakeServicioEcualizador(principal: principal),
|
|
resolverArchivoCustom: _archivoCustomVacio,
|
|
iniciarAutomaticamente: false,
|
|
);
|
|
|
|
await estado.inicializar();
|
|
await estado.reproducir(emisora);
|
|
|
|
expect(estado.ecualizador.presetActual, principal);
|
|
expect(audio.presetsAplicados.first, principal);
|
|
expect(audio.presetsAplicados.last, principal);
|
|
},
|
|
);
|
|
|
|
test(
|
|
'mantiene EQ persistido aunque el ecualizador nativo no esté disponible',
|
|
() async {
|
|
final principal = PresetEcualizador.jazz;
|
|
final porEmisora = {'fav-1': PresetEcualizador.rock};
|
|
final estado = EstadoRadio(
|
|
audio: FakeServicioAudio(ecualizadorActivo: false),
|
|
favoritos: FakeServicioFavoritos(),
|
|
radio: FakeServicioRadio(),
|
|
servicioEcualizador: FakeServicioEcualizador(
|
|
principal: principal,
|
|
porEmisora: porEmisora,
|
|
),
|
|
resolverArchivoCustom: _archivoCustomVacio,
|
|
iniciarAutomaticamente: false,
|
|
);
|
|
|
|
await estado.inicializar();
|
|
|
|
expect(estado.ecualizador.disponible, isFalse);
|
|
expect(estado.ecualizador.presetActual, principal);
|
|
expect(estado.ecualizador.presetPrincipal, principal);
|
|
expect(
|
|
estado.ecualizador.presetPorEmisora('fav-1'),
|
|
PresetEcualizador.rock,
|
|
);
|
|
},
|
|
);
|
|
|
|
test(
|
|
'inicializar deja error tras fallo y cargarPopulares manual recupera estaciones',
|
|
() async {
|
|
final radio = FakeServicioRadio(
|
|
erroresPopularesPorLlamada: [Exception('sin red')],
|
|
popularesPorLlamada: [
|
|
const [],
|
|
[emisoraDemo(uuid: 'api-ok', nombre: 'API Recuperada')],
|
|
],
|
|
tendenciasPorLlamada: [
|
|
const [],
|
|
[emisoraDemo(uuid: 'trend-ok', nombre: 'Trend Recuperada')],
|
|
],
|
|
);
|
|
final estado = EstadoRadio(
|
|
audio: FakeServicioAudio(),
|
|
favoritos: FakeServicioFavoritos(),
|
|
radio: radio,
|
|
servicioEcualizador: FakeServicioEcualizador(),
|
|
resolverArchivoCustom: _archivoCustomVacio,
|
|
iniciarAutomaticamente: false,
|
|
);
|
|
|
|
await estado.inicializar();
|
|
expect(estado.error, 'Sin conexión a la API de radio');
|
|
|
|
await estado.cargarPopulares();
|
|
|
|
expect(estado.error, isNull);
|
|
expect(estado.populares.map((e) => e.uuid), contains('api-ok'));
|
|
expect(estado.tendencias.map((e) => e.uuid), contains('trend-ok'));
|
|
expect(radio.obtenerPopularesCalls, 2);
|
|
},
|
|
);
|
|
|
|
test(
|
|
'EQ propio por emisora pisa al principal y puede volver a fallback',
|
|
() async {
|
|
final audio = FakeServicioAudio();
|
|
final favoritos = FakeServicioFavoritos();
|
|
final emisora = emisoraDemo(uuid: 'fav-1', nombre: 'Favorita');
|
|
final principal = PresetEcualizador.pop;
|
|
final propio = PresetEcualizador.jazz;
|
|
await favoritos.agregar(emisora);
|
|
|
|
final estado = EstadoRadio(
|
|
audio: audio,
|
|
favoritos: favoritos,
|
|
radio: FakeServicioRadio(populares: [emisora]),
|
|
servicioEcualizador: FakeServicioEcualizador(principal: principal),
|
|
resolverArchivoCustom: _archivoCustomVacio,
|
|
iniciarAutomaticamente: false,
|
|
);
|
|
|
|
await estado.inicializar();
|
|
await estado.cargarFavoritos();
|
|
await estado.ecualizador.guardarPresetPorEmisora(emisora.uuid, propio);
|
|
|
|
await estado.reproducir(emisora);
|
|
expect(estado.ecualizador.presetActual, propio);
|
|
expect(audio.presetsAplicados.last, propio);
|
|
|
|
await estado.ecualizador.deshabilitarPresetPorEmisora(emisora.uuid);
|
|
await estado.reproducir(emisora);
|
|
expect(estado.ecualizador.presetActual, principal);
|
|
expect(audio.presetsAplicados.last, principal);
|
|
},
|
|
);
|
|
|
|
test(
|
|
'favorita sin EQ propio usa EQ principal desde el primer play',
|
|
() async {
|
|
final audio = FakeServicioAudio();
|
|
final favoritos = FakeServicioFavoritos();
|
|
final emisora = emisoraDemo(uuid: 'fav-main', nombre: 'Favorita Main');
|
|
final principal = PresetEcualizador.voz;
|
|
await favoritos.agregar(emisora);
|
|
|
|
final estado = EstadoRadio(
|
|
audio: audio,
|
|
favoritos: favoritos,
|
|
radio: FakeServicioRadio(populares: [emisora]),
|
|
servicioEcualizador: FakeServicioEcualizador(principal: principal),
|
|
resolverArchivoCustom: _archivoCustomVacio,
|
|
iniciarAutomaticamente: false,
|
|
);
|
|
|
|
await estado.inicializar();
|
|
await estado.cargarFavoritos();
|
|
await estado.reproducir(emisora);
|
|
|
|
expect(estado.ecualizador.tienePresetPorEmisora(emisora.uuid), isFalse);
|
|
expect(estado.ecualizador.presetActual, principal);
|
|
expect(audio.presetsAplicados.last, principal);
|
|
},
|
|
);
|
|
|
|
test(
|
|
'permite activar y desactivar el ecualizador de forma persistente',
|
|
() async {
|
|
final audio = FakeServicioAudio();
|
|
final servicioEcualizador = FakeServicioEcualizador();
|
|
final estado = EstadoRadio(
|
|
audio: audio,
|
|
favoritos: FakeServicioFavoritos(),
|
|
radio: FakeServicioRadio(),
|
|
servicioEcualizador: servicioEcualizador,
|
|
resolverArchivoCustom: _archivoCustomVacio,
|
|
iniciarAutomaticamente: false,
|
|
);
|
|
|
|
await estado.inicializar();
|
|
expect(estado.ecualizador.activo, isTrue);
|
|
|
|
await estado.ecualizador.cambiarActivo(false);
|
|
expect(estado.ecualizador.activo, isFalse);
|
|
expect(servicioEcualizador.config.activo, isFalse);
|
|
expect(audio.cambiosEcualizadorActivo.last, isFalse);
|
|
|
|
await estado.ecualizador.cambiarActivo(true);
|
|
expect(estado.ecualizador.activo, isTrue);
|
|
expect(servicioEcualizador.config.activo, isTrue);
|
|
expect(audio.cambiosEcualizadorActivo.last, isTrue);
|
|
},
|
|
);
|
|
|
|
test(
|
|
'notifica cambios de estado de audio para mostrar reproductor al primer play',
|
|
() async {
|
|
final audio = FakeServicioAudio();
|
|
final emisora = emisoraDemo(uuid: 'play-1', nombre: 'Primera');
|
|
final estado = EstadoRadio(
|
|
audio: audio,
|
|
favoritos: FakeServicioFavoritos(),
|
|
radio: FakeServicioRadio(populares: [emisora]),
|
|
servicioEcualizador: FakeServicioEcualizador(),
|
|
resolverArchivoCustom: _archivoCustomVacio,
|
|
iniciarAutomaticamente: false,
|
|
);
|
|
var notificaciones = 0;
|
|
estado.addListener(() => notificaciones++);
|
|
|
|
await estado.inicializar();
|
|
final antes = notificaciones;
|
|
audio.emitirEstado(EstadoReproduccion.cargando);
|
|
await Future<void>.delayed(Duration.zero);
|
|
|
|
expect(notificaciones, greaterThan(antes));
|
|
},
|
|
);
|
|
|
|
test(
|
|
'reproducir la misma emisora mientras suena fuerza recarga del stream',
|
|
() async {
|
|
final audio = FakeServicioAudio();
|
|
final emisora = emisoraDemo(uuid: 'same-1', nombre: 'Misma');
|
|
final estado = EstadoRadio(
|
|
audio: audio,
|
|
favoritos: FakeServicioFavoritos(),
|
|
radio: FakeServicioRadio(populares: [emisora]),
|
|
servicioEcualizador: FakeServicioEcualizador(),
|
|
resolverArchivoCustom: _archivoCustomVacio,
|
|
iniciarAutomaticamente: false,
|
|
);
|
|
|
|
await estado.inicializar();
|
|
await estado.reproducir(emisora);
|
|
await estado.reproducir(emisora);
|
|
|
|
expect(audio.emisorasReproducidas, hasLength(2));
|
|
},
|
|
);
|
|
|
|
test(
|
|
'ignora finalizaciones stale cuando se cambia de emisora rapido',
|
|
() async {
|
|
final audio = _AudioControlado();
|
|
final radio = FakeServicioRadio();
|
|
final primera = emisoraDemo(uuid: 'slow-1', nombre: 'Lenta');
|
|
final segunda = emisoraDemo(uuid: 'fast-2', nombre: 'Rapida');
|
|
final estado = EstadoRadio(
|
|
audio: audio,
|
|
favoritos: FakeServicioFavoritos(),
|
|
radio: radio,
|
|
servicioEcualizador: FakeServicioEcualizador(),
|
|
resolverArchivoCustom: _archivoCustomVacio,
|
|
iniciarAutomaticamente: false,
|
|
);
|
|
|
|
await estado.inicializar();
|
|
await estado.ecualizador.guardarPresetPorEmisora(
|
|
primera.uuid,
|
|
PresetEcualizador.rock,
|
|
);
|
|
await estado.ecualizador.guardarPresetPorEmisora(
|
|
segunda.uuid,
|
|
PresetEcualizador.jazz,
|
|
);
|
|
|
|
final primeraFuture = estado.reproducir(primera);
|
|
final segundaFuture = estado.reproducir(segunda);
|
|
audio.completar(segunda.uuid);
|
|
await segundaFuture;
|
|
audio.completar(primera.uuid);
|
|
await primeraFuture;
|
|
|
|
expect(estado.ecualizador.presetActual, PresetEcualizador.jazz);
|
|
expect(radio.ultimoUuidClick, segunda.uuid);
|
|
},
|
|
);
|
|
|
|
test('reordenar favoritos reindexa de forma determinística', () async {
|
|
final favoritos = FakeServicioFavoritos();
|
|
await favoritos.agregar(emisoraDemo(uuid: 'a', nombre: 'A'));
|
|
await favoritos.agregar(emisoraDemo(uuid: 'b', nombre: 'B'));
|
|
await favoritos.agregar(emisoraDemo(uuid: 'c', nombre: 'C'));
|
|
|
|
await favoritos.reordenar('a', 2);
|
|
final lista = await favoritos.obtenerTodos();
|
|
|
|
expect(lista.map((e) => e.uuid).toList(), equals(['b', 'c', 'a']));
|
|
expect(lista.map((e) => e.orden).toList(), equals([0, 1, 2]));
|
|
});
|
|
|
|
// The search pagination test moved to test/estado/estado_busqueda_test.dart
|
|
// (S4-R3: search state extracted to EstadoBusqueda).
|
|
|
|
test('toggleFavorito refresca lista global y evita estado stale', () async {
|
|
final favoritos = FakeServicioFavoritos();
|
|
final emisora = emisoraDemo(uuid: 'fav-sync', nombre: 'Sync');
|
|
final estado = EstadoRadio(
|
|
audio: FakeServicioAudio(),
|
|
favoritos: favoritos,
|
|
radio: FakeServicioRadio(populares: [emisora]),
|
|
servicioEcualizador: FakeServicioEcualizador(),
|
|
resolverArchivoCustom: _archivoCustomVacio,
|
|
iniciarAutomaticamente: false,
|
|
);
|
|
|
|
await estado.inicializar();
|
|
expect(estado.listaFavoritos.any((e) => e.uuid == emisora.uuid), isFalse);
|
|
|
|
await estado.toggleFavorito(emisora);
|
|
expect(estado.listaFavoritos.any((e) => e.uuid == emisora.uuid), isTrue);
|
|
|
|
await estado.toggleFavorito(emisora);
|
|
expect(estado.listaFavoritos.any((e) => e.uuid == emisora.uuid), isFalse);
|
|
});
|
|
|
|
test('crea y asigna grupos de favoritos sin tocar Sin asignar', () async {
|
|
final favoritos = FakeServicioFavoritos();
|
|
final emisora = emisoraDemo(uuid: 'fav-group', nombre: 'Grupo');
|
|
await favoritos.agregar(emisora);
|
|
final estado = EstadoRadio(
|
|
audio: FakeServicioAudio(),
|
|
favoritos: favoritos,
|
|
radio: FakeServicioRadio(populares: [emisora]),
|
|
servicioEcualizador: FakeServicioEcualizador(),
|
|
resolverArchivoCustom: _archivoCustomVacio,
|
|
iniciarAutomaticamente: false,
|
|
);
|
|
|
|
await estado.inicializar();
|
|
expect(estado.gruposFavoritos.first.protegido, isTrue);
|
|
|
|
await estado.crearGrupoFavoritos('Noticias');
|
|
final grupo = estado.gruposFavoritos.last;
|
|
await estado.asignarGrupoFavorito(emisora.uuid, grupo.id);
|
|
|
|
expect(estado.listaFavoritos.first.grupoFavoritosId, grupo.id);
|
|
|
|
await estado.eliminarGrupoFavoritos(grupo.id);
|
|
expect(estado.listaFavoritos.first.grupoFavoritosId, 'sin_asignar');
|
|
});
|
|
});
|
|
|
|
group(
|
|
'EstadoRadio — emisoras custom: lectura tolerante y guardia de '
|
|
'degradacion (persistence-resilience)',
|
|
() {
|
|
test(
|
|
'entradas invalidas se omiten sin perder las validas ni fabricar '
|
|
'uuid (D5 parcial)',
|
|
() async {
|
|
final archivo = await _crearArchivoCustomRaw(
|
|
jsonEncode([
|
|
{'uuid': 'custom-1', 'nombre': 'Valida Uno', 'url': 'http://a'},
|
|
{'uuid': 'custom-2', 'nombre': 'Valida Dos', 'url': 'http://b'},
|
|
// falta 'url' (campo requerido) -> Emisora.fromMap lanza.
|
|
{'uuid': 'custom-3', 'nombre': 'Sin url'},
|
|
// falta 'uuid' -> Emisora.fromMap lanza.
|
|
{'nombre': 'Sin uuid', 'url': 'http://d'},
|
|
]),
|
|
);
|
|
final estado = EstadoRadio(
|
|
audio: FakeServicioAudio(),
|
|
favoritos: FakeServicioFavoritos(),
|
|
radio: FakeServicioRadio(),
|
|
servicioEcualizador: FakeServicioEcualizador(),
|
|
resolverArchivoCustom: () async => archivo,
|
|
iniciarAutomaticamente: false,
|
|
);
|
|
|
|
await estado.inicializar();
|
|
|
|
expect(estado.emisorasCustom, hasLength(2));
|
|
expect(estado.emisorasCustom.map((e) => e.uuid).toSet(), {
|
|
'custom-1',
|
|
'custom-2',
|
|
});
|
|
},
|
|
);
|
|
|
|
test(
|
|
'si resolver la ruta del archivo custom falla, la inicializacion '
|
|
'sobrevive y las cargas hermanas no se pierden (D5 IO-fail)',
|
|
() async {
|
|
final estado = EstadoRadio(
|
|
audio: FakeServicioAudio(),
|
|
favoritos: FakeServicioFavoritos(),
|
|
radio: FakeServicioRadio(),
|
|
servicioEcualizador: FakeServicioEcualizador(),
|
|
resolverArchivoCustom:
|
|
() async => throw const FileSystemException('sin storage'),
|
|
iniciarAutomaticamente: false,
|
|
);
|
|
|
|
// Path resolution failing must be treated as an IO-fail, not
|
|
// escape _cargarEmisorasCustom: it runs inside _init()'s
|
|
// Future.wait, so an uncaught throw would also reject the
|
|
// sibling loads (populares/favoritos/grupos).
|
|
await estado.inicializar();
|
|
|
|
expect(estado.emisorasCustom, isEmpty);
|
|
},
|
|
);
|
|
|
|
test(
|
|
'JSON invalido al nivel superior pone en cuarentena el archivo '
|
|
'original (D5 parse-fail)',
|
|
() async {
|
|
final archivo = await _crearArchivoCustomRaw('{bad');
|
|
final estado = EstadoRadio(
|
|
audio: FakeServicioAudio(),
|
|
favoritos: FakeServicioFavoritos(),
|
|
radio: FakeServicioRadio(),
|
|
servicioEcualizador: FakeServicioEcualizador(),
|
|
resolverArchivoCustom: () async => archivo,
|
|
iniciarAutomaticamente: false,
|
|
);
|
|
|
|
await estado.inicializar();
|
|
|
|
expect(estado.emisorasCustom, isEmpty);
|
|
final sidecar = File('${archivo.path}.corrupt');
|
|
expect(await sidecar.exists(), isTrue);
|
|
expect(await sidecar.readAsString(), '{bad');
|
|
expect(await archivo.exists(), isFalse);
|
|
},
|
|
);
|
|
|
|
test(
|
|
'agregar tras la cuarentena escribe solo la nueva emisora y no '
|
|
'toca el sidecar (D5, autoridad de escritura restaurada)',
|
|
() async {
|
|
final archivo = await _crearArchivoCustomRaw('{bad');
|
|
final estado = EstadoRadio(
|
|
audio: FakeServicioAudio(),
|
|
favoritos: FakeServicioFavoritos(),
|
|
radio: FakeServicioRadio(),
|
|
servicioEcualizador: FakeServicioEcualizador(),
|
|
resolverArchivoCustom: () async => archivo,
|
|
iniciarAutomaticamente: false,
|
|
);
|
|
await estado.inicializar();
|
|
final sidecar = File('${archivo.path}.corrupt');
|
|
final sidecarPrevio = await sidecar.readAsString();
|
|
|
|
final nueva = emisoraDemo(uuid: 'nueva-1', nombre: 'Nueva');
|
|
await estado.agregarEmisoraCustom(nueva);
|
|
|
|
expect(estado.emisorasCustom.map((e) => e.uuid), ['nueva-1']);
|
|
final contenidoVivo =
|
|
jsonDecode(await archivo.readAsString()) as List;
|
|
expect(contenidoVivo, hasLength(1));
|
|
expect((contenidoVivo.single as Map)['uuid'], 'nueva-1');
|
|
expect(await sidecar.readAsString(), sidecarPrevio);
|
|
},
|
|
);
|
|
|
|
test(
|
|
'fallo de IO al leer suprime la escritura y no se restaura con un '
|
|
'alta explicita (D5 IO-fail)',
|
|
() async {
|
|
final espia = _ArchivoEspia(
|
|
path: '/fake/emisoras_custom.json',
|
|
exists: () async => true,
|
|
readAsString:
|
|
() async => throw const FileSystemException(
|
|
'fallo simulado de lectura',
|
|
),
|
|
);
|
|
final estado = EstadoRadio(
|
|
audio: FakeServicioAudio(),
|
|
favoritos: FakeServicioFavoritos(),
|
|
radio: FakeServicioRadio(),
|
|
servicioEcualizador: FakeServicioEcualizador(),
|
|
resolverArchivoCustom: () async => espia,
|
|
iniciarAutomaticamente: false,
|
|
);
|
|
|
|
await estado.inicializar();
|
|
expect(estado.emisorasCustom, isEmpty);
|
|
|
|
await estado.agregarEmisoraCustom(
|
|
emisoraDemo(uuid: 'nueva-x', nombre: 'X'),
|
|
);
|
|
|
|
expect(
|
|
estado.emisorasCustom.map((e) => e.uuid),
|
|
contains('nueva-x'),
|
|
);
|
|
expect(espia.writeAsStringCalls, 0);
|
|
},
|
|
);
|
|
|
|
test(
|
|
'si ya existe un sidecar .corrupt no lo pisa, solo limpia el '
|
|
'archivo vivo (D5)',
|
|
() async {
|
|
final archivo = await _crearArchivoCustomRaw('{bad-nuevo');
|
|
final sidecar = File('${archivo.path}.corrupt');
|
|
await sidecar.writeAsString('contenido-previo-X');
|
|
|
|
final estado = EstadoRadio(
|
|
audio: FakeServicioAudio(),
|
|
favoritos: FakeServicioFavoritos(),
|
|
radio: FakeServicioRadio(),
|
|
servicioEcualizador: FakeServicioEcualizador(),
|
|
resolverArchivoCustom: () async => archivo,
|
|
iniciarAutomaticamente: false,
|
|
);
|
|
|
|
await estado.inicializar();
|
|
|
|
expect(await sidecar.readAsString(), 'contenido-previo-X');
|
|
expect(await archivo.exists(), isFalse);
|
|
expect(estado.emisorasCustom, isEmpty);
|
|
},
|
|
);
|
|
},
|
|
);
|
|
}
|
|
|
|
/// [File] spy: only the members `_cargarEmisorasCustom`/
|
|
/// `_guardarEmisorasCustom` touch are implemented (with injectable
|
|
/// overrides); everything else throws via noSuchMethod. Mirrors
|
|
/// servicio_alarmas_corrupcion_test.dart's `_PrefsEspia` pattern, adapted
|
|
/// to force an IO failure independent of the host OS (persistence-
|
|
/// resilience D5).
|
|
class _ArchivoEspia implements File {
|
|
_ArchivoEspia({
|
|
required String path,
|
|
Future<bool> Function()? exists,
|
|
Future<String> Function()? readAsString,
|
|
Future<File> Function(String contenido)? writeAsString,
|
|
Future<File> Function(String nuevoPath)? rename,
|
|
}) : _path = path,
|
|
_existsImpl = exists,
|
|
_readAsStringImpl = readAsString,
|
|
_writeAsStringImpl = writeAsString,
|
|
_renameImpl = rename;
|
|
|
|
final String _path;
|
|
final Future<bool> Function()? _existsImpl;
|
|
final Future<String> Function()? _readAsStringImpl;
|
|
final Future<File> Function(String contenido)? _writeAsStringImpl;
|
|
final Future<File> Function(String nuevoPath)? _renameImpl;
|
|
|
|
int writeAsStringCalls = 0;
|
|
|
|
@override
|
|
String get path => _path;
|
|
|
|
@override
|
|
Future<bool> exists() => (_existsImpl ?? () async => true)();
|
|
|
|
@override
|
|
Future<String> readAsString({Encoding encoding = utf8}) =>
|
|
(_readAsStringImpl ?? () async => '')();
|
|
|
|
@override
|
|
Future<File> writeAsString(
|
|
String contents, {
|
|
FileMode mode = FileMode.write,
|
|
Encoding encoding = utf8,
|
|
bool flush = false,
|
|
}) {
|
|
writeAsStringCalls++;
|
|
return (_writeAsStringImpl ?? (_) async => this)(contents);
|
|
}
|
|
|
|
@override
|
|
Future<File> rename(String newPath) =>
|
|
(_renameImpl ?? (_) async => this)(newPath);
|
|
|
|
@override
|
|
dynamic noSuchMethod(Invocation invocation) => super.noSuchMethod(invocation);
|
|
}
|
|
|
|
class _AudioControlado extends ServicioAudio {
|
|
final _estadoController = StreamController<EstadoReproduccion>.broadcast();
|
|
final _pendientes = <String, Completer<void>>{};
|
|
Emisora? _actual;
|
|
|
|
@override
|
|
Emisora? get emisoraActual => _actual;
|
|
|
|
@override
|
|
Stream<EstadoReproduccion> get estadoStream => _estadoController.stream;
|
|
|
|
@override
|
|
Future<void> reproducir(Emisora emisora) async {
|
|
final completer = Completer<void>();
|
|
_pendientes[emisora.uuid] = completer;
|
|
_actual = emisora;
|
|
_estadoController.add(EstadoReproduccion.cargando);
|
|
await completer.future;
|
|
_estadoController.add(EstadoReproduccion.reproduciendo);
|
|
}
|
|
|
|
void completar(String uuid) {
|
|
_pendientes.remove(uuid)?.complete();
|
|
}
|
|
|
|
@override
|
|
Future<void> aplicarPreset(PresetEcualizador preset) async {}
|
|
|
|
@override
|
|
Future<void> dispose() async {
|
|
await _estadoController.close();
|
|
}
|
|
}
|
|
|
|
Future<File> _archivoCustomVacio() async => _crearArchivoCustom(const []);
|
|
|
|
Future<File> _crearArchivoCustom(List<Emisora> emisoras) async {
|
|
final dir = await Directory.systemTemp.createTemp('pluriwave-test-');
|
|
final archivo = File('${dir.path}/emisoras_custom.json');
|
|
await archivo.writeAsString(
|
|
jsonEncode(emisoras.map((e) => e.toMap()).toList()),
|
|
);
|
|
return archivo;
|
|
}
|
|
|
|
/// Writes RAW (already-encoded) [contenido] to a fresh temp
|
|
/// `emisoras_custom.json` -- unlike [_crearArchivoCustom], this bypasses
|
|
/// `Emisora.toMap` so tests can seed malformed/partially-invalid JSON that
|
|
/// a real Emisora could never produce (persistence-resilience D5).
|
|
Future<File> _crearArchivoCustomRaw(String contenido) async {
|
|
final dir = await Directory.systemTemp.createTemp('pluriwave-test-');
|
|
final archivo = File('${dir.path}/emisoras_custom.json');
|
|
await archivo.writeAsString(contenido);
|
|
return archivo;
|
|
}
|