feat: Implement startup retry mechanism for custom stations and equalizer persistence
- Added state management for startup retry and custom station handling in `EstadoRadio`. - Created tasks for implementing strict TDD with RED tests for HTTP failure retries and EQ persistence. - Developed verification report to ensure compliance with TDD practices. - Introduced fake services for testing, including `FakeServicioAudio`, `FakeServicioFavoritos`, and `FakeServicioRadio`. - Implemented widget tests for `PantallaInicio` and `PantallaFavoritos` to validate UI behavior with custom stations. - Enhanced `ServicioRadio` to support host rotation and retry logic for API calls. - Established a new configuration file to enforce project constraints and testing rules.
This commit is contained in:
184
test/estado/estado_radio_test.dart
Normal file
184
test/estado/estado_radio_test.dart
Normal file
@@ -0,0 +1,184 @@
|
||||
import 'dart:convert';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:pluriwave/estado/estado_radio.dart';
|
||||
import 'package:pluriwave/modelos/preset_ecualizador.dart';
|
||||
|
||||
import '../helpers/fakes.dart';
|
||||
|
||||
void main() {
|
||||
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.presetEcualizador, 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.ecualizadorDisponible, isFalse);
|
||||
expect(estado.presetEcualizador, principal);
|
||||
expect(estado.presetPrincipalEcualizador, principal);
|
||||
expect(
|
||||
estado.presetEcualizadorPorEmisora('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.guardarPresetEcualizadorPorEmisora(emisora.uuid, propio);
|
||||
|
||||
await estado.reproducir(emisora);
|
||||
expect(estado.presetEcualizador, propio);
|
||||
expect(audio.presetsAplicados.last, propio);
|
||||
|
||||
await estado.deshabilitarPresetEcualizadorPorEmisora(emisora.uuid);
|
||||
await estado.reproducir(emisora);
|
||||
expect(estado.presetEcualizador, 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.classical;
|
||||
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.tienePresetEcualizadorPorEmisora(emisora.uuid), isFalse);
|
||||
expect(estado.presetEcualizador, principal);
|
||||
expect(audio.presetsAplicados.last, principal);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
Future<File> _archivoCustomVacio() async => _crearArchivoCustom(const []);
|
||||
|
||||
Future<File> _crearArchivoCustom(List<dynamic> 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;
|
||||
}
|
||||
213
test/helpers/fakes.dart
Normal file
213
test/helpers/fakes.dart
Normal file
@@ -0,0 +1,213 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:pluriwave/modelos/emisora.dart';
|
||||
import 'package:pluriwave/modelos/preset_ecualizador.dart';
|
||||
import 'package:pluriwave/servicios/servicio_audio.dart';
|
||||
import 'package:pluriwave/servicios/servicio_ecualizador.dart';
|
||||
import 'package:pluriwave/servicios/servicio_favoritos.dart';
|
||||
import 'package:pluriwave/servicios/servicio_radio.dart';
|
||||
|
||||
class FakeServicioAudio extends ServicioAudio {
|
||||
FakeServicioAudio({this.ecualizadorActivo = true}) {
|
||||
_estadoController.add(EstadoReproduccion.detenido);
|
||||
}
|
||||
|
||||
final bool ecualizadorActivo;
|
||||
final _estadoController = StreamController<EstadoReproduccion>.broadcast();
|
||||
final List<PresetEcualizador> presetsAplicados = [];
|
||||
final List<Emisora> emisorasReproducidas = [];
|
||||
Emisora? _emisoraActual;
|
||||
|
||||
@override
|
||||
Emisora? get emisoraActual => _emisoraActual;
|
||||
|
||||
@override
|
||||
bool get ecualizadorDisponible => ecualizadorActivo;
|
||||
|
||||
@override
|
||||
Stream<EstadoReproduccion> get estadoStream => _estadoController.stream;
|
||||
|
||||
@override
|
||||
Future<void> reproducir(Emisora emisora) async {
|
||||
_emisoraActual = emisora;
|
||||
emisorasReproducidas.add(emisora);
|
||||
_estadoController.add(EstadoReproduccion.reproduciendo);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> detener() async {
|
||||
_emisoraActual = null;
|
||||
_estadoController.add(EstadoReproduccion.detenido);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> aplicarPreset(PresetEcualizador preset) async {
|
||||
presetsAplicados.add(preset);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> setBanda(int index, double db) async {}
|
||||
|
||||
@override
|
||||
Future<void> dispose() async {
|
||||
await _estadoController.close();
|
||||
}
|
||||
}
|
||||
|
||||
class FakeServicioFavoritos extends ServicioFavoritos {
|
||||
final Map<String, Emisora> _favoritos = {};
|
||||
int toggleCalls = 0;
|
||||
|
||||
@override
|
||||
Future<List<Emisora>> obtenerTodos() async => _favoritos.values.toList();
|
||||
|
||||
@override
|
||||
Future<void> agregar(Emisora emisora) async {
|
||||
_favoritos[emisora.uuid] = emisora;
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> eliminar(String uuid) async {
|
||||
_favoritos.remove(uuid);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<bool> esFavorito(String uuid) async => _favoritos.containsKey(uuid);
|
||||
|
||||
@override
|
||||
Future<bool> toggleFavorito(Emisora emisora) async {
|
||||
toggleCalls += 1;
|
||||
if (_favoritos.containsKey(emisora.uuid)) {
|
||||
_favoritos.remove(emisora.uuid);
|
||||
return false;
|
||||
}
|
||||
_favoritos[emisora.uuid] = emisora;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
class FakeServicioRadio extends ServicioRadio {
|
||||
FakeServicioRadio({
|
||||
List<Emisora>? populares,
|
||||
List<Emisora>? tendencias,
|
||||
List<Emisora>? busqueda,
|
||||
List<List<Emisora>>? popularesPorLlamada,
|
||||
List<List<Emisora>>? tendenciasPorLlamada,
|
||||
List<Object>? erroresPopularesPorLlamada,
|
||||
List<Object>? erroresTendenciasPorLlamada,
|
||||
}) : _populares = populares ?? [],
|
||||
_tendencias = tendencias ?? [],
|
||||
_busqueda = busqueda ?? [],
|
||||
_popularesPorLlamada = popularesPorLlamada ?? const [],
|
||||
_tendenciasPorLlamada = tendenciasPorLlamada ?? const [],
|
||||
_erroresPopularesPorLlamada = erroresPopularesPorLlamada ?? const [],
|
||||
_erroresTendenciasPorLlamada = erroresTendenciasPorLlamada ?? const [];
|
||||
|
||||
final List<Emisora> _populares;
|
||||
final List<Emisora> _tendencias;
|
||||
final List<Emisora> _busqueda;
|
||||
final List<List<Emisora>> _popularesPorLlamada;
|
||||
final List<List<Emisora>> _tendenciasPorLlamada;
|
||||
final List<Object> _erroresPopularesPorLlamada;
|
||||
final List<Object> _erroresTendenciasPorLlamada;
|
||||
|
||||
int obtenerPopularesCalls = 0;
|
||||
int obtenerTendenciasCalls = 0;
|
||||
int registrarClickCalls = 0;
|
||||
String? ultimoUuidClick;
|
||||
|
||||
Exception _normalizarError(Object error) =>
|
||||
error is Exception ? error : Exception(error.toString());
|
||||
|
||||
@override
|
||||
Future<List<Emisora>> obtenerPopulares({int limit = 30}) async {
|
||||
final llamada = obtenerPopularesCalls++;
|
||||
if (llamada < _erroresPopularesPorLlamada.length) {
|
||||
throw _normalizarError(_erroresPopularesPorLlamada[llamada]);
|
||||
}
|
||||
final data = llamada < _popularesPorLlamada.length
|
||||
? _popularesPorLlamada[llamada]
|
||||
: _populares;
|
||||
return data.take(limit).toList();
|
||||
}
|
||||
|
||||
@override
|
||||
Future<List<Emisora>> obtenerTendencias({int limit = 20}) async {
|
||||
final llamada = obtenerTendenciasCalls++;
|
||||
if (llamada < _erroresTendenciasPorLlamada.length) {
|
||||
throw _normalizarError(_erroresTendenciasPorLlamada[llamada]);
|
||||
}
|
||||
final data = llamada < _tendenciasPorLlamada.length
|
||||
? _tendenciasPorLlamada[llamada]
|
||||
: _tendencias;
|
||||
return data.take(limit).toList();
|
||||
}
|
||||
|
||||
@override
|
||||
Future<List<Emisora>> buscar({
|
||||
String? nombre,
|
||||
String? pais,
|
||||
String? idioma,
|
||||
String? tag,
|
||||
int limit = 30,
|
||||
}) async {
|
||||
return _busqueda.take(limit).toList();
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> registrarClick(String uuid) async {
|
||||
registrarClickCalls += 1;
|
||||
ultimoUuidClick = uuid;
|
||||
}
|
||||
}
|
||||
|
||||
class FakeServicioEcualizador extends ServicioEcualizador {
|
||||
FakeServicioEcualizador({
|
||||
PresetEcualizador? principal,
|
||||
Map<String, PresetEcualizador>? porEmisora,
|
||||
}) : _config = ConfiguracionEcualizador(
|
||||
principal: principal ?? PresetEcualizador.flat,
|
||||
porEmisora: porEmisora ?? {},
|
||||
);
|
||||
|
||||
ConfiguracionEcualizador _config;
|
||||
|
||||
@override
|
||||
Future<ConfiguracionEcualizador> cargar() async => _config;
|
||||
|
||||
@override
|
||||
Future<void> guardarPrincipal(PresetEcualizador preset) async {
|
||||
_config = ConfiguracionEcualizador(
|
||||
principal: preset,
|
||||
porEmisora: _config.porEmisora,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> guardarPorEmisora(String uuid, PresetEcualizador preset) async {
|
||||
final mapa = Map<String, PresetEcualizador>.from(_config.porEmisora);
|
||||
mapa[uuid] = preset;
|
||||
_config = ConfiguracionEcualizador(
|
||||
principal: _config.principal,
|
||||
porEmisora: mapa,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> eliminarPorEmisora(String uuid) async {
|
||||
final mapa = Map<String, PresetEcualizador>.from(_config.porEmisora);
|
||||
mapa.remove(uuid);
|
||||
_config = ConfiguracionEcualizador(
|
||||
principal: _config.principal,
|
||||
porEmisora: mapa,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Emisora emisoraDemo({
|
||||
required String uuid,
|
||||
required String nombre,
|
||||
String url = 'https://stream.demo/radio',
|
||||
}) {
|
||||
return Emisora(uuid: uuid, nombre: nombre, url: url);
|
||||
}
|
||||
173
test/pantallas/pantalla_inicio_test.dart
Normal file
173
test/pantallas/pantalla_inicio_test.dart
Normal file
@@ -0,0 +1,173 @@
|
||||
import 'dart:convert';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:pluriwave/estado/estado_radio.dart';
|
||||
import 'package:pluriwave/pantallas/pantalla_favoritos.dart';
|
||||
import 'package:pluriwave/pantallas/pantalla_inicio.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
import '../helpers/fakes.dart';
|
||||
|
||||
void main() {
|
||||
testWidgets(
|
||||
'PantallaInicio muestra custom, reproducir usa EstadoRadio y favorito usa flujo existente',
|
||||
(tester) async {
|
||||
final audio = FakeServicioAudio();
|
||||
final favoritos = FakeServicioFavoritos();
|
||||
final radio = FakeServicioRadio();
|
||||
final custom = emisoraDemo(uuid: 'custom-1', nombre: 'Custom Uno');
|
||||
final archivo = await _crearArchivoCustom([custom]);
|
||||
final estado = EstadoRadio(
|
||||
audio: audio,
|
||||
favoritos: favoritos,
|
||||
radio: radio,
|
||||
servicioEcualizador: FakeServicioEcualizador(),
|
||||
resolverArchivoCustom: () async => archivo,
|
||||
iniciarAutomaticamente: false,
|
||||
);
|
||||
await estado.inicializar();
|
||||
|
||||
await tester.pumpWidget(
|
||||
ChangeNotifierProvider<EstadoRadio>.value(
|
||||
value: estado,
|
||||
child: const MaterialApp(
|
||||
home: Scaffold(body: PantallaInicio()),
|
||||
),
|
||||
),
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(find.text('Custom Uno'), findsOneWidget);
|
||||
|
||||
await tester.tap(find.text('Custom Uno'));
|
||||
await tester.pumpAndSettle();
|
||||
expect(audio.emisorasReproducidas.map((e) => e.uuid), contains('custom-1'));
|
||||
expect(radio.ultimoUuidClick, 'custom-1');
|
||||
|
||||
final tarjetaCustom = find.ancestor(
|
||||
of: find.text('Custom Uno'),
|
||||
matching: find.byType(Card),
|
||||
);
|
||||
final botonFavorito = find.descendant(
|
||||
of: tarjetaCustom.first,
|
||||
matching: find.byIcon(Icons.favorite_outline_rounded),
|
||||
);
|
||||
expect(botonFavorito, findsOneWidget);
|
||||
|
||||
await tester.tap(botonFavorito);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(favoritos.toggleCalls, 1);
|
||||
expect(await favoritos.esFavorito(custom.uuid), isTrue);
|
||||
});
|
||||
|
||||
testWidgets(
|
||||
'PantallaInicio permite reintentar manualmente tras fallo inicial agotado',
|
||||
(tester) async {
|
||||
final radio = FakeServicioRadio(
|
||||
erroresPopularesPorLlamada: [Exception('sin red')],
|
||||
popularesPorLlamada: [
|
||||
const [],
|
||||
[emisoraDemo(uuid: 'api-1', nombre: 'API Uno')],
|
||||
),
|
||||
tendenciasPorLlamada: [
|
||||
const [],
|
||||
[emisoraDemo(uuid: 'trend-1', nombre: 'Trend Uno')],
|
||||
],
|
||||
);
|
||||
final estado = EstadoRadio(
|
||||
audio: FakeServicioAudio(),
|
||||
favoritos: FakeServicioFavoritos(),
|
||||
radio: radio,
|
||||
servicioEcualizador: FakeServicioEcualizador(),
|
||||
resolverArchivoCustom: _archivoCustomVacio,
|
||||
iniciarAutomaticamente: false,
|
||||
);
|
||||
await estado.inicializar();
|
||||
|
||||
await tester.pumpWidget(
|
||||
ChangeNotifierProvider<EstadoRadio>.value(
|
||||
value: estado,
|
||||
child: const MaterialApp(
|
||||
home: Scaffold(body: PantallaInicio()),
|
||||
),
|
||||
),
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(find.text('Sin conexión a la API de radio'), findsOneWidget);
|
||||
expect(find.text('Reintentar'), findsOneWidget);
|
||||
|
||||
await tester.tap(find.text('Reintentar'));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(radio.obtenerPopularesCalls, 2);
|
||||
expect(find.text('Sin conexión a la API de radio'), findsNothing);
|
||||
expect(find.text('API Uno'), findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets('PantallaFavoritos muestra custom favorito tras recarga',
|
||||
(tester) async {
|
||||
final favoritos = FakeServicioFavoritos();
|
||||
final custom = emisoraDemo(uuid: 'custom-1', nombre: 'Custom Uno');
|
||||
final archivo = await _crearArchivoCustom([custom]);
|
||||
final estado = EstadoRadio(
|
||||
audio: FakeServicioAudio(),
|
||||
favoritos: favoritos,
|
||||
radio: FakeServicioRadio(
|
||||
populares: [emisoraDemo(uuid: 'api-1', nombre: 'API Uno')],
|
||||
),
|
||||
servicioEcualizador: FakeServicioEcualizador(),
|
||||
resolverArchivoCustom: () async => archivo,
|
||||
iniciarAutomaticamente: false,
|
||||
);
|
||||
await estado.inicializar();
|
||||
|
||||
await tester.pumpWidget(
|
||||
ChangeNotifierProvider<EstadoRadio>.value(
|
||||
value: estado,
|
||||
child: const MaterialApp(
|
||||
home: Scaffold(body: PantallaInicio()),
|
||||
),
|
||||
),
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
final tarjetaCustom = find.ancestor(
|
||||
of: find.text('Custom Uno'),
|
||||
matching: find.byType(Card),
|
||||
);
|
||||
final botonFavorito = find.descendant(
|
||||
of: tarjetaCustom.first,
|
||||
matching: find.byIcon(Icons.favorite_outline_rounded),
|
||||
);
|
||||
expect(botonFavorito, findsOneWidget);
|
||||
|
||||
await tester.tap(botonFavorito);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
await tester.pumpWidget(
|
||||
ChangeNotifierProvider<EstadoRadio>.value(
|
||||
value: estado,
|
||||
child: const MaterialApp(
|
||||
home: Scaffold(body: PantallaFavoritos()),
|
||||
),
|
||||
),
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(await favoritos.esFavorito(custom.uuid), isTrue);
|
||||
expect(find.text('Custom Uno'), findsOneWidget);
|
||||
});
|
||||
}
|
||||
|
||||
Future<File> _crearArchivoCustom(List<dynamic> 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;
|
||||
}
|
||||
|
||||
Future<File> _archivoCustomVacio() async => _crearArchivoCustom(const []);
|
||||
72
test/servicios/servicio_radio_test.dart
Normal file
72
test/servicios/servicio_radio_test.dart
Normal file
@@ -0,0 +1,72 @@
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:http/http.dart' as http;
|
||||
import 'package:http/testing.dart';
|
||||
import 'package:pluriwave/servicios/servicio_radio.dart';
|
||||
|
||||
void main() {
|
||||
group('ServicioRadio retry + rotación', () {
|
||||
test(
|
||||
'reintenta con otro host cuando el primero falla y recupera en el segundo',
|
||||
() async {
|
||||
final hostsSolicitados = <String>[];
|
||||
final servicio = ServicioRadio(
|
||||
cliente: MockClient((request) async {
|
||||
hostsSolicitados.add(request.url.host);
|
||||
if (request.url.host == 'host-1.api.radio-browser.info') {
|
||||
return http.Response('fallo', 500);
|
||||
}
|
||||
return http.Response(
|
||||
jsonEncode([
|
||||
{
|
||||
'stationuuid': 'uuid-ok',
|
||||
'name': 'Radio Recuperada',
|
||||
'url_resolved': 'https://stream.recuperada/audio',
|
||||
},
|
||||
]),
|
||||
200,
|
||||
headers: {'content-type': 'application/json'},
|
||||
);
|
||||
}),
|
||||
servidores: const [
|
||||
'host-1.api.radio-browser.info',
|
||||
'host-2.api.radio-browser.info',
|
||||
],
|
||||
maxIntentos: 3,
|
||||
retryDelay: Duration.zero,
|
||||
);
|
||||
|
||||
final emisoras = await servicio.obtenerPopulares(limit: 1);
|
||||
|
||||
expect(emisoras, hasLength(1));
|
||||
expect(emisoras.first.uuid, 'uuid-ok');
|
||||
expect(
|
||||
hostsSolicitados,
|
||||
equals([
|
||||
'host-1.api.radio-browser.info',
|
||||
'host-2.api.radio-browser.info',
|
||||
]),
|
||||
);
|
||||
});
|
||||
|
||||
test('corta al llegar al tope de intentos y propaga error final', () async {
|
||||
var intentos = 0;
|
||||
final servicio = ServicioRadio(
|
||||
cliente: MockClient((request) async {
|
||||
intentos += 1;
|
||||
throw http.ClientException('sin red', request.url);
|
||||
}),
|
||||
servidores: const ['host-unico.api.radio-browser.info'],
|
||||
maxIntentos: 2,
|
||||
retryDelay: Duration.zero,
|
||||
);
|
||||
|
||||
expect(
|
||||
() => servicio.obtenerPopulares(limit: 1),
|
||||
throwsA(isA<Exception>()),
|
||||
);
|
||||
expect(intentos, 2);
|
||||
});
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user