Fix multi-device EQ auto-switching by calling obtenerDispositivoActual() during cargarPersistido() to seed the initial device ID. Add device management modal with rename support, EQ preset editing, and connection status indicator. Translate device UI keys to all 13 locales.
946 lines
33 KiB
Dart
946 lines
33 KiB
Dart
import 'package:flutter_test/flutter_test.dart';
|
||
import 'package:pluriwave/estado/estado_ecualizador.dart';
|
||
import 'package:pluriwave/estado/estado_radio.dart';
|
||
import 'package:pluriwave/modelos/dispositivo_audio.dart';
|
||
import 'package:pluriwave/modelos/preset_ecualizador.dart';
|
||
|
||
import '../helpers/fakes.dart';
|
||
|
||
void main() {
|
||
group('EstadoEcualizador (S4-R1)', () {
|
||
test(
|
||
'cambiarPreset notifies EstadoEcualizador listeners (S4-R1-A)',
|
||
() async {
|
||
final audio = FakeServicioAudio();
|
||
final eq = EstadoEcualizador(
|
||
audio: audio,
|
||
servicio: FakeServicioEcualizador(),
|
||
);
|
||
var avisos = 0;
|
||
eq.addListener(() => avisos++);
|
||
|
||
await eq.cambiarPreset(PresetEcualizador.jazz);
|
||
|
||
expect(avisos, greaterThanOrEqualTo(1));
|
||
expect(eq.presetActual, PresetEcualizador.jazz);
|
||
expect(audio.presetsAplicados, contains(PresetEcualizador.jazz));
|
||
eq.dispose();
|
||
},
|
||
);
|
||
|
||
test('EQ preset change does NOT rebuild EstadoRadio listeners '
|
||
'(S4-R1-A, S4-R5)', () async {
|
||
final estado = EstadoRadio(
|
||
audio: FakeServicioAudio(),
|
||
favoritos: FakeServicioFavoritos(),
|
||
radio: FakeServicioRadio(),
|
||
servicioEcualizador: FakeServicioEcualizador(),
|
||
iniciarAutomaticamente: false,
|
||
);
|
||
var avisosRadio = 0;
|
||
var avisosEq = 0;
|
||
estado.addListener(() => avisosRadio++);
|
||
estado.ecualizador.addListener(() => avisosEq++);
|
||
|
||
await estado.ecualizador.cambiarPreset(PresetEcualizador.jazz);
|
||
|
||
expect(avisosEq, greaterThanOrEqualTo(1));
|
||
expect(avisosRadio, 0);
|
||
estado.dispose();
|
||
});
|
||
});
|
||
|
||
// ---------------------------------------------------------------------------
|
||
// Phase 5: 4-level resolution hierarchy
|
||
// ---------------------------------------------------------------------------
|
||
|
||
group('EstadoEcualizador — 4-level resolution (Phase 5)', () {
|
||
const btDevice = DispositivoAudio(
|
||
id: 'bt_a2dp:AA:BB:CC:DD:EE:FF',
|
||
tipo: TipoDispositivo.bluetoothA2dp,
|
||
nombre: 'BT Speaker',
|
||
);
|
||
const stationUuid = 'station-uuid-123';
|
||
|
||
EstadoEcualizador buildEq({
|
||
FakeServicioEcualizador? servicio,
|
||
FakeServicioDispositivoAudio? dispositivoAudio,
|
||
bool multiDeviceEnabled = true,
|
||
String? emisoraActualUuid,
|
||
}) {
|
||
return EstadoEcualizador(
|
||
audio: FakeServicioAudio(),
|
||
servicio:
|
||
servicio ??
|
||
FakeServicioEcualizador(eqMultiDeviceEnabled: multiDeviceEnabled),
|
||
dispositivoAudio: dispositivoAudio ?? FakeServicioDispositivoAudio(),
|
||
emisoraActualUuid: () => emisoraActualUuid,
|
||
);
|
||
}
|
||
|
||
// 5.1a — matrix entry wins over all others
|
||
test(
|
||
'5.1a matrix entry (stationUuid:deviceId) wins top priority',
|
||
() async {
|
||
final matrizPreset = PresetEcualizador.bassBoost;
|
||
final stationPreset = PresetEcualizador.rock;
|
||
final devicePreset = PresetEcualizador.jazz;
|
||
final globalPreset = PresetEcualizador.pop;
|
||
|
||
final eq = buildEq(
|
||
servicio: FakeServicioEcualizador(
|
||
principal: globalPreset,
|
||
porEmisora: {stationUuid: stationPreset},
|
||
eqMultiDeviceEnabled: true,
|
||
presetsDispositivo: {btDevice.id: devicePreset},
|
||
presetsMatriz: {'$stationUuid:${btDevice.id}': matrizPreset},
|
||
),
|
||
emisoraActualUuid: stationUuid,
|
||
);
|
||
await eq.cargarPersistido();
|
||
|
||
expect(
|
||
eq.presetEfectivo(stationUuid: stationUuid, deviceId: btDevice.id),
|
||
equals(matrizPreset),
|
||
);
|
||
eq.dispose();
|
||
},
|
||
);
|
||
|
||
// 5.1b — falls back to station when no matrix entry
|
||
test('5.1b falls back to station preset when no matrix entry', () async {
|
||
final stationPreset = PresetEcualizador.rock;
|
||
final devicePreset = PresetEcualizador.jazz;
|
||
final globalPreset = PresetEcualizador.pop;
|
||
|
||
final eq = buildEq(
|
||
servicio: FakeServicioEcualizador(
|
||
principal: globalPreset,
|
||
porEmisora: {stationUuid: stationPreset},
|
||
eqMultiDeviceEnabled: true,
|
||
presetsDispositivo: {btDevice.id: devicePreset},
|
||
presetsMatriz: {},
|
||
),
|
||
emisoraActualUuid: stationUuid,
|
||
);
|
||
await eq.cargarPersistido();
|
||
|
||
expect(
|
||
eq.presetEfectivo(stationUuid: stationUuid, deviceId: btDevice.id),
|
||
equals(stationPreset),
|
||
);
|
||
eq.dispose();
|
||
});
|
||
|
||
// 5.1c — falls back to device when no station preset
|
||
test(
|
||
'5.1c falls back to device preset when no station or matrix entry',
|
||
() async {
|
||
final devicePreset = PresetEcualizador.jazz;
|
||
final globalPreset = PresetEcualizador.pop;
|
||
|
||
final eq = buildEq(
|
||
servicio: FakeServicioEcualizador(
|
||
principal: globalPreset,
|
||
porEmisora: {},
|
||
eqMultiDeviceEnabled: true,
|
||
presetsDispositivo: {btDevice.id: devicePreset},
|
||
presetsMatriz: {},
|
||
),
|
||
emisoraActualUuid: stationUuid,
|
||
);
|
||
await eq.cargarPersistido();
|
||
|
||
expect(
|
||
eq.presetEfectivo(stationUuid: stationUuid, deviceId: btDevice.id),
|
||
equals(devicePreset),
|
||
);
|
||
eq.dispose();
|
||
},
|
||
);
|
||
|
||
// 5.1d — falls back to global when nothing else matches
|
||
test(
|
||
'5.1d falls back to global preset when no matrix, station, or device',
|
||
() async {
|
||
final globalPreset = PresetEcualizador.pop;
|
||
|
||
final eq = buildEq(
|
||
servicio: FakeServicioEcualizador(
|
||
principal: globalPreset,
|
||
porEmisora: {},
|
||
eqMultiDeviceEnabled: true,
|
||
presetsDispositivo: {},
|
||
presetsMatriz: {},
|
||
),
|
||
emisoraActualUuid: stationUuid,
|
||
);
|
||
await eq.cargarPersistido();
|
||
|
||
expect(
|
||
eq.presetEfectivo(stationUuid: stationUuid, deviceId: btDevice.id),
|
||
equals(globalPreset),
|
||
);
|
||
eq.dispose();
|
||
},
|
||
);
|
||
|
||
// 5.1e — toggle off collapses to 2-level
|
||
test(
|
||
'5.1e toggle off: presetEfectivo uses only station → global',
|
||
() async {
|
||
final stationPreset = PresetEcualizador.rock;
|
||
final devicePreset = PresetEcualizador.jazz;
|
||
final matrizPreset = PresetEcualizador.bassBoost;
|
||
|
||
final eq = buildEq(
|
||
servicio: FakeServicioEcualizador(
|
||
principal: PresetEcualizador.flat,
|
||
porEmisora: {stationUuid: stationPreset},
|
||
eqMultiDeviceEnabled: false,
|
||
presetsDispositivo: {btDevice.id: devicePreset},
|
||
presetsMatriz: {'$stationUuid:${btDevice.id}': matrizPreset},
|
||
),
|
||
multiDeviceEnabled: false,
|
||
emisoraActualUuid: stationUuid,
|
||
);
|
||
await eq.cargarPersistido();
|
||
|
||
// When toggle is off, device and matrix entries must be ignored
|
||
expect(
|
||
eq.presetEfectivo(stationUuid: stationUuid, deviceId: btDevice.id),
|
||
equals(stationPreset),
|
||
);
|
||
eq.dispose();
|
||
},
|
||
);
|
||
|
||
// 5.1f — toggle off with no station falls back to global
|
||
test('5.1f toggle off with no station: falls back to global', () async {
|
||
final devicePreset = PresetEcualizador.jazz;
|
||
|
||
final eq = buildEq(
|
||
servicio: FakeServicioEcualizador(
|
||
principal: PresetEcualizador.flat,
|
||
porEmisora: {},
|
||
eqMultiDeviceEnabled: false,
|
||
presetsDispositivo: {btDevice.id: devicePreset},
|
||
presetsMatriz: {},
|
||
),
|
||
multiDeviceEnabled: false,
|
||
emisoraActualUuid: stationUuid,
|
||
);
|
||
await eq.cargarPersistido();
|
||
|
||
expect(
|
||
eq.presetEfectivo(stationUuid: stationUuid, deviceId: btDevice.id),
|
||
equals(PresetEcualizador.flat),
|
||
);
|
||
eq.dispose();
|
||
});
|
||
});
|
||
|
||
// ---------------------------------------------------------------------------
|
||
// Phase 5.3: Device stream subscription triggers re-resolution
|
||
// ---------------------------------------------------------------------------
|
||
|
||
group('EstadoEcualizador — device stream subscription (Phase 5.3–5.4)', () {
|
||
const stationUuid = 'station-uuid-stream';
|
||
const btDevice = DispositivoAudio(
|
||
id: 'bt_a2dp:AA:BB:CC:DD:EE:FF',
|
||
tipo: TipoDispositivo.bluetoothA2dp,
|
||
nombre: 'BT Speaker',
|
||
);
|
||
|
||
test('5.3a device change emits notifyListeners', () async {
|
||
final fakeAudio = FakeServicioAudio();
|
||
final fakeDispositivo = FakeServicioDispositivoAudio();
|
||
final eq = EstadoEcualizador(
|
||
audio: fakeAudio,
|
||
servicio: FakeServicioEcualizador(
|
||
eqMultiDeviceEnabled: true,
|
||
presetsDispositivo: {btDevice.id: PresetEcualizador.jazz},
|
||
),
|
||
dispositivoAudio: fakeDispositivo,
|
||
emisoraActualUuid: () => stationUuid,
|
||
);
|
||
await eq.cargarPersistido();
|
||
|
||
var avisos = 0;
|
||
eq.addListener(() => avisos++);
|
||
|
||
fakeDispositivo.emitirDispositivo(btDevice);
|
||
// Wait for async processing
|
||
await Future<void>.delayed(const Duration(milliseconds: 50));
|
||
|
||
expect(avisos, greaterThanOrEqualTo(1));
|
||
eq.dispose();
|
||
});
|
||
|
||
test(
|
||
'5.3b device change re-applies resolved preset to audio engine',
|
||
() async {
|
||
final fakeAudio = FakeServicioAudio();
|
||
final fakeDispositivo = FakeServicioDispositivoAudio();
|
||
final btPreset = PresetEcualizador.jazz;
|
||
|
||
final eq = EstadoEcualizador(
|
||
audio: fakeAudio,
|
||
servicio: FakeServicioEcualizador(
|
||
eqMultiDeviceEnabled: true,
|
||
presetsDispositivo: {btDevice.id: btPreset},
|
||
),
|
||
dispositivoAudio: fakeDispositivo,
|
||
emisoraActualUuid: () => stationUuid,
|
||
);
|
||
await eq.cargarPersistido();
|
||
final initialCount = fakeAudio.presetsAplicados.length;
|
||
|
||
fakeDispositivo.emitirDispositivo(btDevice);
|
||
await Future<void>.delayed(const Duration(milliseconds: 50));
|
||
|
||
expect(fakeAudio.presetsAplicados.length, greaterThan(initialCount));
|
||
expect(fakeAudio.presetsAplicados.last, equals(btPreset));
|
||
eq.dispose();
|
||
},
|
||
);
|
||
|
||
test(
|
||
'5.3c toggle off: device stream events do NOT trigger any action',
|
||
() async {
|
||
final fakeAudio = FakeServicioAudio();
|
||
final fakeDispositivo = FakeServicioDispositivoAudio();
|
||
|
||
final eq = EstadoEcualizador(
|
||
audio: fakeAudio,
|
||
servicio: FakeServicioEcualizador(eqMultiDeviceEnabled: false),
|
||
dispositivoAudio: fakeDispositivo,
|
||
emisoraActualUuid: () => stationUuid,
|
||
);
|
||
await eq.cargarPersistido();
|
||
|
||
var avisos = 0;
|
||
eq.addListener(() => avisos++);
|
||
final initialCount = fakeAudio.presetsAplicados.length;
|
||
|
||
fakeDispositivo.emitirDispositivo(btDevice);
|
||
await Future<void>.delayed(const Duration(milliseconds: 50));
|
||
|
||
expect(avisos, equals(0));
|
||
expect(fakeAudio.presetsAplicados.length, equals(initialCount));
|
||
eq.dispose();
|
||
},
|
||
);
|
||
});
|
||
|
||
// ---------------------------------------------------------------------------
|
||
// Phase 5.5–5.6: First-seen device copies current preset
|
||
// ---------------------------------------------------------------------------
|
||
|
||
group(
|
||
'EstadoEcualizador — first-seen device initialization (Phase 5.5–5.6)',
|
||
() {
|
||
const stationUuid = 'station-uuid-first-seen';
|
||
const newDevice = DispositivoAudio(
|
||
id: 'bt_a2dp:NEW:DE:VI:CE:00:00',
|
||
tipo: TipoDispositivo.bluetoothA2dp,
|
||
nombre: 'New BT Device',
|
||
);
|
||
|
||
test('5.5a new device gets copy of current resolved preset', () async {
|
||
final fakeServicio = FakeServicioEcualizador(
|
||
principal: PresetEcualizador.rock,
|
||
eqMultiDeviceEnabled: true,
|
||
presetsDispositivo: {},
|
||
);
|
||
final fakeDispositivo = FakeServicioDispositivoAudio();
|
||
|
||
final eq = EstadoEcualizador(
|
||
audio: FakeServicioAudio(),
|
||
servicio: fakeServicio,
|
||
dispositivoAudio: fakeDispositivo,
|
||
emisoraActualUuid: () => stationUuid,
|
||
);
|
||
await eq.cargarPersistido();
|
||
|
||
fakeDispositivo.emitirDispositivo(newDevice);
|
||
await Future<void>.delayed(const Duration(milliseconds: 50));
|
||
|
||
// The device should now have a preset saved equal to the resolved preset
|
||
// at the time of first connection (rock — global fallback)
|
||
expect(
|
||
eq.presetsDispositivo[newDevice.id],
|
||
equals(PresetEcualizador.rock),
|
||
);
|
||
eq.dispose();
|
||
});
|
||
|
||
test(
|
||
'5.5b subsequent reconnection does NOT overwrite user-saved preset',
|
||
() async {
|
||
final userSavedPreset = PresetEcualizador.jazz;
|
||
final fakeServicio = FakeServicioEcualizador(
|
||
principal: PresetEcualizador.rock,
|
||
eqMultiDeviceEnabled: true,
|
||
presetsDispositivo: {newDevice.id: userSavedPreset},
|
||
);
|
||
final fakeDispositivo = FakeServicioDispositivoAudio();
|
||
|
||
final eq = EstadoEcualizador(
|
||
audio: FakeServicioAudio(),
|
||
servicio: fakeServicio,
|
||
dispositivoAudio: fakeDispositivo,
|
||
emisoraActualUuid: () => stationUuid,
|
||
);
|
||
await eq.cargarPersistido();
|
||
|
||
fakeDispositivo.emitirDispositivo(newDevice);
|
||
await Future<void>.delayed(const Duration(milliseconds: 50));
|
||
|
||
// Preset must remain the user-saved one, not overwritten with rock
|
||
expect(eq.presetsDispositivo[newDevice.id], equals(userSavedPreset));
|
||
eq.dispose();
|
||
},
|
||
);
|
||
},
|
||
);
|
||
|
||
// ---------------------------------------------------------------------------
|
||
// Phase 5.7–5.8: Toggle-off path uses only 2-level resolution
|
||
// ---------------------------------------------------------------------------
|
||
|
||
group('EstadoEcualizador — toggle-off path (Phase 5.7–5.8)', () {
|
||
const stationUuid = 'station-toggle-off';
|
||
const btDevice = DispositivoAudio(
|
||
id: 'bt_a2dp:AA:BB:CC:DD:EE:FF',
|
||
tipo: TipoDispositivo.bluetoothA2dp,
|
||
nombre: 'BT Speaker',
|
||
);
|
||
|
||
test(
|
||
'5.7a cargarPersistido with toggle off applies 2-level resolution',
|
||
() async {
|
||
final fakeAudio = FakeServicioAudio();
|
||
final stationPreset = PresetEcualizador.rock;
|
||
final devicePreset = PresetEcualizador.jazz;
|
||
|
||
final eq = EstadoEcualizador(
|
||
audio: fakeAudio,
|
||
servicio: FakeServicioEcualizador(
|
||
principal: PresetEcualizador.flat,
|
||
porEmisora: {stationUuid: stationPreset},
|
||
eqMultiDeviceEnabled: false,
|
||
presetsDispositivo: {btDevice.id: devicePreset},
|
||
),
|
||
dispositivoAudio: FakeServicioDispositivoAudio(),
|
||
emisoraActualUuid: () => stationUuid,
|
||
);
|
||
await eq.cargarPersistido();
|
||
|
||
// With toggle off, no device subscription, direct 2-level on presetEfectivo
|
||
expect(
|
||
eq.presetEfectivo(stationUuid: stationUuid, deviceId: btDevice.id),
|
||
equals(stationPreset),
|
||
);
|
||
eq.dispose();
|
||
},
|
||
);
|
||
|
||
test('5.8a eqMultiDeviceEnabled getter reflects loaded value', () async {
|
||
final eq = EstadoEcualizador(
|
||
audio: FakeServicioAudio(),
|
||
servicio: FakeServicioEcualizador(eqMultiDeviceEnabled: true),
|
||
dispositivoAudio: FakeServicioDispositivoAudio(),
|
||
);
|
||
await eq.cargarPersistido();
|
||
expect(eq.eqMultiDeviceEnabled, isTrue);
|
||
eq.dispose();
|
||
});
|
||
|
||
test('5.8b toggle defaults to false and eq behaves as before', () async {
|
||
final eq = EstadoEcualizador(
|
||
audio: FakeServicioAudio(),
|
||
servicio: FakeServicioEcualizador(eqMultiDeviceEnabled: false),
|
||
dispositivoAudio: FakeServicioDispositivoAudio(),
|
||
);
|
||
await eq.cargarPersistido();
|
||
expect(eq.eqMultiDeviceEnabled, isFalse);
|
||
eq.dispose();
|
||
});
|
||
|
||
test(
|
||
'5.8c cambiarMultiDeviceEnabled persists toggle and notifies',
|
||
() async {
|
||
final fakeServicio = FakeServicioEcualizador(
|
||
eqMultiDeviceEnabled: false,
|
||
);
|
||
final eq = EstadoEcualizador(
|
||
audio: FakeServicioAudio(),
|
||
servicio: fakeServicio,
|
||
dispositivoAudio: FakeServicioDispositivoAudio(),
|
||
);
|
||
await eq.cargarPersistido();
|
||
|
||
var avisos = 0;
|
||
eq.addListener(() => avisos++);
|
||
await eq.cambiarMultiDeviceEnabled(true);
|
||
|
||
expect(eq.eqMultiDeviceEnabled, isTrue);
|
||
expect(fakeServicio.config.eqMultiDeviceEnabled, isTrue);
|
||
expect(avisos, greaterThanOrEqualTo(1));
|
||
eq.dispose();
|
||
},
|
||
);
|
||
});
|
||
|
||
// ---------------------------------------------------------------------------
|
||
// Phase 5.9: REFACTOR — aplicarPresetActivo path applies device-resolved preset
|
||
// ---------------------------------------------------------------------------
|
||
|
||
group(
|
||
'EstadoEcualizador — aplicarPresetActivo uses device resolution (5.9)',
|
||
() {
|
||
const stationUuid = 'station-refactor';
|
||
const btDevice = DispositivoAudio(
|
||
id: 'bt_a2dp:AA:BB:CC:DD:EE:FF',
|
||
tipo: TipoDispositivo.bluetoothA2dp,
|
||
nombre: 'BT Speaker',
|
||
);
|
||
|
||
test(
|
||
'5.9a aplicarPresetEmisoraActual uses device-resolved preset when toggle on',
|
||
() async {
|
||
final fakeAudio = FakeServicioAudio();
|
||
final fakeDispositivo =
|
||
FakeServicioDispositivoAudio()..emitirDispositivo(btDevice);
|
||
final matrizPreset = PresetEcualizador.bassBoost;
|
||
|
||
final eq = EstadoEcualizador(
|
||
audio: fakeAudio,
|
||
servicio: FakeServicioEcualizador(
|
||
eqMultiDeviceEnabled: true,
|
||
presetsMatriz: {'$stationUuid:${btDevice.id}': matrizPreset},
|
||
),
|
||
dispositivoAudio: fakeDispositivo,
|
||
emisoraActualUuid: () => stationUuid,
|
||
);
|
||
await eq.cargarPersistido();
|
||
await Future<void>.delayed(const Duration(milliseconds: 50));
|
||
|
||
// Clear application history from init; emit device to trigger re-apply
|
||
fakeAudio.presetsAplicados.clear();
|
||
fakeDispositivo.emitirDispositivo(btDevice);
|
||
await Future<void>.delayed(const Duration(milliseconds: 50));
|
||
|
||
expect(fakeAudio.presetsAplicados.last, equals(matrizPreset));
|
||
eq.dispose();
|
||
},
|
||
);
|
||
},
|
||
);
|
||
|
||
// ---------------------------------------------------------------------------
|
||
// eq-device-autoswitch-ux Phase 2: startup seeding + rename API
|
||
// ---------------------------------------------------------------------------
|
||
|
||
group('EstadoEcualizador — startup device seeding (eq-device-autoswitch-ux Phase 2)', () {
|
||
const btDevice = DispositivoAudio(
|
||
id: 'bt_a2dp:AA:BB',
|
||
tipo: TipoDispositivo.bluetoothA2dp,
|
||
nombre: 'BT Speaker',
|
||
);
|
||
|
||
// 2.1 RED — after cargarPersistido with multiDevice ON, dispositivoActualId is seeded
|
||
test('2.1 cargarPersistido seeds dispositivoActualId from obtenerDispositivoActual', () async {
|
||
final fakeDispositivo = FakeServicioDispositivoAudio()
|
||
..emitirDispositivo(btDevice); // sets _dispositivoActual
|
||
|
||
final eq = EstadoEcualizador(
|
||
audio: FakeServicioAudio(),
|
||
servicio: FakeServicioEcualizador(eqMultiDeviceEnabled: true),
|
||
dispositivoAudio: fakeDispositivo,
|
||
);
|
||
await eq.cargarPersistido();
|
||
|
||
expect(eq.dispositivoActualId, equals('bt_a2dp:AA:BB'));
|
||
eq.dispose();
|
||
});
|
||
|
||
// 2.2 RED — first-seen device is bootstrapped from resolved preset at startup
|
||
test('2.2 first-seen device is bootstrapped at startup with resolved preset', () async {
|
||
final fakeServicio = FakeServicioEcualizador(
|
||
principal: PresetEcualizador.rock,
|
||
eqMultiDeviceEnabled: true,
|
||
presetsDispositivo: {},
|
||
);
|
||
final fakeDispositivo = FakeServicioDispositivoAudio()
|
||
..emitirDispositivo(btDevice);
|
||
|
||
final eq = EstadoEcualizador(
|
||
audio: FakeServicioAudio(),
|
||
servicio: fakeServicio,
|
||
dispositivoAudio: fakeDispositivo,
|
||
);
|
||
await eq.cargarPersistido();
|
||
|
||
expect(
|
||
eq.presetsDispositivo[btDevice.id],
|
||
equals(PresetEcualizador.rock),
|
||
);
|
||
eq.dispose();
|
||
});
|
||
|
||
// 2.3 RED — obtenerDispositivoActual throws → fallback to 'builtin_speaker'
|
||
test('2.3 obtenerDispositivoActual throws → dispositivoActualId falls back to builtin_speaker', () async {
|
||
final fakeDispositivo = FakeServicioDispositivoAudioThrows();
|
||
|
||
final eq = EstadoEcualizador(
|
||
audio: FakeServicioAudio(),
|
||
servicio: FakeServicioEcualizador(eqMultiDeviceEnabled: true),
|
||
dispositivoAudio: fakeDispositivo,
|
||
);
|
||
await eq.cargarPersistido(); // must not throw
|
||
|
||
expect(eq.dispositivoActualId, equals('builtin_speaker'));
|
||
eq.dispose();
|
||
});
|
||
|
||
// 2.4 RED — toggle off → obtenerDispositivoActual is never called
|
||
test('2.4 eqMultiDeviceEnabled=false → obtenerDispositivoActual NOT called', () async {
|
||
final fakeDispositivo = FakeServicioDispositivoAudioThrows();
|
||
|
||
final eq = EstadoEcualizador(
|
||
audio: FakeServicioAudio(),
|
||
servicio: FakeServicioEcualizador(eqMultiDeviceEnabled: false),
|
||
dispositivoAudio: fakeDispositivo,
|
||
);
|
||
// If obtenerDispositivoActual were called it would throw → test would fail
|
||
await expectLater(eq.cargarPersistido(), completes);
|
||
expect(eq.dispositivoActualId, isNull);
|
||
eq.dispose();
|
||
});
|
||
});
|
||
|
||
group('EstadoEcualizador — rename API (eq-device-autoswitch-ux Phase 2)', () {
|
||
// 2.6 RED — renombrarDispositivo writes to _nombresDispositivos and notifies
|
||
test('2.6a renombrarDispositivo stores name and notifyListeners', () async {
|
||
final eq = EstadoEcualizador(
|
||
audio: FakeServicioAudio(),
|
||
servicio: FakeServicioEcualizador(eqMultiDeviceEnabled: true),
|
||
dispositivoAudio: FakeServicioDispositivoAudio(),
|
||
);
|
||
await eq.cargarPersistido();
|
||
|
||
var avisos = 0;
|
||
eq.addListener(() => avisos++);
|
||
await eq.renombrarDispositivo('bt_a2dp:AA:BB', 'Living Room BT');
|
||
|
||
expect(eq.obtenerNombreDispositivo('bt_a2dp:AA:BB'), equals('Living Room BT'));
|
||
expect(avisos, greaterThanOrEqualTo(1));
|
||
eq.dispose();
|
||
});
|
||
|
||
// 2.6 RED — empty name is no-op (preserves existing name)
|
||
test('2.6b renombrarDispositivo with empty string is no-op', () async {
|
||
final eq = EstadoEcualizador(
|
||
audio: FakeServicioAudio(),
|
||
servicio: FakeServicioEcualizador(eqMultiDeviceEnabled: true),
|
||
dispositivoAudio: FakeServicioDispositivoAudio(),
|
||
);
|
||
await eq.cargarPersistido();
|
||
await eq.renombrarDispositivo('bt_a2dp:AA:BB', 'My Headset');
|
||
await eq.renombrarDispositivo('bt_a2dp:AA:BB', '');
|
||
|
||
expect(eq.obtenerNombreDispositivo('bt_a2dp:AA:BB'), equals('My Headset'));
|
||
eq.dispose();
|
||
});
|
||
|
||
// 2.7 RED — obtenerNombreDispositivo returns empty string for unknown device
|
||
test('2.7 obtenerNombreDispositivo returns empty string for unknown device', () async {
|
||
final eq = EstadoEcualizador(
|
||
audio: FakeServicioAudio(),
|
||
servicio: FakeServicioEcualizador(),
|
||
dispositivoAudio: FakeServicioDispositivoAudio(),
|
||
);
|
||
await eq.cargarPersistido();
|
||
expect(eq.obtenerNombreDispositivo('unknown_device'), equals(''));
|
||
eq.dispose();
|
||
});
|
||
|
||
// 2.8 RED — nombreVisible fallback chain: custom > platform > id
|
||
test('2.8a nombreVisible returns custom name when available', () async {
|
||
final eq = EstadoEcualizador(
|
||
audio: FakeServicioAudio(),
|
||
servicio: FakeServicioEcualizador(eqMultiDeviceEnabled: true),
|
||
dispositivoAudio: FakeServicioDispositivoAudio(),
|
||
);
|
||
await eq.cargarPersistido();
|
||
await eq.renombrarDispositivo('bt_a2dp:AA:BB', 'Studio Monitors');
|
||
|
||
expect(
|
||
eq.nombreVisible('bt_a2dp:AA:BB', 'USB Audio'),
|
||
equals('Studio Monitors'),
|
||
);
|
||
eq.dispose();
|
||
});
|
||
|
||
test('2.8b nombreVisible returns platform name when no custom name', () async {
|
||
final eq = EstadoEcualizador(
|
||
audio: FakeServicioAudio(),
|
||
servicio: FakeServicioEcualizador(),
|
||
dispositivoAudio: FakeServicioDispositivoAudio(),
|
||
);
|
||
await eq.cargarPersistido();
|
||
|
||
expect(
|
||
eq.nombreVisible('bt_a2dp:AA:BB', 'Sony WH-1000XM5'),
|
||
equals('Sony WH-1000XM5'),
|
||
);
|
||
eq.dispose();
|
||
});
|
||
|
||
test('2.8c nombreVisible returns raw deviceId when no custom and no platform name', () async {
|
||
final eq = EstadoEcualizador(
|
||
audio: FakeServicioAudio(),
|
||
servicio: FakeServicioEcualizador(),
|
||
dispositivoAudio: FakeServicioDispositivoAudio(),
|
||
);
|
||
await eq.cargarPersistido();
|
||
|
||
expect(eq.nombreVisible('bt_a2dp:AA:BB', ''), equals('bt_a2dp:AA:BB'));
|
||
eq.dispose();
|
||
});
|
||
|
||
// nombresDispositivos getter
|
||
test('2.9 nombresDispositivos getter returns current map', () async {
|
||
final eq = EstadoEcualizador(
|
||
audio: FakeServicioAudio(),
|
||
servicio: FakeServicioEcualizador(eqMultiDeviceEnabled: true),
|
||
dispositivoAudio: FakeServicioDispositivoAudio(),
|
||
);
|
||
await eq.cargarPersistido();
|
||
await eq.renombrarDispositivo('bt_a2dp:AA:BB', 'My BT');
|
||
|
||
expect(eq.nombresDispositivos['bt_a2dp:AA:BB'], equals('My BT'));
|
||
eq.dispose();
|
||
});
|
||
|
||
// 2.10 RED — _nombresDispositivos loaded from ConfiguracionEcualizador.nombresDispositivos
|
||
test('2.10 cargarPersistido loads nombresDispositivos from config', () async {
|
||
final eq = EstadoEcualizador(
|
||
audio: FakeServicioAudio(),
|
||
servicio: FakeServicioEcualizador(
|
||
nombresDispositivos: {'bt_a2dp:AA:BB': 'Loaded Name'},
|
||
),
|
||
dispositivoAudio: FakeServicioDispositivoAudio(),
|
||
);
|
||
await eq.cargarPersistido();
|
||
|
||
expect(eq.obtenerNombreDispositivo('bt_a2dp:AA:BB'), equals('Loaded Name'));
|
||
eq.dispose();
|
||
});
|
||
});
|
||
|
||
// ---------------------------------------------------------------------------
|
||
// CRITICAL-1: renombrarDispositivo is a no-op when toggle is OFF
|
||
// ---------------------------------------------------------------------------
|
||
|
||
group('EstadoEcualizador — renombrarDispositivo toggle guard (CRITICAL-1)', () {
|
||
test(
|
||
'renombrarDispositivo is no-op when eqMultiDeviceEnabled is false',
|
||
() async {
|
||
final fakeServicio = FakeServicioEcualizador(eqMultiDeviceEnabled: false);
|
||
final eq = EstadoEcualizador(
|
||
audio: FakeServicioAudio(),
|
||
servicio: fakeServicio,
|
||
dispositivoAudio: FakeServicioDispositivoAudio(),
|
||
);
|
||
await eq.cargarPersistido();
|
||
|
||
var avisos = 0;
|
||
eq.addListener(() => avisos++);
|
||
await eq.renombrarDispositivo('bt_a2dp:AA:BB', 'My Device');
|
||
|
||
// No name stored, no listeners notified, service not called
|
||
expect(eq.obtenerNombreDispositivo('bt_a2dp:AA:BB'), equals(''));
|
||
expect(avisos, equals(0));
|
||
expect(fakeServicio.config.nombresDispositivos, isEmpty);
|
||
eq.dispose();
|
||
},
|
||
);
|
||
|
||
test(
|
||
'renombrarDispositivo works normally when eqMultiDeviceEnabled is true',
|
||
() async {
|
||
final eq = EstadoEcualizador(
|
||
audio: FakeServicioAudio(),
|
||
servicio: FakeServicioEcualizador(eqMultiDeviceEnabled: true),
|
||
dispositivoAudio: FakeServicioDispositivoAudio(),
|
||
);
|
||
await eq.cargarPersistido();
|
||
|
||
var avisos = 0;
|
||
eq.addListener(() => avisos++);
|
||
await eq.renombrarDispositivo('bt_a2dp:AA:BB', 'My Device');
|
||
|
||
expect(eq.obtenerNombreDispositivo('bt_a2dp:AA:BB'), equals('My Device'));
|
||
expect(avisos, greaterThanOrEqualTo(1));
|
||
eq.dispose();
|
||
},
|
||
);
|
||
});
|
||
|
||
// ---------------------------------------------------------------------------
|
||
// CRITICAL-2: guardarPresetDispositivo method
|
||
// ---------------------------------------------------------------------------
|
||
|
||
group('EstadoEcualizador — guardarPresetDispositivo (CRITICAL-2)', () {
|
||
const btDevice = DispositivoAudio(
|
||
id: 'bt_a2dp:AA:BB',
|
||
tipo: TipoDispositivo.bluetoothA2dp,
|
||
nombre: 'BT Speaker',
|
||
);
|
||
|
||
test(
|
||
'guardarPresetDispositivo stores preset and notifies listeners',
|
||
() async {
|
||
final fakeServicio = FakeServicioEcualizador(
|
||
eqMultiDeviceEnabled: true,
|
||
presetsDispositivo: {btDevice.id: PresetEcualizador.flat},
|
||
);
|
||
final eq = EstadoEcualizador(
|
||
audio: FakeServicioAudio(),
|
||
servicio: fakeServicio,
|
||
dispositivoAudio: FakeServicioDispositivoAudio(),
|
||
);
|
||
await eq.cargarPersistido();
|
||
|
||
var avisos = 0;
|
||
eq.addListener(() => avisos++);
|
||
await eq.guardarPresetDispositivo(btDevice.id, PresetEcualizador.jazz);
|
||
|
||
expect(eq.presetsDispositivo[btDevice.id], equals(PresetEcualizador.jazz));
|
||
expect(fakeServicio.config.presetsDispositivo[btDevice.id], equals(PresetEcualizador.jazz));
|
||
expect(avisos, greaterThanOrEqualTo(1));
|
||
eq.dispose();
|
||
},
|
||
);
|
||
|
||
test(
|
||
'guardarPresetDispositivo is no-op when toggle is OFF',
|
||
() async {
|
||
final fakeServicio = FakeServicioEcualizador(
|
||
eqMultiDeviceEnabled: false,
|
||
presetsDispositivo: {btDevice.id: PresetEcualizador.flat},
|
||
);
|
||
final eq = EstadoEcualizador(
|
||
audio: FakeServicioAudio(),
|
||
servicio: fakeServicio,
|
||
dispositivoAudio: FakeServicioDispositivoAudio(),
|
||
);
|
||
await eq.cargarPersistido();
|
||
|
||
var avisos = 0;
|
||
eq.addListener(() => avisos++);
|
||
await eq.guardarPresetDispositivo(btDevice.id, PresetEcualizador.jazz);
|
||
|
||
// Preset must remain unchanged
|
||
expect(eq.presetsDispositivo[btDevice.id], equals(PresetEcualizador.flat));
|
||
expect(avisos, equals(0));
|
||
eq.dispose();
|
||
},
|
||
);
|
||
|
||
test(
|
||
'guardarPresetDispositivo triggers re-resolve and apply when device is active',
|
||
() async {
|
||
final fakeAudio = FakeServicioAudio();
|
||
final fakeDispositivo = FakeServicioDispositivoAudio()
|
||
..emitirDispositivo(btDevice);
|
||
const stationUuid = 'station-save-test';
|
||
final fakeServicio = FakeServicioEcualizador(
|
||
eqMultiDeviceEnabled: true,
|
||
presetsDispositivo: {btDevice.id: PresetEcualizador.flat},
|
||
);
|
||
final eq = EstadoEcualizador(
|
||
audio: fakeAudio,
|
||
servicio: fakeServicio,
|
||
dispositivoAudio: fakeDispositivo,
|
||
emisoraActualUuid: () => stationUuid,
|
||
);
|
||
await eq.cargarPersistido();
|
||
await Future<void>.delayed(const Duration(milliseconds: 50));
|
||
|
||
fakeAudio.presetsAplicados.clear();
|
||
await eq.guardarPresetDispositivo(btDevice.id, PresetEcualizador.rock);
|
||
|
||
// Audio was re-applied (re-resolve was triggered)
|
||
expect(fakeAudio.presetsAplicados, isNotEmpty);
|
||
// The resolved preset is the device preset (level 3, no station preset set)
|
||
expect(fakeAudio.presetsAplicados.last, equals(PresetEcualizador.rock));
|
||
eq.dispose();
|
||
},
|
||
);
|
||
|
||
test(
|
||
'guardarPresetDispositivo does NOT re-apply when device is not active',
|
||
() async {
|
||
final fakeAudio = FakeServicioAudio();
|
||
final fakeServicio = FakeServicioEcualizador(
|
||
eqMultiDeviceEnabled: true,
|
||
presetsDispositivo: {
|
||
btDevice.id: PresetEcualizador.flat,
|
||
'other_device': PresetEcualizador.flat,
|
||
},
|
||
);
|
||
final fakeDispositivo = FakeServicioDispositivoAudio()
|
||
..emitirDispositivo(btDevice);
|
||
final eq = EstadoEcualizador(
|
||
audio: fakeAudio,
|
||
servicio: fakeServicio,
|
||
dispositivoAudio: fakeDispositivo,
|
||
emisoraActualUuid: () => null,
|
||
);
|
||
await eq.cargarPersistido();
|
||
await Future<void>.delayed(const Duration(milliseconds: 50));
|
||
// Active device is btDevice; save for 'other_device'
|
||
fakeAudio.presetsAplicados.clear();
|
||
await eq.guardarPresetDispositivo('other_device', PresetEcualizador.rock);
|
||
|
||
// Preset saved but audio NOT re-applied since 'other_device' is not active
|
||
expect(eq.presetsDispositivo['other_device'], equals(PresetEcualizador.rock));
|
||
expect(fakeAudio.presetsAplicados, isEmpty);
|
||
eq.dispose();
|
||
},
|
||
);
|
||
});
|
||
|
||
// ---------------------------------------------------------------------------
|
||
// WARNING-3: FakeServicioEcualizador preserves nombresDispositivos
|
||
// ---------------------------------------------------------------------------
|
||
|
||
group('FakeServicioEcualizador — nombresDispositivos preservation (WARNING-3)', () {
|
||
test(
|
||
'guardarPrincipal preserves nombresDispositivos',
|
||
() async {
|
||
final fake = FakeServicioEcualizador(
|
||
nombresDispositivos: {'dev1': 'My Speaker'},
|
||
);
|
||
await fake.guardarPrincipal(PresetEcualizador.jazz);
|
||
expect(fake.config.nombresDispositivos['dev1'], equals('My Speaker'));
|
||
},
|
||
);
|
||
|
||
test(
|
||
'guardarActivo preserves nombresDispositivos',
|
||
() async {
|
||
final fake = FakeServicioEcualizador(
|
||
nombresDispositivos: {'dev1': 'My Speaker'},
|
||
);
|
||
await fake.guardarActivo(false);
|
||
expect(fake.config.nombresDispositivos['dev1'], equals('My Speaker'));
|
||
},
|
||
);
|
||
});
|
||
}
|