feat(auto): expose EQ presets as a browsable Android Auto folder
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.
This commit is contained in:
@@ -2,6 +2,7 @@ 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() {
|
||||
@@ -218,11 +219,11 @@ void main() {
|
||||
});
|
||||
|
||||
group('ConstructorArbolAuto.raiz', () {
|
||||
test('devuelve exactamente 3 carpetas no reproducibles con los ids '
|
||||
'esperados', () {
|
||||
test('devuelve exactamente 4 carpetas no reproducibles con los ids '
|
||||
'esperados, terminando en Ecualizador', () {
|
||||
final raiz = ConstructorArbolAuto().raiz();
|
||||
|
||||
expect(raiz, hasLength(3));
|
||||
expect(raiz, hasLength(4));
|
||||
final ids = raiz.map((item) => item.id).toSet();
|
||||
expect(
|
||||
ids,
|
||||
@@ -230,12 +231,63 @@ void main() {
|
||||
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);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
@@ -564,6 +616,174 @@ void main() {
|
||||
});
|
||||
});
|
||||
|
||||
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 '
|
||||
|
||||
Reference in New Issue
Block a user