Adds an Ecualizador folder listing the 6 fixed presets; selecting one applies and persists it through the existing headless-safe seam without touching playback or the now-playing media item.
893 lines
28 KiB
Dart
893 lines
28 KiB
Dart
import 'package:audio_service/audio_service.dart';
|
|
import 'package:flutter_test/flutter_test.dart';
|
|
import 'package:pluriwave/modelos/emisora.dart';
|
|
import 'package:pluriwave/modelos/grupo_favoritos.dart';
|
|
import 'package:pluriwave/modelos/preset_ecualizador.dart';
|
|
import 'package:pluriwave/servicios/navegacion_auto.dart';
|
|
|
|
void main() {
|
|
group('faviconUsable', () {
|
|
test('rechaza favicons malformados o no http(s), acepta http(s) válidos', () {
|
|
final casos = <String?, bool>{
|
|
null: false,
|
|
'': false,
|
|
' ': false,
|
|
'ftp://cdn.example.com/logo.png': false,
|
|
'cdn.example.com/logo.png': false,
|
|
'http://': false,
|
|
'/relative/path/logo.png': false,
|
|
'not a url at all \$\$\$ ///': false,
|
|
'http://cdn.example.com/logo.png': true,
|
|
'https://cdn.example.com/logo.png': true,
|
|
};
|
|
|
|
casos.forEach((entrada, esperado) {
|
|
expect(
|
|
faviconUsable(entrada),
|
|
esperado,
|
|
reason: 'faviconUsable(${entrada.toString()}) debería ser $esperado',
|
|
);
|
|
});
|
|
});
|
|
});
|
|
|
|
group('indiceArtePara', () {
|
|
test('reproduce la misma fórmula que _fallbackArtFor', () {
|
|
const seeds = ['', 'a', 'uuid-1234-abcd-real-looking'];
|
|
|
|
for (final seed in seeds) {
|
|
final esperado =
|
|
seed.codeUnits.fold<int>(0, (a, b) => a + b) % 4;
|
|
expect(indiceArtePara(seed), esperado);
|
|
}
|
|
});
|
|
|
|
test('el orden canónico de nombres es aurora, cosmic, pulse, nova '
|
|
'(verificado vía artUriPara con seeds que cubren los 4 índices)', () {
|
|
// Seeds de una sola letra cuyo codeUnit % 4 cubre los 4 índices, en
|
|
// el mismo orden que tarjeta_emisora.dart's _fallbackArtFor:
|
|
// 'd'(100)->índice 0 aurora, 'a'(97)->índice 1 cosmic,
|
|
// 'b'(98)->índice 2 pulse, 'c'(99)->índice 3 nova.
|
|
const indicesEsperados = {'d': 0, 'a': 1, 'b': 2, 'c': 3};
|
|
const nombresPorIndice = ['aurora', 'cosmic', 'pulse', 'nova'];
|
|
|
|
indicesEsperados.forEach((seed, indiceEsperado) {
|
|
expect(indiceArtePara(seed), indiceEsperado);
|
|
final emisora = _emisora(uuid: seed, nombre: 'Radio $seed');
|
|
expect(
|
|
artUriPara(emisora),
|
|
'android.resource://es.freetimelab.pluriwave/drawable/station_art_'
|
|
'${nombresPorIndice[indiceEsperado]}',
|
|
);
|
|
});
|
|
});
|
|
});
|
|
|
|
group('artUriPara', () {
|
|
String nombreEsperadoPara(String uuid) {
|
|
final indice = indiceArtePara(uuid);
|
|
const nombres = ['aurora', 'cosmic', 'pulse', 'nova'];
|
|
return nombres[indice];
|
|
}
|
|
|
|
test('favicon https válido se usa como artUri', () {
|
|
final emisora = _emisora(
|
|
uuid: 'uuid-logo',
|
|
nombre: 'Radio Con Logo',
|
|
favicon: 'https://cdn.example.com/logo.png',
|
|
);
|
|
|
|
expect(artUriPara(emisora), emisora.favicon);
|
|
});
|
|
|
|
test('favicon null cae al drawable rotativo correspondiente', () {
|
|
final emisora = _emisora(uuid: 'uuid-null', nombre: 'Radio Sin Logo');
|
|
final nombre = nombreEsperadoPara(emisora.uuid);
|
|
|
|
expect(
|
|
artUriPara(emisora),
|
|
'android.resource://es.freetimelab.pluriwave/drawable/station_art_$nombre',
|
|
);
|
|
});
|
|
|
|
test('favicon vacío cae al mismo comportamiento rotativo que null', () {
|
|
final emisora = _emisora(
|
|
uuid: 'uuid-vacio',
|
|
nombre: 'Radio Logo Vacio',
|
|
favicon: '',
|
|
);
|
|
final nombre = nombreEsperadoPara(emisora.uuid);
|
|
|
|
expect(
|
|
artUriPara(emisora),
|
|
'android.resource://es.freetimelab.pluriwave/drawable/station_art_$nombre',
|
|
);
|
|
});
|
|
|
|
test('favicon malformado cae al mismo comportamiento rotativo', () {
|
|
final malformados = [
|
|
_emisora(uuid: 'uuid-m1', nombre: 'M1', favicon: 'ftp://x/y.png'),
|
|
_emisora(uuid: 'uuid-m2', nombre: 'M2', favicon: 'not a url'),
|
|
];
|
|
|
|
for (final emisora in malformados) {
|
|
final nombre = nombreEsperadoPara(emisora.uuid);
|
|
expect(
|
|
artUriPara(emisora),
|
|
'android.resource://es.freetimelab.pluriwave/drawable/station_art_$nombre',
|
|
);
|
|
}
|
|
});
|
|
|
|
test('uuids distintos que cayeron en índices distintos resuelven a '
|
|
'artUriPara distintos', () {
|
|
final a = _emisora(uuid: 'uuid-a', nombre: 'A');
|
|
final b = _emisora(uuid: 'uuid-bbbb', nombre: 'B');
|
|
|
|
// Solo tiene sentido si de hecho hashean a índices distintos.
|
|
if (indiceArtePara(a.uuid) != indiceArtePara(b.uuid)) {
|
|
expect(artUriPara(a), isNot(equals(artUriPara(b))));
|
|
}
|
|
});
|
|
|
|
test('el mismo uuid llamado dos veces da resultado idéntico', () {
|
|
final emisora = _emisora(uuid: 'uuid-determinista', nombre: 'Det');
|
|
|
|
expect(artUriPara(emisora), artUriPara(emisora));
|
|
});
|
|
});
|
|
|
|
group('subtituloCalidad', () {
|
|
test('matriz de codec/bitrate conocidos y desconocidos', () {
|
|
expect(
|
|
subtituloCalidad(_emisora(uuid: 'u1', nombre: 'N', codec: 'mp3', bitrate: 128)),
|
|
'128 kbps · MP3',
|
|
);
|
|
expect(
|
|
subtituloCalidad(_emisora(uuid: 'u2', nombre: 'N', codec: null, bitrate: 128)),
|
|
'128 kbps',
|
|
);
|
|
expect(
|
|
subtituloCalidad(_emisora(uuid: 'u3', nombre: 'N', codec: 'mp3', bitrate: null)),
|
|
'MP3',
|
|
);
|
|
expect(
|
|
subtituloCalidad(_emisora(uuid: 'u4', nombre: 'N', codec: 'mp3', bitrate: 0)),
|
|
'MP3',
|
|
);
|
|
expect(
|
|
subtituloCalidad(_emisora(uuid: 'u5', nombre: 'N', codec: null, bitrate: null)),
|
|
isNull,
|
|
);
|
|
expect(
|
|
subtituloCalidad(_emisora(uuid: 'u6', nombre: 'N', codec: null, bitrate: 0)),
|
|
isNull,
|
|
);
|
|
expect(
|
|
subtituloCalidad(
|
|
_emisora(uuid: 'u7', nombre: 'N', codec: ' ', bitrate: null),
|
|
),
|
|
isNull,
|
|
);
|
|
expect(
|
|
subtituloCalidad(
|
|
_emisora(uuid: 'u8', nombre: 'N', codec: ' mp3 ', bitrate: 128),
|
|
),
|
|
'128 kbps · MP3',
|
|
);
|
|
});
|
|
|
|
test('ningún resultado contiene la subcadena "null"', () {
|
|
final casos = [
|
|
_emisora(uuid: 'u1', nombre: 'N', codec: 'mp3', bitrate: 128),
|
|
_emisora(uuid: 'u2', nombre: 'N', codec: null, bitrate: 128),
|
|
_emisora(uuid: 'u3', nombre: 'N', codec: 'mp3', bitrate: null),
|
|
_emisora(uuid: 'u4', nombre: 'N', codec: null, bitrate: null),
|
|
];
|
|
|
|
for (final emisora in casos) {
|
|
final resultado = subtituloCalidad(emisora);
|
|
expect(resultado?.contains('null') ?? false, isFalse);
|
|
}
|
|
});
|
|
});
|
|
|
|
group('parity: phone/auto art order', () {
|
|
test('el orden de nombres de arte coincide con tarjeta_emisora.dart', () {
|
|
// Orden canónico hardcodeado una única vez aquí (mismo patrón que
|
|
// tarjeta_emisora.dart:363-367's _fallbackArtFor); debe mantenerse
|
|
// sincronizado manualmente si esa lista cambia. _nombresArte es
|
|
// privado en navegacion_auto.dart, así que este test verifica el
|
|
// orden indirectamente vía artUriPara con seeds que cubren los 4
|
|
// índices (mismo patrón que el segundo test de 'indiceArtePara').
|
|
const ordenCanonico = ['aurora', 'cosmic', 'pulse', 'nova'];
|
|
const seedsPorIndice = ['d', 'a', 'b', 'c'];
|
|
|
|
for (var indice = 0; indice < ordenCanonico.length; indice++) {
|
|
final emisora = _emisora(
|
|
uuid: seedsPorIndice[indice],
|
|
nombre: 'Radio ${seedsPorIndice[indice]}',
|
|
);
|
|
expect(indiceArtePara(emisora.uuid), indice);
|
|
expect(
|
|
artUriPara(emisora),
|
|
'android.resource://es.freetimelab.pluriwave/drawable/station_art_'
|
|
'${ordenCanonico[indice]}',
|
|
);
|
|
}
|
|
});
|
|
});
|
|
|
|
group('ConstructorArbolAuto.raiz', () {
|
|
test('devuelve exactamente 4 carpetas no reproducibles con los ids '
|
|
'esperados, terminando en Ecualizador', () {
|
|
final raiz = ConstructorArbolAuto().raiz();
|
|
|
|
expect(raiz, hasLength(4));
|
|
final ids = raiz.map((item) => item.id).toSet();
|
|
expect(
|
|
ids,
|
|
equals({
|
|
ConstructorArbolAuto.idFavoritos,
|
|
ConstructorArbolAuto.idTodas,
|
|
ConstructorArbolAuto.idMisEmisoras,
|
|
ConstructorArbolAuto.idEcualizador,
|
|
}),
|
|
);
|
|
for (final item in raiz) {
|
|
expect(item.playable, isFalse);
|
|
expect(item.title, isNotEmpty);
|
|
}
|
|
expect(raiz.last.id, ConstructorArbolAuto.idEcualizador);
|
|
expect(raiz.last.playable, isFalse);
|
|
});
|
|
});
|
|
|
|
group('esPresetMediaId', () {
|
|
test('reconoce ids con el prefijo eq_preset:, rechaza el resto', () {
|
|
final casos = <String, bool>{
|
|
'eq_preset:Rock': true,
|
|
'eq_preset:': true,
|
|
'emisora:x': false,
|
|
'grupo:g1': false,
|
|
'': false,
|
|
};
|
|
|
|
casos.forEach((id, esperado) {
|
|
expect(
|
|
esPresetMediaId(id),
|
|
esperado,
|
|
reason: 'esPresetMediaId($id) debería ser $esperado',
|
|
);
|
|
});
|
|
});
|
|
});
|
|
|
|
group('ConstructorArbolAuto.itemPresetEq', () {
|
|
test('mapea un PresetEcualizador a un item reproducible con id '
|
|
'eq_preset:<nombre>', () {
|
|
final item = ConstructorArbolAuto().itemPresetEq(
|
|
PresetEcualizador.rock,
|
|
);
|
|
|
|
expect(item.id, 'eq_preset:${PresetEcualizador.rock.nombre}');
|
|
expect(item.playable, isTrue);
|
|
expect(item.title, PresetEcualizador.rock.nombre);
|
|
});
|
|
});
|
|
|
|
group('ConstructorArbolAuto.presetsEq', () {
|
|
test('devuelve exactamente 6 items reproducibles, uno por preset', () {
|
|
final items = ConstructorArbolAuto().presetsEq(
|
|
PresetEcualizador.presets,
|
|
);
|
|
|
|
expect(items, hasLength(6));
|
|
for (var i = 0; i < items.length; i++) {
|
|
expect(items[i].id, 'eq_preset:${PresetEcualizador.presets[i].nombre}');
|
|
expect(items[i].title, PresetEcualizador.presets[i].nombre);
|
|
expect(items[i].playable, isTrue);
|
|
}
|
|
});
|
|
});
|
|
|
|
group('ConstructorArbolAuto.itemEmisora', () {
|
|
test('usa el favicon remoto como artUri cuando existe', () {
|
|
final emisora = _emisora(
|
|
uuid: 'uuid-logo',
|
|
nombre: 'Radio Con Logo',
|
|
favicon: 'https://cdn.example.com/logo.png',
|
|
);
|
|
|
|
final item = ConstructorArbolAuto().itemEmisora(emisora);
|
|
|
|
expect(item.id, 'emisora:${emisora.uuid}');
|
|
expect(item.title, emisora.nombre);
|
|
expect(item.artUri.toString(), emisora.favicon);
|
|
expect(item.playable, isTrue);
|
|
});
|
|
|
|
test('cae al drawable rotativo correspondiente cuando el favicon es '
|
|
'null o vacío', () {
|
|
final sinFavicon = _emisora(
|
|
uuid: 'uuid-null',
|
|
nombre: 'Radio Sin Logo',
|
|
favicon: null,
|
|
);
|
|
final faviconVacio = _emisora(
|
|
uuid: 'uuid-vacio',
|
|
nombre: 'Radio Logo Vacio',
|
|
favicon: '',
|
|
);
|
|
|
|
final builder = ConstructorArbolAuto();
|
|
|
|
expect(
|
|
builder.itemEmisora(sinFavicon).artUri.toString(),
|
|
artUriPara(sinFavicon),
|
|
);
|
|
expect(
|
|
builder.itemEmisora(faviconVacio).artUri.toString(),
|
|
artUriPara(faviconVacio),
|
|
);
|
|
});
|
|
|
|
test('setea displaySubtitle desde subtituloCalidad cuando el codec y '
|
|
'bitrate son conocidos', () {
|
|
final emisora = _emisora(
|
|
uuid: 'uuid-calidad',
|
|
nombre: 'Radio Calidad',
|
|
codec: 'mp3',
|
|
bitrate: 128,
|
|
);
|
|
|
|
final item = ConstructorArbolAuto().itemEmisora(emisora);
|
|
|
|
expect(item.displaySubtitle, '128 kbps · MP3');
|
|
});
|
|
|
|
test('omite displaySubtitle (queda null) cuando codec y bitrate son '
|
|
'ambos desconocidos', () {
|
|
final emisora = _emisora(
|
|
uuid: 'uuid-sin-calidad',
|
|
nombre: 'Radio Sin Calidad',
|
|
);
|
|
|
|
final item = ConstructorArbolAuto().itemEmisora(emisora);
|
|
|
|
expect(item.displaySubtitle, isNull);
|
|
});
|
|
});
|
|
|
|
group('artUriPara: favicon sin sanear desde Emisora.fromMap', () {
|
|
test('un favicon malformado proveniente de favoritos (SQLite) resuelve '
|
|
'al drawable rotativo, no al string malformado', () {
|
|
// Emisora.fromMap (favoritos) no sanea favicon (a diferencia de
|
|
// Emisora.fromApi), así que valores en blanco/malformados llegan sin
|
|
// filtrar. Este test fija que la validez-gate de artUriPara es el
|
|
// punto que efectivamente los atrapa.
|
|
final desdeMapa = Emisora.fromMap({
|
|
'uuid': 'uuid-favorito',
|
|
'nombre': 'Radio Favorita',
|
|
'url': 'https://stream.demo/radio',
|
|
'favicon': ' ',
|
|
});
|
|
|
|
final resultado = artUriPara(desdeMapa);
|
|
|
|
expect(resultado, isNot(equals(' ')));
|
|
expect(
|
|
resultado,
|
|
startsWith('android.resource://es.freetimelab.pluriwave/drawable/station_art_'),
|
|
);
|
|
});
|
|
});
|
|
|
|
group('ConstructorArbolAuto.hijos', () {
|
|
test('limita a 50 items y respeta el orden de ordenarEmisoras', () {
|
|
final emisoras = List.generate(
|
|
60,
|
|
(i) => _emisora(uuid: 'uuid-$i', nombre: 'Radio $i', bitrate: i),
|
|
);
|
|
|
|
final hijos = ConstructorArbolAuto().hijos(
|
|
ConstructorArbolAuto.idFavoritos,
|
|
emisoras: emisoras,
|
|
);
|
|
|
|
expect(hijos, hasLength(50));
|
|
// ordenarEmisoras por calidad ordena por bitrate descendente: el
|
|
// primer item debe ser el de mayor bitrate (59) y el ultimo (el #50)
|
|
// el de bitrate 10 (59..10 son 50 valores).
|
|
expect(hijos.first.id, 'emisora:uuid-59');
|
|
expect(hijos.last.id, 'emisora:uuid-10');
|
|
});
|
|
|
|
test('lista vacía cuando no hay emisoras, sin lanzar', () {
|
|
final hijos = ConstructorArbolAuto().hijos(
|
|
ConstructorArbolAuto.idFavoritos,
|
|
emisoras: const [],
|
|
);
|
|
|
|
expect(hijos, isEmpty);
|
|
});
|
|
|
|
test('parentId desconocido devuelve lista vacía', () {
|
|
final emisoras = [_emisora(uuid: 'uuid-1', nombre: 'Radio 1')];
|
|
|
|
final hijos = ConstructorArbolAuto().hijos(
|
|
'carpeta-inexistente',
|
|
emisoras: emisoras,
|
|
);
|
|
|
|
expect(hijos, isEmpty);
|
|
});
|
|
});
|
|
|
|
group('ConstructorArbolAuto.resolver', () {
|
|
test('resuelve un uuid conocido a su Emisora', () {
|
|
final emisora = _emisora(uuid: 'uuid-conocido', nombre: 'Conocida');
|
|
final universo = [emisora, _emisora(uuid: 'otra', nombre: 'Otra')];
|
|
|
|
final resultado = ConstructorArbolAuto().resolver(
|
|
'emisora:uuid-conocido',
|
|
universo,
|
|
);
|
|
|
|
expect(resultado, equals(emisora));
|
|
});
|
|
|
|
test(
|
|
'devuelve null para id sin prefijo, id malformado o uuid sin match, '
|
|
'sin lanzar',
|
|
() {
|
|
final universo = [_emisora(uuid: 'uuid-conocido', nombre: 'Conocida')];
|
|
final builder = ConstructorArbolAuto();
|
|
|
|
expect(builder.resolver('favoritos', universo), isNull);
|
|
expect(builder.resolver('emisora:', universo), isNull);
|
|
expect(
|
|
builder.resolver('emisora:uuid-desconocido', universo),
|
|
isNull,
|
|
);
|
|
},
|
|
);
|
|
});
|
|
|
|
group('ConstructorArbolAuto.esCarpetaGrupo', () {
|
|
test('reconoce ids con el prefijo grupo:, rechaza el resto', () {
|
|
final builder = ConstructorArbolAuto();
|
|
|
|
expect(builder.esCarpetaGrupo('grupo:g1'), isTrue);
|
|
expect(builder.esCarpetaGrupo('favoritos'), isFalse);
|
|
expect(builder.esCarpetaGrupo('emisora:x'), isFalse);
|
|
expect(builder.esCarpetaGrupo(''), isFalse);
|
|
});
|
|
});
|
|
|
|
group('ConstructorArbolAuto.itemGrupo', () {
|
|
test('mapea un GrupoFavoritos a una carpeta no reproducible con id '
|
|
'grupo:<id>', () {
|
|
final grupo = _grupo(id: 'g1', nombre: 'Rock');
|
|
|
|
final item = ConstructorArbolAuto().itemGrupo(grupo);
|
|
|
|
expect(item.id, 'grupo:g1');
|
|
expect(item.playable, isFalse);
|
|
expect(item.title, grupo.nombre);
|
|
});
|
|
});
|
|
|
|
group('ConstructorArbolAuto.carpetasFavoritos', () {
|
|
test('sin grupos personalizados, el resultado es idéntico a hijos '
|
|
'(idFavoritos, emisoras: favoritos) — regresión más importante de '
|
|
'este cambio', () {
|
|
final favoritos = [
|
|
_emisora(uuid: 'uuid-1', nombre: 'Radio 1', bitrate: 128),
|
|
_emisora(uuid: 'uuid-2', nombre: 'Radio 2', bitrate: 64),
|
|
];
|
|
final builder = ConstructorArbolAuto();
|
|
|
|
final resultado = builder.carpetasFavoritos(
|
|
grupos: const [
|
|
GrupoFavoritos(id: GrupoFavoritos.sinAsignarId, nombre: 'Sin asignar', orden: 0),
|
|
],
|
|
favoritos: favoritos,
|
|
);
|
|
final esperado = builder.hijos(
|
|
ConstructorArbolAuto.idFavoritos,
|
|
emisoras: favoritos,
|
|
);
|
|
|
|
expect(resultado.map((i) => i.id).toList(), esperado.map((i) => i.id).toList());
|
|
expect(resultado.length, esperado.length);
|
|
for (var i = 0; i < resultado.length; i++) {
|
|
expect(resultado[i].id, esperado[i].id);
|
|
expect(resultado[i].playable, esperado[i].playable);
|
|
expect(resultado[i].title, esperado[i].title);
|
|
}
|
|
});
|
|
|
|
test('con un grupo personalizado no vacío, devuelve la carpeta grupo:<id> '
|
|
'seguida de las emisoras sin asignar como hojas', () {
|
|
final rock = _grupo(id: 'g-rock', nombre: 'Rock', orden: 1);
|
|
final sinAsignar = const GrupoFavoritos(
|
|
id: GrupoFavoritos.sinAsignarId,
|
|
nombre: 'Sin asignar',
|
|
orden: 0,
|
|
);
|
|
final favoritos = [
|
|
_emisora(uuid: 'uuid-rock', nombre: 'Radio Rock', grupoId: 'g-rock'),
|
|
_emisora(uuid: 'uuid-suelta', nombre: 'Radio Suelta'),
|
|
];
|
|
|
|
final resultado = ConstructorArbolAuto().carpetasFavoritos(
|
|
grupos: [sinAsignar, rock],
|
|
favoritos: favoritos,
|
|
);
|
|
|
|
expect(resultado, hasLength(2));
|
|
expect(resultado[0].id, 'grupo:g-rock');
|
|
expect(resultado[0].playable, isFalse);
|
|
expect(resultado[1].id, 'emisora:uuid-suelta');
|
|
expect(resultado[1].playable, isTrue);
|
|
});
|
|
|
|
test('un grupo personalizado vacío se omite; sin_asignar nunca aparece '
|
|
'como carpeta propia aunque esté en la lista de grupos', () {
|
|
final vacio = _grupo(id: 'g-vacio', nombre: 'Vacío', orden: 1);
|
|
final sinAsignar = const GrupoFavoritos(
|
|
id: GrupoFavoritos.sinAsignarId,
|
|
nombre: 'Sin asignar',
|
|
orden: 0,
|
|
);
|
|
final favoritos = [_emisora(uuid: 'uuid-1', nombre: 'Radio 1')];
|
|
|
|
final resultado = ConstructorArbolAuto().carpetasFavoritos(
|
|
grupos: [sinAsignar, vacio],
|
|
favoritos: favoritos,
|
|
);
|
|
|
|
expect(resultado.map((i) => i.id), isNot(contains('grupo:g-vacio')));
|
|
expect(
|
|
resultado.map((i) => i.id),
|
|
isNot(contains('grupo:${GrupoFavoritos.sinAsignarId}')),
|
|
);
|
|
expect(resultado.map((i) => i.id), contains('emisora:uuid-1'));
|
|
});
|
|
|
|
test('más de 50 grupos elegibles no vacíos se truncan a 50', () {
|
|
final favoritos = List.generate(
|
|
60,
|
|
(i) => _emisora(uuid: 'uuid-$i', nombre: 'Radio $i', grupoId: 'g-$i'),
|
|
);
|
|
final grupos = List.generate(
|
|
60,
|
|
(i) => _grupo(id: 'g-$i', nombre: 'Grupo $i', orden: i),
|
|
);
|
|
|
|
final resultado = ConstructorArbolAuto().carpetasFavoritos(
|
|
grupos: grupos,
|
|
favoritos: favoritos,
|
|
);
|
|
|
|
final carpetas = resultado.where((i) => i.playable != true).toList();
|
|
expect(carpetas, hasLength(50));
|
|
});
|
|
});
|
|
|
|
group('ConstructorArbolAuto.hijosGrupo', () {
|
|
test('filtra por grupoFavoritosId, ordena y capea a 50; id desconocido '
|
|
'devuelve lista vacía', () {
|
|
final favoritos = List.generate(
|
|
60,
|
|
(i) => _emisora(
|
|
uuid: 'uuid-$i',
|
|
nombre: 'Radio $i',
|
|
bitrate: i,
|
|
grupoId: 'g-rock',
|
|
),
|
|
);
|
|
|
|
final hijos = ConstructorArbolAuto().hijosGrupo(
|
|
'grupo:g-rock',
|
|
favoritos: favoritos,
|
|
);
|
|
|
|
expect(hijos, hasLength(50));
|
|
expect(hijos.first.id, 'emisora:uuid-59');
|
|
expect(hijos.last.id, 'emisora:uuid-10');
|
|
|
|
final vacio = ConstructorArbolAuto().hijosGrupo(
|
|
'grupo:g-inexistente',
|
|
favoritos: favoritos,
|
|
);
|
|
expect(vacio, isEmpty);
|
|
});
|
|
});
|
|
|
|
group('ConstructorArbolAuto.resolver: colisión con esquema grupo:', () {
|
|
test('un id grupo:<id> nunca resuelve a una Emisora', () {
|
|
final universo = [_emisora(uuid: 'g1', nombre: 'No debería matchear')];
|
|
|
|
final resultado = ConstructorArbolAuto().resolver('grupo:g1', universo);
|
|
|
|
expect(resultado, isNull);
|
|
});
|
|
});
|
|
|
|
group('resolverPresetEq', () {
|
|
test('resuelve un nombre conocido al preset exacto', () {
|
|
final resultado = resolverPresetEq(
|
|
'eq_preset:${PresetEcualizador.rock.nombre}',
|
|
PresetEcualizador.presets,
|
|
);
|
|
|
|
expect(resultado, equals(PresetEcualizador.rock));
|
|
});
|
|
|
|
test(
|
|
'devuelve null para nombre desconocido, id vacío-tras-prefijo o id sin '
|
|
'prefijo eq_preset:, sin lanzar',
|
|
() {
|
|
expect(
|
|
resolverPresetEq('eq_preset:Inexistente', PresetEcualizador.presets),
|
|
isNull,
|
|
);
|
|
expect(
|
|
resolverPresetEq('eq_preset:', PresetEcualizador.presets),
|
|
isNull,
|
|
);
|
|
expect(
|
|
resolverPresetEq('emisora:x', PresetEcualizador.presets),
|
|
isNull,
|
|
);
|
|
},
|
|
);
|
|
});
|
|
|
|
group('debeAplicarPrincipalAhora', () {
|
|
test('true cuando no hay estación actual', () {
|
|
expect(
|
|
debeAplicarPrincipalAhora(
|
|
uuidActual: null,
|
|
clavesPorEmisora: const {'uuid-otra'},
|
|
),
|
|
isTrue,
|
|
);
|
|
});
|
|
|
|
test('true cuando la estación actual no tiene override por-emisora', () {
|
|
expect(
|
|
debeAplicarPrincipalAhora(
|
|
uuidActual: 'uuid-sin-override',
|
|
clavesPorEmisora: const {'uuid-otra'},
|
|
),
|
|
isTrue,
|
|
);
|
|
});
|
|
|
|
test('false cuando la estación actual tiene override por-emisora', () {
|
|
expect(
|
|
debeAplicarPrincipalAhora(
|
|
uuidActual: 'uuid-con-override',
|
|
clavesPorEmisora: const {'uuid-con-override'},
|
|
),
|
|
isFalse,
|
|
);
|
|
});
|
|
});
|
|
|
|
group('aplicarPresetPorMediaId', () {
|
|
test(
|
|
'preset conocido: persiste siempre y aplica cuando el gate es true',
|
|
() async {
|
|
PresetEcualizador? persistido;
|
|
PresetEcualizador? aplicado;
|
|
|
|
await aplicarPresetPorMediaId(
|
|
'eq_preset:${PresetEcualizador.rock.nombre}',
|
|
presets: PresetEcualizador.presets,
|
|
uuidActual: null,
|
|
clavesPorEmisora: () async => <String>{},
|
|
persistirPrincipal: (preset) async {
|
|
persistido = preset;
|
|
},
|
|
aplicar: (preset) async {
|
|
aplicado = preset;
|
|
},
|
|
);
|
|
|
|
expect(persistido, equals(PresetEcualizador.rock));
|
|
expect(aplicado, equals(PresetEcualizador.rock));
|
|
},
|
|
);
|
|
|
|
test(
|
|
'preset conocido con override por-emisora en la estación actual: '
|
|
'persiste pero NO aplica en vivo (gate false)',
|
|
() async {
|
|
var persistirLlamadas = 0;
|
|
var aplicarLlamadas = 0;
|
|
|
|
await aplicarPresetPorMediaId(
|
|
'eq_preset:${PresetEcualizador.jazz.nombre}',
|
|
presets: PresetEcualizador.presets,
|
|
uuidActual: 'uuid-con-override',
|
|
clavesPorEmisora: () async => {'uuid-con-override'},
|
|
persistirPrincipal: (preset) async {
|
|
persistirLlamadas++;
|
|
},
|
|
aplicar: (preset) async {
|
|
aplicarLlamadas++;
|
|
},
|
|
);
|
|
|
|
expect(persistirLlamadas, 1);
|
|
expect(aplicarLlamadas, 0);
|
|
},
|
|
);
|
|
|
|
test(
|
|
'id desconocido/obsoleto: no persiste ni aplica, no lanza excepción',
|
|
() async {
|
|
var persistirLlamadas = 0;
|
|
var aplicarLlamadas = 0;
|
|
|
|
await aplicarPresetPorMediaId(
|
|
'eq_preset:Inexistente',
|
|
presets: PresetEcualizador.presets,
|
|
uuidActual: null,
|
|
clavesPorEmisora: () async => <String>{},
|
|
persistirPrincipal: (preset) async {
|
|
persistirLlamadas++;
|
|
},
|
|
aplicar: (preset) async {
|
|
aplicarLlamadas++;
|
|
},
|
|
);
|
|
|
|
expect(persistirLlamadas, 0);
|
|
expect(aplicarLlamadas, 0);
|
|
},
|
|
);
|
|
|
|
test(
|
|
'invariante estructural: la firma solo expone seams de '
|
|
'persistencia/aplicación EQ, ningun seam de reproducción es '
|
|
'inyectable (ADR-3, no-playback-by-construction)',
|
|
() async {
|
|
// Este test documenta y prueba por construcción que
|
|
// aplicarPresetPorMediaId no puede tocar playback: los únicos
|
|
// parámetros de función inyectables en su firma son
|
|
// persistirPrincipal y aplicar (ambos EQ-only). No existe ningún
|
|
// parámetro playMediaItem/mediaItem/playbackState que un caller
|
|
// pueda pasar — de haberlo, este call site fallaría a compilar.
|
|
final llamadasAplicar = <PresetEcualizador>[];
|
|
final llamadasPersistir = <PresetEcualizador>[];
|
|
|
|
await aplicarPresetPorMediaId(
|
|
'eq_preset:${PresetEcualizador.pop.nombre}',
|
|
presets: PresetEcualizador.presets,
|
|
uuidActual: null,
|
|
clavesPorEmisora: () async => <String>{},
|
|
persistirPrincipal: (preset) async => llamadasPersistir.add(preset),
|
|
aplicar: (preset) async => llamadasAplicar.add(preset),
|
|
// NOTE: no `playMediaItem`/`mediaItem`/`playbackState` parameter
|
|
// exists on this function — there is nothing to pass here even
|
|
// if a caller wanted to. That absence IS the non-playback proof.
|
|
);
|
|
|
|
expect(llamadasPersistir, [PresetEcualizador.pop]);
|
|
expect(llamadasAplicar, [PresetEcualizador.pop]);
|
|
},
|
|
);
|
|
});
|
|
|
|
group('reproducirPorMediaId', () {
|
|
test(
|
|
'resuelve el id y delega a reproducir con un MediaItem con forma de '
|
|
'telefono (id=url, extras.uuid)',
|
|
() async {
|
|
final emisora = _emisora(
|
|
uuid: 'uuid-play',
|
|
nombre: 'Radio a reproducir',
|
|
url: 'https://stream.demo/play',
|
|
);
|
|
final fuente = _FakeFuenteEmisorasAuto(
|
|
porUuidResultado: {emisora.uuid: emisora},
|
|
);
|
|
MediaItem? recibido;
|
|
|
|
await reproducirPorMediaId(
|
|
'emisora:${emisora.uuid}',
|
|
fuente: fuente,
|
|
reproducir: (item) async {
|
|
recibido = item;
|
|
},
|
|
);
|
|
|
|
expect(recibido, isNotNull);
|
|
expect(recibido!.id, emisora.url);
|
|
expect(recibido!.extras?['uuid'], emisora.uuid);
|
|
},
|
|
);
|
|
|
|
test(
|
|
'id obsoleto/desconocido no llama a reproducir ni lanza excepción',
|
|
() async {
|
|
final fuente = _FakeFuenteEmisorasAuto(porUuidResultado: const {});
|
|
var llamadas = 0;
|
|
|
|
await reproducirPorMediaId(
|
|
'emisora:uuid-fantasma',
|
|
fuente: fuente,
|
|
reproducir: (item) async {
|
|
llamadas++;
|
|
},
|
|
);
|
|
|
|
expect(llamadas, 0);
|
|
},
|
|
);
|
|
});
|
|
}
|
|
|
|
Emisora _emisora({
|
|
required String uuid,
|
|
required String nombre,
|
|
String url = 'https://stream.demo/radio',
|
|
String? favicon,
|
|
String? codec,
|
|
int? bitrate,
|
|
String grupoId = GrupoFavoritos.sinAsignarId,
|
|
}) {
|
|
return Emisora(
|
|
uuid: uuid,
|
|
nombre: nombre,
|
|
url: url,
|
|
favicon: favicon,
|
|
codec: codec,
|
|
bitrate: bitrate,
|
|
grupoFavoritosId: grupoId,
|
|
);
|
|
}
|
|
|
|
GrupoFavoritos _grupo({
|
|
required String id,
|
|
required String nombre,
|
|
int orden = 0,
|
|
}) {
|
|
return GrupoFavoritos(id: id, nombre: nombre, orden: orden);
|
|
}
|
|
|
|
class _FakeFuenteEmisorasAuto implements FuenteEmisorasAuto {
|
|
_FakeFuenteEmisorasAuto({required Map<String, Emisora> porUuidResultado})
|
|
: _porUuidResultado = porUuidResultado;
|
|
|
|
final Map<String, Emisora> _porUuidResultado;
|
|
|
|
@override
|
|
Future<List<Emisora>> favoritos() async => const [];
|
|
|
|
@override
|
|
Future<List<Emisora>> misEmisoras() async => const [];
|
|
|
|
@override
|
|
Future<List<Emisora>> todas() async => const [];
|
|
|
|
@override
|
|
Future<Emisora?> porUuid(String uuid) async => _porUuidResultado[uuid];
|
|
|
|
@override
|
|
Future<List<GrupoFavoritos>> grupos() async => const [];
|
|
|
|
@override
|
|
void actualizarSnapshot({
|
|
List<Emisora>? favoritos,
|
|
List<Emisora>? misEmisoras,
|
|
List<Emisora>? todas,
|
|
List<GrupoFavoritos>? grupos,
|
|
}) {}
|
|
}
|