fix(eq): seed device ID at startup and add device management UI
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.
This commit is contained in:
@@ -538,4 +538,408 @@ void main() {
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// 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'));
|
||||
},
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -287,6 +287,7 @@ class FakeServicioEcualizador extends ServicioEcualizador {
|
||||
bool eqMultiDeviceEnabled = false,
|
||||
Map<String, PresetEcualizador>? presetsDispositivo,
|
||||
Map<String, PresetEcualizador>? presetsMatriz,
|
||||
Map<String, String>? nombresDispositivos,
|
||||
}) : _config = ConfiguracionEcualizador(
|
||||
principal: principal ?? PresetEcualizador.flat,
|
||||
porEmisora: porEmisora ?? {},
|
||||
@@ -294,6 +295,7 @@ class FakeServicioEcualizador extends ServicioEcualizador {
|
||||
eqMultiDeviceEnabled: eqMultiDeviceEnabled,
|
||||
presetsDispositivo: presetsDispositivo ?? {},
|
||||
presetsMatriz: presetsMatriz ?? {},
|
||||
nombresDispositivos: nombresDispositivos ?? {},
|
||||
);
|
||||
|
||||
ConfiguracionEcualizador _config;
|
||||
@@ -311,6 +313,7 @@ class FakeServicioEcualizador extends ServicioEcualizador {
|
||||
eqMultiDeviceEnabled: _config.eqMultiDeviceEnabled,
|
||||
presetsDispositivo: _config.presetsDispositivo,
|
||||
presetsMatriz: _config.presetsMatriz,
|
||||
nombresDispositivos: _config.nombresDispositivos,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -323,6 +326,7 @@ class FakeServicioEcualizador extends ServicioEcualizador {
|
||||
eqMultiDeviceEnabled: _config.eqMultiDeviceEnabled,
|
||||
presetsDispositivo: _config.presetsDispositivo,
|
||||
presetsMatriz: _config.presetsMatriz,
|
||||
nombresDispositivos: _config.nombresDispositivos,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -399,8 +403,45 @@ class FakeServicioEcualizador extends ServicioEcualizador {
|
||||
eqMultiDeviceEnabled: _config.eqMultiDeviceEnabled,
|
||||
presetsDispositivo: _config.presetsDispositivo,
|
||||
presetsMatriz: mapa,
|
||||
nombresDispositivos: _config.nombresDispositivos,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> guardarNombresDispositivos(Map<String, String> nombres) async {
|
||||
_config = ConfiguracionEcualizador(
|
||||
principal: _config.principal,
|
||||
porEmisora: _config.porEmisora,
|
||||
activo: _config.activo,
|
||||
eqMultiDeviceEnabled: _config.eqMultiDeviceEnabled,
|
||||
presetsDispositivo: _config.presetsDispositivo,
|
||||
presetsMatriz: _config.presetsMatriz,
|
||||
nombresDispositivos: nombres,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// A [ServicioDispositivoAudio] fake that throws on [obtenerDispositivoActual].
|
||||
///
|
||||
/// Used to test the graceful-failure path in [EstadoEcualizador.cargarPersistido].
|
||||
class FakeServicioDispositivoAudioThrows extends ServicioDispositivoAudio {
|
||||
final _controller = StreamController<DispositivoAudio>.broadcast();
|
||||
|
||||
@override
|
||||
DispositivoAudio? get dispositivoActual => null;
|
||||
|
||||
@override
|
||||
Stream<DispositivoAudio> get onDispositivoCambiado => _controller.stream;
|
||||
|
||||
@override
|
||||
Future<DispositivoAudio> obtenerDispositivoActual() async {
|
||||
throw Exception('Platform channel error: device unavailable');
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> dispose() async {
|
||||
await _controller.close();
|
||||
}
|
||||
}
|
||||
|
||||
/// Fake implementation of [ServicioDispositivoAudio] for unit tests.
|
||||
|
||||
@@ -8,6 +8,7 @@ import 'package:pluriwave/estado/estado_grabacion.dart';
|
||||
import 'package:pluriwave/estado/estado_idioma.dart';
|
||||
import 'package:pluriwave/estado/estado_radio.dart';
|
||||
import 'package:pluriwave/l10n/gen/app_localizations.dart';
|
||||
import 'package:pluriwave/modelos/dispositivo_audio.dart';
|
||||
import 'package:pluriwave/modelos/preset_ecualizador.dart';
|
||||
import 'package:pluriwave/pantallas/pantalla_ajustes.dart';
|
||||
import 'package:pluriwave/servicios/servicio_grabacion_radio.dart';
|
||||
@@ -92,6 +93,42 @@ void main() {
|
||||
await tester.pumpAndSettle(const Duration(milliseconds: 100));
|
||||
}
|
||||
|
||||
// Also update crearEstado to support nombresDispositivos
|
||||
Future<EstadoRadio> crearEstadoConNombres({
|
||||
bool eqMultiDeviceEnabled = true,
|
||||
Map<String, PresetEcualizador>? presetsDispositivo,
|
||||
Map<String, String>? nombresDispositivos,
|
||||
String? activeDeviceId,
|
||||
}) async {
|
||||
final fakeDispositivo = activeDeviceId != null
|
||||
? (FakeServicioDispositivoAudio()
|
||||
..emitirDispositivo(
|
||||
DispositivoAudio(
|
||||
id: activeDeviceId,
|
||||
tipo: TipoDispositivo.bluetoothA2dp,
|
||||
nombre: 'BT Speaker',
|
||||
),
|
||||
))
|
||||
: null;
|
||||
final estado = EstadoRadio(
|
||||
audio: FakeServicioAudio(),
|
||||
favoritos: FakeServicioFavoritos(),
|
||||
radio: FakeServicioRadio(),
|
||||
servicioEcualizador: FakeServicioEcualizador(
|
||||
eqMultiDeviceEnabled: eqMultiDeviceEnabled,
|
||||
presetsDispositivo: presetsDispositivo ??
|
||||
{'bt_a2dp:AA:BB': PresetEcualizador.rock},
|
||||
nombresDispositivos: nombresDispositivos ?? {},
|
||||
),
|
||||
servicioGrabacion: _FakeGrabacion(),
|
||||
resolverArchivoCustom: _archivoCustomVacio,
|
||||
iniciarAutomaticamente: false,
|
||||
dispositivoAudio: fakeDispositivo,
|
||||
);
|
||||
await estado.ecualizador.cargarPersistido();
|
||||
return estado;
|
||||
}
|
||||
|
||||
// ── Phase 7 tests ──────────────────────────────────────────────────────────
|
||||
|
||||
group('_SeccionEcualizadorAvanzado (Phase 7)', () {
|
||||
@@ -194,6 +231,155 @@ void main() {
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
// ── Phase 3 (eq-device-autoswitch-ux): connection indicator + modal ──────────
|
||||
|
||||
group('SeccionEcualizadorAvanzado connection indicator Phase 3', () {
|
||||
// 3.1 RED — active device row shows green connection dot
|
||||
testWidgets('3.1 active device row shows green connection indicator', (tester) async {
|
||||
setLargeSurface(tester);
|
||||
_suppressListTileInkAssertion();
|
||||
const activeId = 'bt_a2dp:AA:BB';
|
||||
final estado = await crearEstadoConNombres(
|
||||
eqMultiDeviceEnabled: true,
|
||||
presetsDispositivo: {
|
||||
activeId: PresetEcualizador.rock,
|
||||
'wired_headset': PresetEcualizador.jazz,
|
||||
},
|
||||
activeDeviceId: activeId,
|
||||
);
|
||||
addTearDown(estado.dispose);
|
||||
|
||||
await tester.pumpWidget(buildAjustes(estado));
|
||||
await pumpStable(tester);
|
||||
|
||||
await tester.scrollUntilVisible(
|
||||
find.text('Known audio devices'),
|
||||
300,
|
||||
scrollable: find.byType(Scrollable).first,
|
||||
);
|
||||
await pumpStable(tester);
|
||||
|
||||
// Green connection dot should be present (Icon with green color for active device)
|
||||
final greenIcons = tester.widgetList<Icon>(find.byType(Icon)).where(
|
||||
(icon) => icon.color == Colors.green,
|
||||
);
|
||||
// At least one green icon present
|
||||
expect(greenIcons, isNotEmpty);
|
||||
});
|
||||
|
||||
// 3.2 RED — tapping device row opens bottom sheet with TextField and EcualizadorWidget
|
||||
testWidgets('3.2 tapping device row opens modal with TextField', (tester) async {
|
||||
setLargeSurface(tester);
|
||||
_suppressListTileInkAssertion();
|
||||
final estado = await crearEstadoConNombres(
|
||||
presetsDispositivo: {'bt_a2dp:AA:BB': PresetEcualizador.rock},
|
||||
);
|
||||
addTearDown(estado.dispose);
|
||||
|
||||
await tester.pumpWidget(buildAjustes(estado));
|
||||
await pumpStable(tester);
|
||||
|
||||
await tester.scrollUntilVisible(
|
||||
find.text('Known audio devices'),
|
||||
300,
|
||||
scrollable: find.byType(Scrollable).first,
|
||||
);
|
||||
await pumpStable(tester);
|
||||
|
||||
// Tap the device row (tap the edit icon or the row itself)
|
||||
final editIcons = find.byIcon(Icons.edit_rounded);
|
||||
expect(editIcons, findsWidgets);
|
||||
await tester.tap(editIcons.first);
|
||||
await pumpStable(tester);
|
||||
|
||||
// Bottom sheet should appear with a TextField
|
||||
expect(find.byType(TextField), findsWidgets);
|
||||
});
|
||||
|
||||
// 3.6 RED — renaming in modal and confirming calls renombrarDispositivo
|
||||
testWidgets('3.6 confirming rename in modal updates device name', (tester) async {
|
||||
setLargeSurface(tester);
|
||||
_suppressListTileInkAssertion();
|
||||
const deviceId = 'bt_a2dp:AA:BB';
|
||||
final estado = await crearEstadoConNombres(
|
||||
presetsDispositivo: {deviceId: PresetEcualizador.rock},
|
||||
);
|
||||
addTearDown(estado.dispose);
|
||||
|
||||
await tester.pumpWidget(buildAjustes(estado));
|
||||
await pumpStable(tester);
|
||||
|
||||
await tester.scrollUntilVisible(
|
||||
find.text('Known audio devices'),
|
||||
300,
|
||||
scrollable: find.byType(Scrollable).first,
|
||||
);
|
||||
await pumpStable(tester);
|
||||
|
||||
// Open modal
|
||||
await tester.tap(find.byIcon(Icons.edit_rounded).first);
|
||||
await pumpStable(tester);
|
||||
|
||||
// Enter a new name
|
||||
final textField = find.byType(TextField).first;
|
||||
await tester.enterText(textField, 'My Living Room Speaker');
|
||||
await pumpStable(tester);
|
||||
|
||||
// Tap confirm/save button
|
||||
final saveButton = find.byIcon(Icons.save_rounded);
|
||||
expect(saveButton, findsWidgets);
|
||||
await tester.tap(saveButton.first);
|
||||
await pumpStable(tester);
|
||||
|
||||
// Device should now have the new name
|
||||
expect(
|
||||
estado.ecualizador.obtenerNombreDispositivo(deviceId),
|
||||
equals('My Living Room Speaker'),
|
||||
);
|
||||
});
|
||||
|
||||
// 3.7 RED — dismissing modal without confirming leaves name unchanged
|
||||
testWidgets('3.7 dismissing modal without confirming leaves name unchanged', (tester) async {
|
||||
setLargeSurface(tester);
|
||||
_suppressListTileInkAssertion();
|
||||
const deviceId = 'bt_a2dp:AA:BB';
|
||||
final estado = await crearEstadoConNombres(
|
||||
presetsDispositivo: {deviceId: PresetEcualizador.rock},
|
||||
nombresDispositivos: {deviceId: 'Original Name'},
|
||||
);
|
||||
addTearDown(estado.dispose);
|
||||
|
||||
await tester.pumpWidget(buildAjustes(estado));
|
||||
await pumpStable(tester);
|
||||
|
||||
await tester.scrollUntilVisible(
|
||||
find.text('Known audio devices'),
|
||||
300,
|
||||
scrollable: find.byType(Scrollable).first,
|
||||
);
|
||||
await pumpStable(tester);
|
||||
|
||||
// Open modal
|
||||
await tester.tap(find.byIcon(Icons.edit_rounded).first);
|
||||
await pumpStable(tester);
|
||||
|
||||
// Change the text but do NOT confirm
|
||||
final textField = find.byType(TextField).first;
|
||||
await tester.enterText(textField, 'New Name Not Saved');
|
||||
await pumpStable(tester);
|
||||
|
||||
// Dismiss by pressing back/escape
|
||||
await tester.tapAt(const Offset(100, 100)); // tap outside bottom sheet
|
||||
await pumpStable(tester);
|
||||
|
||||
// Name should remain unchanged
|
||||
expect(
|
||||
estado.ecualizador.obtenerNombreDispositivo(deviceId),
|
||||
equals('Original Name'),
|
||||
);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// ── Infrastructure ──────────────────────────────────────────────────────────
|
||||
|
||||
@@ -169,4 +169,64 @@ void main() {
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Phase 1 (eq-device-autoswitch-ux): nombresDispositivos persistence
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
group('ServicioEcualizador — nombresDispositivos (eq-device-autoswitch-ux)', () {
|
||||
// 1.1 RED — cargarNombresDispositivos returns empty map when SP key absent
|
||||
test('1.1 cargar returns empty nombresDispositivos when SP key absent', () async {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
final servicio = ServicioEcualizador(prefs: prefs);
|
||||
final config = await servicio.cargar();
|
||||
expect(config.nombresDispositivos, isEmpty);
|
||||
});
|
||||
|
||||
// 1.2 RED — guardarNombresDispositivos writes to eq_nombres_dispositivos_v1 SP key
|
||||
test('1.2 guardarNombresDispositivos persists names and cargar restores them', () async {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
final servicio = ServicioEcualizador(prefs: prefs);
|
||||
|
||||
await servicio.guardarNombresDispositivos({'bt_a2dp:AA:BB': 'Living Room BT'});
|
||||
final config = await servicio.cargar();
|
||||
|
||||
expect(config.nombresDispositivos['bt_a2dp:AA:BB'], equals('Living Room BT'));
|
||||
});
|
||||
|
||||
// 1.3 RED — round-trip: save → load returns same map
|
||||
test('1.3 round-trip save → cargar preserves all entries', () async {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
final servicio = ServicioEcualizador(prefs: prefs);
|
||||
const nombres = {
|
||||
'bt_a2dp:AA:BB': 'Living Room BT',
|
||||
'wired_headset': 'Office Headset',
|
||||
'builtin_speaker': 'Built-in',
|
||||
};
|
||||
|
||||
await servicio.guardarNombresDispositivos(nombres);
|
||||
final config = await servicio.cargar();
|
||||
|
||||
expect(config.nombresDispositivos, equals(nombres));
|
||||
});
|
||||
|
||||
// guardarConfiguracion also persists nombresDispositivos
|
||||
test('1.7 guardarConfiguracion persists nombresDispositivos round-trip', () async {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
final servicio = ServicioEcualizador(prefs: prefs);
|
||||
final config = ConfiguracionEcualizador(
|
||||
principal: PresetEcualizador.flat,
|
||||
porEmisora: {},
|
||||
eqMultiDeviceEnabled: true,
|
||||
presetsDispositivo: {},
|
||||
presetsMatriz: {},
|
||||
nombresDispositivos: {'bt_a2dp:AA:BB': 'My Speaker'},
|
||||
);
|
||||
|
||||
await servicio.guardarConfiguracion(config);
|
||||
final restored = await servicio.cargar();
|
||||
|
||||
expect(restored.nombresDispositivos['bt_a2dp:AA:BB'], equals('My Speaker'));
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user