feat(eq): add per-device equalizer with 4-level preset resolution
Introduce multi-device EQ support allowing each audio output device (built-in speaker, wired, USB, individual Bluetooth by MAC) to have its own equalizer preset, combined with existing per-station presets for a full station×device matrix. - Add DispositivoAudio model and ServicioDispositivoAudio interface - Add Android platform channel (AudioDeviceCallback) for device detection - Add iOS AudioDevicesPlugin (AVAudioSession route tracking) - Extend ServicioEcualizador with device and matrix persistence keys - Implement 4-level resolution: matrix > station > device > global - Add advanced EQ settings section with feature toggle (off by default) - Extend export/import to v3 with backward compatibility - 184 tests passing, zero analyzer issues
This commit is contained in:
@@ -0,0 +1,109 @@
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:pluriwave/modelos/dispositivo_audio.dart';
|
||||
import 'package:pluriwave/servicios/servicio_dispositivo_audio.dart';
|
||||
|
||||
// Type int constants from the platform channel protocol (design doc):
|
||||
// 2 = builtin_speaker, 3 = wired_headset, 8 = bt_a2dp, 14 = usb_headset
|
||||
|
||||
void main() {
|
||||
TestWidgetsFlutterBinding.ensureInitialized();
|
||||
|
||||
group('ServicioDispositivoAudioReal', () {
|
||||
const methodChannelName = 'pluriwave/audio_devices';
|
||||
|
||||
late ServicioDispositivoAudioReal servicio;
|
||||
|
||||
setUp(() {
|
||||
servicio = ServicioDispositivoAudioReal();
|
||||
});
|
||||
|
||||
tearDown(() async {
|
||||
await servicio.dispose();
|
||||
TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger
|
||||
.setMockMethodCallHandler(
|
||||
const MethodChannel(methodChannelName),
|
||||
null,
|
||||
);
|
||||
});
|
||||
|
||||
void stubGetActiveDevice(Map<String, dynamic> response) {
|
||||
TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger
|
||||
.setMockMethodCallHandler(const MethodChannel(methodChannelName), (
|
||||
call,
|
||||
) async {
|
||||
if (call.method == 'getActiveDevice') return response;
|
||||
return null;
|
||||
});
|
||||
}
|
||||
|
||||
test('obtenerDispositivoActual maps builtin_speaker (type=2)', () async {
|
||||
stubGetActiveDevice({
|
||||
'id': 'builtin_speaker',
|
||||
'type': 2,
|
||||
'name': 'Speaker',
|
||||
});
|
||||
final device = await servicio.obtenerDispositivoActual();
|
||||
expect(device.id, 'builtin_speaker');
|
||||
expect(device.tipo, TipoDispositivo.altavozInterno);
|
||||
expect(device.nombre, 'Speaker');
|
||||
});
|
||||
|
||||
test('obtenerDispositivoActual maps wired_headset (type=3)', () async {
|
||||
stubGetActiveDevice({
|
||||
'id': 'wired_headset',
|
||||
'type': 3,
|
||||
'name': 'Wired Headset',
|
||||
});
|
||||
final device = await servicio.obtenerDispositivoActual();
|
||||
expect(device.id, 'wired_headset');
|
||||
expect(device.tipo, TipoDispositivo.auricularesCable);
|
||||
});
|
||||
|
||||
test('obtenerDispositivoActual maps bt_a2dp (type=8)', () async {
|
||||
stubGetActiveDevice({
|
||||
'id': 'bt_a2dp:AA:BB:CC:DD:EE:FF',
|
||||
'type': 8,
|
||||
'name': 'My BT Device',
|
||||
});
|
||||
final device = await servicio.obtenerDispositivoActual();
|
||||
expect(device.id, 'bt_a2dp:AA:BB:CC:DD:EE:FF');
|
||||
expect(device.tipo, TipoDispositivo.bluetoothA2dp);
|
||||
});
|
||||
|
||||
test('obtenerDispositivoActual maps usb_headset (type=14)', () async {
|
||||
stubGetActiveDevice({
|
||||
'id': 'usb_headset:0001:0002',
|
||||
'type': 14,
|
||||
'name': 'USB Headset',
|
||||
});
|
||||
final device = await servicio.obtenerDispositivoActual();
|
||||
expect(device.id, 'usb_headset:0001:0002');
|
||||
expect(device.tipo, TipoDispositivo.usbAudio);
|
||||
});
|
||||
|
||||
test('obtenerDispositivoActual maps unknown type to desconocido', () async {
|
||||
stubGetActiveDevice({
|
||||
'id': 'unknown_device',
|
||||
'type': 99,
|
||||
'name': 'Unknown',
|
||||
});
|
||||
final device = await servicio.obtenerDispositivoActual();
|
||||
expect(device.tipo, TipoDispositivo.desconocido);
|
||||
});
|
||||
|
||||
test('obtenerDispositivoActual updates dispositivoActual cache', () async {
|
||||
stubGetActiveDevice({
|
||||
'id': 'builtin_speaker',
|
||||
'type': 2,
|
||||
'name': 'Speaker',
|
||||
});
|
||||
await servicio.obtenerDispositivoActual();
|
||||
expect(servicio.dispositivoActual?.id, 'builtin_speaker');
|
||||
});
|
||||
|
||||
test('onDispositivoCambiado is a broadcast stream', () {
|
||||
expect(servicio.onDispositivoCambiado.isBroadcast, isTrue);
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:pluriwave/modelos/dispositivo_audio.dart';
|
||||
|
||||
import '../helpers/fakes.dart';
|
||||
|
||||
void main() {
|
||||
group('ServicioDispositivoAudio (abstract contract)', () {
|
||||
late FakeServicioDispositivoAudio fake;
|
||||
|
||||
setUp(() {
|
||||
fake = FakeServicioDispositivoAudio();
|
||||
});
|
||||
|
||||
tearDown(() async {
|
||||
await fake.dispose();
|
||||
});
|
||||
|
||||
test('dispositivoActual is null initially', () {
|
||||
expect(fake.dispositivoActual, isNull);
|
||||
});
|
||||
|
||||
test(
|
||||
'obtenerDispositivoActual returns current device after emit',
|
||||
() async {
|
||||
const device = DispositivoAudio(
|
||||
id: 'builtin_speaker',
|
||||
tipo: TipoDispositivo.altavozInterno,
|
||||
nombre: 'Speaker',
|
||||
);
|
||||
fake.emitirDispositivo(device);
|
||||
|
||||
final result = await fake.obtenerDispositivoActual();
|
||||
expect(result, equals(device));
|
||||
},
|
||||
);
|
||||
|
||||
test('onDispositivoCambiado stream emits when device changes', () async {
|
||||
const deviceA = DispositivoAudio(
|
||||
id: 'builtin_speaker',
|
||||
tipo: TipoDispositivo.altavozInterno,
|
||||
nombre: 'Speaker',
|
||||
);
|
||||
const deviceB = DispositivoAudio(
|
||||
id: 'bt_a2dp:AA:BB:CC:DD:EE:FF',
|
||||
tipo: TipoDispositivo.bluetoothA2dp,
|
||||
nombre: 'BT Headphones',
|
||||
);
|
||||
|
||||
final received = <DispositivoAudio>[];
|
||||
final sub = fake.onDispositivoCambiado.listen(received.add);
|
||||
|
||||
fake.emitirDispositivo(deviceA);
|
||||
fake.emitirDispositivo(deviceB);
|
||||
|
||||
await Future<void>.delayed(Duration.zero);
|
||||
await sub.cancel();
|
||||
|
||||
expect(received, equals([deviceA, deviceB]));
|
||||
});
|
||||
|
||||
test('dispositivoActual tracks last emitted device', () {
|
||||
const device = DispositivoAudio(
|
||||
id: 'wired_headset',
|
||||
tipo: TipoDispositivo.auricularesCable,
|
||||
nombre: 'Wired Headset',
|
||||
);
|
||||
fake.emitirDispositivo(device);
|
||||
expect(fake.dispositivoActual, equals(device));
|
||||
});
|
||||
|
||||
test(
|
||||
'onDispositivoCambiado is a broadcast stream (multiple listeners)',
|
||||
() {
|
||||
expect(fake.onDispositivoCambiado.isBroadcast, isTrue);
|
||||
},
|
||||
);
|
||||
|
||||
test('dispose closes the stream without error', () async {
|
||||
await expectLater(fake.dispose(), completes);
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:pluriwave/modelos/dispositivo_audio.dart';
|
||||
import 'package:pluriwave/servicios/servicio_dispositivo_audio.dart';
|
||||
|
||||
import '../helpers/fakes.dart';
|
||||
|
||||
/// Null (toggle-off) implementation that returns empty data without errors.
|
||||
/// Represents the behavior when `eqMultiDeviceEnabled` is false.
|
||||
class NullServicioDispositivoAudio extends ServicioDispositivoAudio {
|
||||
@override
|
||||
DispositivoAudio? get dispositivoActual => null;
|
||||
|
||||
@override
|
||||
Stream<DispositivoAudio> get onDispositivoCambiado => const Stream.empty();
|
||||
|
||||
@override
|
||||
Future<DispositivoAudio> obtenerDispositivoActual() async {
|
||||
return const DispositivoAudio(
|
||||
id: 'builtin_speaker',
|
||||
tipo: TipoDispositivo.altavozInterno,
|
||||
nombre: 'Speaker',
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> dispose() async {}
|
||||
}
|
||||
|
||||
void main() {
|
||||
group('Toggle-off path: NullServicioDispositivoAudio', () {
|
||||
late NullServicioDispositivoAudio servicio;
|
||||
|
||||
setUp(() {
|
||||
servicio = NullServicioDispositivoAudio();
|
||||
});
|
||||
|
||||
test('dispositivoActual is always null', () {
|
||||
expect(servicio.dispositivoActual, isNull);
|
||||
});
|
||||
|
||||
test('onDispositivoCambiado emits no events', () async {
|
||||
final received = <DispositivoAudio>[];
|
||||
final sub = servicio.onDispositivoCambiado.listen(received.add);
|
||||
await Future<void>.delayed(const Duration(milliseconds: 10));
|
||||
await sub.cancel();
|
||||
expect(received, isEmpty);
|
||||
});
|
||||
|
||||
test('obtenerDispositivoActual returns fallback builtin_speaker', () async {
|
||||
final device = await servicio.obtenerDispositivoActual();
|
||||
expect(device.id, 'builtin_speaker');
|
||||
expect(device.tipo, TipoDispositivo.altavozInterno);
|
||||
});
|
||||
|
||||
test('dispose completes without error', () async {
|
||||
await expectLater(servicio.dispose(), completes);
|
||||
});
|
||||
|
||||
test('no platform channel code is invoked (fake, not real)', () async {
|
||||
// Verify that FakeServicioDispositivoAudio also produces no events
|
||||
// when no emitirDispositivo is called — simulating toggle-off behavior.
|
||||
final fake = FakeServicioDispositivoAudio();
|
||||
final received = <DispositivoAudio>[];
|
||||
final sub = fake.onDispositivoCambiado.listen(received.add);
|
||||
await Future<void>.delayed(const Duration(milliseconds: 10));
|
||||
await sub.cancel();
|
||||
expect(received, isEmpty);
|
||||
await fake.dispose();
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,172 @@
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:pluriwave/modelos/preset_ecualizador.dart';
|
||||
import 'package:pluriwave/servicios/servicio_ecualizador.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
|
||||
void main() {
|
||||
setUp(() {
|
||||
SharedPreferences.setMockInitialValues({});
|
||||
});
|
||||
|
||||
group('ServicioEcualizador — multi-device persistence (Phase 4)', () {
|
||||
test('4.1a toggle defaults to false when no SP data', () async {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
final servicio = ServicioEcualizador(prefs: prefs);
|
||||
final config = await servicio.cargar();
|
||||
expect(config.eqMultiDeviceEnabled, isFalse);
|
||||
});
|
||||
|
||||
test(
|
||||
'4.1b guardarToggleMultiDispositivo persists and cargar restores it',
|
||||
() async {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
final servicio = ServicioEcualizador(prefs: prefs);
|
||||
|
||||
await servicio.guardarToggleMultiDispositivo(true);
|
||||
final config = await servicio.cargar();
|
||||
|
||||
expect(config.eqMultiDeviceEnabled, isTrue);
|
||||
},
|
||||
);
|
||||
|
||||
test('4.1c guardarPresetDispositivo stores a device preset', () async {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
final servicio = ServicioEcualizador(prefs: prefs);
|
||||
const deviceId = 'bt_a2dp:AA:BB:CC:DD:EE:FF';
|
||||
|
||||
await servicio.guardarPresetDispositivo(deviceId, PresetEcualizador.jazz);
|
||||
final config = await servicio.cargar();
|
||||
|
||||
expect(
|
||||
config.presetsDispositivo[deviceId],
|
||||
equals(PresetEcualizador.jazz),
|
||||
);
|
||||
});
|
||||
|
||||
test(
|
||||
'4.1d obtenerPresetDispositivo returns null for unknown device',
|
||||
() async {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
final servicio = ServicioEcualizador(prefs: prefs);
|
||||
final config = await servicio.cargar();
|
||||
|
||||
expect(config.presetsDispositivo['unknown_device'], isNull);
|
||||
},
|
||||
);
|
||||
|
||||
test('4.1e guardarPresetMatriz stores a matrix entry', () async {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
final servicio = ServicioEcualizador(prefs: prefs);
|
||||
const clave = 'station-uuid-123:bt_a2dp:AA:BB:CC:DD:EE:FF';
|
||||
|
||||
await servicio.guardarPresetMatriz(clave, PresetEcualizador.rock);
|
||||
final config = await servicio.cargar();
|
||||
|
||||
expect(config.presetsMatriz[clave], equals(PresetEcualizador.rock));
|
||||
});
|
||||
|
||||
test('4.1f multiple device presets persist independently', () async {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
final servicio = ServicioEcualizador(prefs: prefs);
|
||||
|
||||
await servicio.guardarPresetDispositivo(
|
||||
'bt_a2dp:11:22:33:44:55:66',
|
||||
PresetEcualizador.rock,
|
||||
);
|
||||
await servicio.guardarPresetDispositivo(
|
||||
'wired_headset',
|
||||
PresetEcualizador.jazz,
|
||||
);
|
||||
|
||||
final config = await servicio.cargar();
|
||||
expect(
|
||||
config.presetsDispositivo['bt_a2dp:11:22:33:44:55:66'],
|
||||
equals(PresetEcualizador.rock),
|
||||
);
|
||||
expect(
|
||||
config.presetsDispositivo['wired_headset'],
|
||||
equals(PresetEcualizador.jazz),
|
||||
);
|
||||
});
|
||||
|
||||
test('4.1g multiple matrix entries persist independently', () async {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
final servicio = ServicioEcualizador(prefs: prefs);
|
||||
|
||||
await servicio.guardarPresetMatriz(
|
||||
'station-A:bt_a2dp:AA:BB:CC:DD:EE:FF',
|
||||
PresetEcualizador.jazz,
|
||||
);
|
||||
await servicio.guardarPresetMatriz(
|
||||
'station-B:wired_headset',
|
||||
PresetEcualizador.bassBoost,
|
||||
);
|
||||
|
||||
final config = await servicio.cargar();
|
||||
expect(
|
||||
config.presetsMatriz['station-A:bt_a2dp:AA:BB:CC:DD:EE:FF'],
|
||||
equals(PresetEcualizador.jazz),
|
||||
);
|
||||
expect(
|
||||
config.presetsMatriz['station-B:wired_headset'],
|
||||
equals(PresetEcualizador.bassBoost),
|
||||
);
|
||||
});
|
||||
|
||||
test(
|
||||
'4.1h existing principal/emisora fields unaffected by new fields',
|
||||
() async {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
final servicio = ServicioEcualizador(prefs: prefs);
|
||||
await servicio.guardarPrincipal(PresetEcualizador.pop);
|
||||
await servicio.guardarPorEmisora('station-X', PresetEcualizador.voz);
|
||||
await servicio.guardarToggleMultiDispositivo(true);
|
||||
await servicio.guardarPresetDispositivo(
|
||||
'builtin_speaker',
|
||||
PresetEcualizador.jazz,
|
||||
);
|
||||
|
||||
final config = await servicio.cargar();
|
||||
expect(config.principal, equals(PresetEcualizador.pop));
|
||||
expect(config.porEmisora['station-X'], equals(PresetEcualizador.voz));
|
||||
expect(config.eqMultiDeviceEnabled, isTrue);
|
||||
expect(
|
||||
config.presetsDispositivo['builtin_speaker'],
|
||||
equals(PresetEcualizador.jazz),
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
test('4.1i guardarConfiguracion with new fields round-trips', () async {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
final servicio = ServicioEcualizador(prefs: prefs);
|
||||
final config = ConfiguracionEcualizador(
|
||||
principal: PresetEcualizador.rock,
|
||||
porEmisora: {'station-1': PresetEcualizador.jazz},
|
||||
activo: true,
|
||||
eqMultiDeviceEnabled: true,
|
||||
presetsDispositivo: {
|
||||
'bt_a2dp:AA:BB:CC:DD:EE:FF': PresetEcualizador.voz,
|
||||
},
|
||||
presetsMatriz: {
|
||||
'station-1:bt_a2dp:AA:BB:CC:DD:EE:FF': PresetEcualizador.bassBoost,
|
||||
},
|
||||
);
|
||||
|
||||
await servicio.guardarConfiguracion(config);
|
||||
final restored = await servicio.cargar();
|
||||
|
||||
expect(restored.principal, equals(PresetEcualizador.rock));
|
||||
expect(restored.porEmisora['station-1'], equals(PresetEcualizador.jazz));
|
||||
expect(restored.eqMultiDeviceEnabled, isTrue);
|
||||
expect(
|
||||
restored.presetsDispositivo['bt_a2dp:AA:BB:CC:DD:EE:FF'],
|
||||
equals(PresetEcualizador.voz),
|
||||
);
|
||||
expect(
|
||||
restored.presetsMatriz['station-1:bt_a2dp:AA:BB:CC:DD:EE:FF'],
|
||||
equals(PresetEcualizador.bassBoost),
|
||||
);
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -7,53 +7,53 @@ import 'package:pluriwave/servicios/servicio_export_import.dart';
|
||||
void main() {
|
||||
const servicio = ServicioExportImport();
|
||||
|
||||
// Shared fixture data used across multiple tests.
|
||||
const gruposFavoritosFixture = [
|
||||
GrupoFavoritos(id: 'g1', nombre: 'Rock', orden: 1),
|
||||
GrupoFavoritos(
|
||||
id: GrupoFavoritos.sinAsignarId,
|
||||
nombre: 'Sin asignar',
|
||||
orden: 0,
|
||||
protegido: true,
|
||||
),
|
||||
];
|
||||
const favoritosFixture = [
|
||||
Emisora(
|
||||
uuid: 'fav-1',
|
||||
nombre: 'Radio Uno',
|
||||
url: 'https://uno.example/stream',
|
||||
grupoFavoritosId: 'g1',
|
||||
bitrate: 192,
|
||||
),
|
||||
];
|
||||
const emisorasCustomFixture = [
|
||||
Emisora(
|
||||
uuid: 'custom-1',
|
||||
nombre: 'Mi Radio',
|
||||
url: 'https://mia.example/stream',
|
||||
),
|
||||
];
|
||||
const alarmasFixture = {
|
||||
'alarmas': [
|
||||
{'id': 'a1', 'hora': 7, 'minuto': 30, 'activa': true, 'snoozeMinutos': 5},
|
||||
],
|
||||
'vacaciones': [
|
||||
{'desde': '2026-07-01', 'hasta': '2026-07-15'},
|
||||
],
|
||||
};
|
||||
|
||||
group('ServicioExportImport (S4-R4)', () {
|
||||
test('v2 round-trip: exportar then importar yields a deep-equal config '
|
||||
'(S4-R4-A, S6-R2 test #4)', () {
|
||||
final config = servicio.construirExportacion(
|
||||
gruposFavoritos: const [
|
||||
GrupoFavoritos(id: 'g1', nombre: 'Rock', orden: 1),
|
||||
GrupoFavoritos(
|
||||
id: GrupoFavoritos.sinAsignarId,
|
||||
nombre: 'Sin asignar',
|
||||
orden: 0,
|
||||
protegido: true,
|
||||
),
|
||||
],
|
||||
favoritos: const [
|
||||
Emisora(
|
||||
uuid: 'fav-1',
|
||||
nombre: 'Radio Uno',
|
||||
url: 'https://uno.example/stream',
|
||||
grupoFavoritosId: 'g1',
|
||||
bitrate: 192,
|
||||
),
|
||||
],
|
||||
emisorasCustom: const [
|
||||
Emisora(
|
||||
uuid: 'custom-1',
|
||||
nombre: 'Mi Radio',
|
||||
url: 'https://mia.example/stream',
|
||||
),
|
||||
],
|
||||
gruposFavoritos: gruposFavoritosFixture,
|
||||
favoritos: favoritosFixture,
|
||||
emisorasCustom: emisorasCustomFixture,
|
||||
presetPrincipal: PresetEcualizador.jazz,
|
||||
presetsPorEmisora: {'fav-1': PresetEcualizador.rock},
|
||||
// Raw alarm block exactly as ServicioAlarmas persists it (alarms +
|
||||
// vacations + exceptions) — must pass through untouched.
|
||||
alarmas: const {
|
||||
'alarmas': [
|
||||
{
|
||||
'id': 'a1',
|
||||
'hora': 7,
|
||||
'minuto': 30,
|
||||
'activa': true,
|
||||
'snoozeMinutos': 5,
|
||||
},
|
||||
],
|
||||
'vacaciones': [
|
||||
{'desde': '2026-07-01', 'hasta': '2026-07-15'},
|
||||
],
|
||||
},
|
||||
alarmas: alarmasFixture,
|
||||
emisoraPreferidaUuid: 'fav-1',
|
||||
ordenListas: 'calidad',
|
||||
timerSuenoPresetsSegundos: const [300, 600, 1800],
|
||||
@@ -80,4 +80,156 @@ void main() {
|
||||
expect(servicio.importar('[1, 2, 3]'), isNull);
|
||||
});
|
||||
});
|
||||
|
||||
group('ServicioExportImport v3 — multi-device EQ (Phase 6)', () {
|
||||
test(
|
||||
'6.1-A: v3 export includes schemaVersion 3 and device/matrix preset keys',
|
||||
() {
|
||||
final presetDispositivo = PresetEcualizador.rock;
|
||||
final presetMatriz = PresetEcualizador.jazz;
|
||||
|
||||
final config = servicio.construirExportacion(
|
||||
gruposFavoritos: gruposFavoritosFixture,
|
||||
favoritos: favoritosFixture,
|
||||
emisorasCustom: emisorasCustomFixture,
|
||||
presetPrincipal: PresetEcualizador.flat,
|
||||
presetsPorEmisora: {},
|
||||
alarmas: null,
|
||||
emisoraPreferidaUuid: null,
|
||||
ordenListas: 'calidad',
|
||||
timerSuenoPresetsSegundos: const [300],
|
||||
presetsPorDispositivo: {
|
||||
'bt_a2dp:AA:BB:CC:DD:EE:FF': presetDispositivo,
|
||||
},
|
||||
presetsMatriz: {'fav-1:bt_a2dp:AA:BB:CC:DD:EE:FF': presetMatriz},
|
||||
eqMultiDeviceEnabled: true,
|
||||
);
|
||||
|
||||
// v3 schema version.
|
||||
expect(config['version'], 3);
|
||||
|
||||
// Device preset map present and correct.
|
||||
expect(config['presetsPorDispositivo'], isA<Map>());
|
||||
final dispositivoMap =
|
||||
config['presetsPorDispositivo'] as Map<String, dynamic>;
|
||||
expect(dispositivoMap, contains('bt_a2dp:AA:BB:CC:DD:EE:FF'));
|
||||
|
||||
// Matrix preset map present and correct.
|
||||
expect(config['presetsMatriz'], isA<Map>());
|
||||
final matrizMap = config['presetsMatriz'] as Map<String, dynamic>;
|
||||
expect(matrizMap, contains('fav-1:bt_a2dp:AA:BB:CC:DD:EE:FF'));
|
||||
|
||||
// Toggle exported.
|
||||
expect(config['eqMultiDeviceEnabled'], isTrue);
|
||||
},
|
||||
);
|
||||
|
||||
test(
|
||||
'6.1-B: v3 round-trip preserves device and matrix presets exactly',
|
||||
() {
|
||||
final dispositivoPreset = PresetEcualizador.rock;
|
||||
final matrizPreset = PresetEcualizador.jazz;
|
||||
|
||||
final config = servicio.construirExportacion(
|
||||
gruposFavoritos: gruposFavoritosFixture,
|
||||
favoritos: favoritosFixture,
|
||||
emisorasCustom: emisorasCustomFixture,
|
||||
presetPrincipal: PresetEcualizador.flat,
|
||||
presetsPorEmisora: {'fav-1': PresetEcualizador.pop},
|
||||
alarmas: alarmasFixture,
|
||||
emisoraPreferidaUuid: 'fav-1',
|
||||
ordenListas: 'calidad',
|
||||
timerSuenoPresetsSegundos: const [300, 1800],
|
||||
presetsPorDispositivo: {
|
||||
'bt_a2dp:AA:BB:CC:DD:EE:FF': dispositivoPreset,
|
||||
'wired_headset': PresetEcualizador.flat,
|
||||
},
|
||||
presetsMatriz: {'fav-1:bt_a2dp:AA:BB:CC:DD:EE:FF': matrizPreset},
|
||||
eqMultiDeviceEnabled: true,
|
||||
);
|
||||
|
||||
final json = servicio.exportar(config);
|
||||
final importado = servicio.importar(json);
|
||||
|
||||
expect(importado, isNotNull);
|
||||
expect(importado!['version'], 3);
|
||||
|
||||
// Device presets survive the round-trip.
|
||||
final deviceMap =
|
||||
importado['presetsPorDispositivo'] as Map<String, dynamic>;
|
||||
expect(
|
||||
deviceMap['bt_a2dp:AA:BB:CC:DD:EE:FF'],
|
||||
equals(dispositivoPreset.toJson()),
|
||||
);
|
||||
expect(
|
||||
deviceMap['wired_headset'],
|
||||
equals(PresetEcualizador.flat.toJson()),
|
||||
);
|
||||
|
||||
// Matrix presets survive the round-trip.
|
||||
final matrizMap = importado['presetsMatriz'] as Map<String, dynamic>;
|
||||
expect(
|
||||
matrizMap['fav-1:bt_a2dp:AA:BB:CC:DD:EE:FF'],
|
||||
equals(matrizPreset.toJson()),
|
||||
);
|
||||
|
||||
// Toggle survives.
|
||||
expect(importado['eqMultiDeviceEnabled'], isTrue);
|
||||
},
|
||||
);
|
||||
|
||||
test(
|
||||
'6.1-C: v2 file (no device fields) imports cleanly — missing keys default to empty maps',
|
||||
() {
|
||||
// Simulate a v2 export file (no presetsPorDispositivo / presetsMatriz).
|
||||
final v2Config = servicio.construirExportacion(
|
||||
gruposFavoritos: gruposFavoritosFixture,
|
||||
favoritos: favoritosFixture,
|
||||
emisorasCustom: emisorasCustomFixture,
|
||||
presetPrincipal: PresetEcualizador.jazz,
|
||||
presetsPorEmisora: {'fav-1': PresetEcualizador.rock},
|
||||
alarmas: null,
|
||||
emisoraPreferidaUuid: null,
|
||||
ordenListas: 'calidad',
|
||||
timerSuenoPresetsSegundos: const [300],
|
||||
// No device fields — emulates a v2 export from the old app.
|
||||
);
|
||||
|
||||
// Verify the export has no device keys (a v2 file would not).
|
||||
expect(v2Config.containsKey('presetsPorDispositivo'), isFalse);
|
||||
expect(v2Config.containsKey('presetsMatriz'), isFalse);
|
||||
expect(v2Config.containsKey('eqMultiDeviceEnabled'), isFalse);
|
||||
|
||||
// Import it — parsing should succeed.
|
||||
final json = servicio.exportar(v2Config);
|
||||
final importado = servicio.importar(json);
|
||||
expect(importado, isNotNull);
|
||||
|
||||
// Missing device keys should gracefully resolve to null (caller uses ?? {}).
|
||||
expect(importado!['presetsPorDispositivo'], isNull);
|
||||
expect(importado['presetsMatriz'], isNull);
|
||||
expect(importado['eqMultiDeviceEnabled'], isNull);
|
||||
},
|
||||
);
|
||||
|
||||
test(
|
||||
'6.1-D: unknown schemaVersion (v4-future) does not crash importar()',
|
||||
() {
|
||||
// Simulate a future v4 export (unknown version, extra fields).
|
||||
final futurista = <String, dynamic>{
|
||||
'version': 99,
|
||||
'presetsPorDispositivo': <String, dynamic>{},
|
||||
'presetsMatriz': <String, dynamic>{},
|
||||
'unknownFutureField': 'some-value',
|
||||
};
|
||||
final json = servicio.exportar(futurista);
|
||||
// importar() should parse without throwing — version checking is the
|
||||
// caller's responsibility (EstadoRadio.importarConfig).
|
||||
expect(() => servicio.importar(json), returnsNormally);
|
||||
final importado = servicio.importar(json);
|
||||
expect(importado, isNotNull);
|
||||
expect(importado!['version'], 99);
|
||||
},
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user