fix(devices): cache platform device names, dedupe placeholder ids, purge collided EQ entries
Dart half of bt-device-identity. EstadoEcualizador now caches each device's platform-reported name in memory so the settings screen shows the device's own Bluetooth name instead of its raw id when no custom rename exists, and skips auto-creating preset entries for the composite-placeholder sentinel. Enabling multi-device EQ triggers the Bluetooth permission request through the new channel contract. A flag-guarded one-time migration purges only entries keyed by the exact literal placeholder id from the three per-device preference maps, since those collided entries cannot be attributed to a device. Work unit 2/2 of bt-device-identity (Dart state + migration).
This commit is contained in:
@@ -1133,6 +1133,194 @@ void main() {
|
||||
// WARNING-3: FakeServicioEcualizador preserves nombresDispositivos
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// bt-device-identity Phase 3: platform-name cache + duplicate-entry guard
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
group('EstadoEcualizador — platform-name cache (bt-device-identity Phase 3)', () {
|
||||
const stationUuid = 'station-uuid-bt-identity';
|
||||
|
||||
test('3.1 platform name is cached from a device-change event', () async {
|
||||
final fakeDispositivo = FakeServicioDispositivoAudio();
|
||||
final eq = EstadoEcualizador(
|
||||
audio: FakeServicioAudio(),
|
||||
servicio: FakeServicioEcualizador(eqMultiDeviceEnabled: true),
|
||||
dispositivoAudio: fakeDispositivo,
|
||||
emisoraActualUuid: () => stationUuid,
|
||||
);
|
||||
await eq.cargarPersistido();
|
||||
|
||||
fakeDispositivo.emitirDispositivo(
|
||||
const DispositivoAudio(
|
||||
id: 'bt_a2dp:AA:BB:CC:DD:EE:FF',
|
||||
tipo: TipoDispositivo.bluetoothA2dp,
|
||||
nombre: 'AirPods Pro',
|
||||
),
|
||||
);
|
||||
await Future<void>.delayed(const Duration(milliseconds: 50));
|
||||
|
||||
expect(
|
||||
eq.nombrePlataforma('bt_a2dp:AA:BB:CC:DD:EE:FF'),
|
||||
equals('AirPods Pro'),
|
||||
);
|
||||
eq.dispose();
|
||||
});
|
||||
|
||||
test(
|
||||
'3.4 composite-placeholder sentinel does not create device-list entry',
|
||||
() async {
|
||||
final fakeDispositivo = FakeServicioDispositivoAudio();
|
||||
final eq = EstadoEcualizador(
|
||||
audio: FakeServicioAudio(),
|
||||
servicio: FakeServicioEcualizador(eqMultiDeviceEnabled: true),
|
||||
dispositivoAudio: fakeDispositivo,
|
||||
emisoraActualUuid: () => stationUuid,
|
||||
);
|
||||
await eq.cargarPersistido();
|
||||
|
||||
fakeDispositivo.emitirDispositivo(
|
||||
const DispositivoAudio(
|
||||
id: 'bt_a2dp:name:AirPods-Pro',
|
||||
tipo: TipoDispositivo.bluetoothA2dp,
|
||||
nombre: 'AirPods Pro',
|
||||
),
|
||||
);
|
||||
await Future<void>.delayed(const Duration(milliseconds: 50));
|
||||
|
||||
expect(
|
||||
eq.presetsDispositivo.containsKey('bt_a2dp:name:AirPods-Pro'),
|
||||
isFalse,
|
||||
);
|
||||
expect(
|
||||
eq.nombrePlataforma('bt_a2dp:name:AirPods-Pro'),
|
||||
equals('AirPods Pro'),
|
||||
);
|
||||
eq.dispose();
|
||||
},
|
||||
);
|
||||
|
||||
test('3.6 multiple denied-permission devices do not collide', () async {
|
||||
final fakeDispositivo = FakeServicioDispositivoAudio();
|
||||
final eq = EstadoEcualizador(
|
||||
audio: FakeServicioAudio(),
|
||||
servicio: FakeServicioEcualizador(eqMultiDeviceEnabled: true),
|
||||
dispositivoAudio: fakeDispositivo,
|
||||
emisoraActualUuid: () => stationUuid,
|
||||
);
|
||||
await eq.cargarPersistido();
|
||||
final baseline = eq.presetsDispositivo.length;
|
||||
|
||||
fakeDispositivo.emitirDispositivo(
|
||||
const DispositivoAudio(
|
||||
id: 'bt_a2dp:name:AirPods-Pro',
|
||||
tipo: TipoDispositivo.bluetoothA2dp,
|
||||
nombre: 'AirPods Pro',
|
||||
),
|
||||
);
|
||||
await Future<void>.delayed(const Duration(milliseconds: 50));
|
||||
|
||||
fakeDispositivo.emitirDispositivo(
|
||||
const DispositivoAudio(
|
||||
id: 'bt_a2dp:name:Sony-WH-1000XM5',
|
||||
tipo: TipoDispositivo.bluetoothA2dp,
|
||||
nombre: 'Sony WH-1000XM5',
|
||||
),
|
||||
);
|
||||
await Future<void>.delayed(const Duration(milliseconds: 50));
|
||||
|
||||
expect(
|
||||
eq.nombrePlataforma('bt_a2dp:name:AirPods-Pro'),
|
||||
equals('AirPods Pro'),
|
||||
);
|
||||
expect(
|
||||
eq.nombrePlataforma('bt_a2dp:name:Sony-WH-1000XM5'),
|
||||
equals('Sony WH-1000XM5'),
|
||||
);
|
||||
expect(eq.presetsDispositivo.length, equals(baseline));
|
||||
eq.dispose();
|
||||
});
|
||||
|
||||
test(
|
||||
'3.7 repeated event for known id is a no-op on preset creation '
|
||||
'(regression-lock)',
|
||||
() async {
|
||||
final fakeDispositivo = FakeServicioDispositivoAudio();
|
||||
const device = DispositivoAudio(
|
||||
id: 'bt_a2dp:AA:BB:CC:DD:EE:FF',
|
||||
tipo: TipoDispositivo.bluetoothA2dp,
|
||||
nombre: 'AirPods Pro',
|
||||
);
|
||||
final eq = EstadoEcualizador(
|
||||
audio: FakeServicioAudio(),
|
||||
servicio: FakeServicioEcualizador(eqMultiDeviceEnabled: true),
|
||||
dispositivoAudio: fakeDispositivo,
|
||||
emisoraActualUuid: () => stationUuid,
|
||||
);
|
||||
await eq.cargarPersistido();
|
||||
|
||||
fakeDispositivo.emitirDispositivo(device);
|
||||
await Future<void>.delayed(const Duration(milliseconds: 50));
|
||||
final presetTrasPrimerEvento = eq.presetsDispositivo[device.id];
|
||||
final cantidadTrasPrimerEvento = eq.presetsDispositivo.length;
|
||||
expect(presetTrasPrimerEvento, isNotNull);
|
||||
|
||||
fakeDispositivo.emitirDispositivo(device);
|
||||
await Future<void>.delayed(const Duration(milliseconds: 50));
|
||||
|
||||
expect(
|
||||
eq.presetsDispositivo[device.id],
|
||||
equals(presetTrasPrimerEvento),
|
||||
);
|
||||
expect(eq.presetsDispositivo.length, equals(cantidadTrasPrimerEvento));
|
||||
eq.dispose();
|
||||
},
|
||||
);
|
||||
|
||||
test(
|
||||
'3.8 transient non-BT id during pairing handshake does not corrupt '
|
||||
'the BT entry (regression-lock)',
|
||||
() async {
|
||||
final fakeDispositivo = FakeServicioDispositivoAudio();
|
||||
const btDevice = DispositivoAudio(
|
||||
id: 'bt_a2dp:AA:BB:CC:DD:EE:FF',
|
||||
tipo: TipoDispositivo.bluetoothA2dp,
|
||||
nombre: 'AirPods Pro',
|
||||
);
|
||||
const builtinSpeaker = DispositivoAudio(
|
||||
id: 'builtin_speaker',
|
||||
tipo: TipoDispositivo.altavozInterno,
|
||||
nombre: 'Speaker',
|
||||
);
|
||||
final eq = EstadoEcualizador(
|
||||
audio: FakeServicioAudio(),
|
||||
servicio: FakeServicioEcualizador(
|
||||
principal: PresetEcualizador.rock,
|
||||
eqMultiDeviceEnabled: true,
|
||||
),
|
||||
dispositivoAudio: fakeDispositivo,
|
||||
emisoraActualUuid: () => stationUuid,
|
||||
);
|
||||
await eq.cargarPersistido();
|
||||
|
||||
// Transient builtin_speaker report mid-handshake.
|
||||
fakeDispositivo.emitirDispositivo(builtinSpeaker);
|
||||
await Future<void>.delayed(const Duration(milliseconds: 50));
|
||||
|
||||
// Final BT event once the A2DP profile attaches.
|
||||
fakeDispositivo.emitirDispositivo(btDevice);
|
||||
await Future<void>.delayed(const Duration(milliseconds: 50));
|
||||
|
||||
expect(eq.presetsDispositivo.containsKey(btDevice.id), isTrue);
|
||||
expect(eq.presetsDispositivo.containsKey('builtin_speaker'), isFalse);
|
||||
expect(
|
||||
eq.presetsDispositivo[btDevice.id],
|
||||
equals(PresetEcualizador.rock),
|
||||
);
|
||||
eq.dispose();
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
group('FakeServicioEcualizador — nombresDispositivos preservation (WARNING-3)', () {
|
||||
test(
|
||||
'guardarPrincipal preserves nombresDispositivos',
|
||||
|
||||
@@ -438,6 +438,9 @@ class FakeServicioDispositivoAudioThrows extends ServicioDispositivoAudio {
|
||||
throw Exception('Platform channel error: device unavailable');
|
||||
}
|
||||
|
||||
@override
|
||||
Future<bool> solicitarPermisoBluetooth() async => true;
|
||||
|
||||
@override
|
||||
Future<void> dispose() async {
|
||||
await _controller.close();
|
||||
@@ -449,6 +452,16 @@ class FakeServicioDispositivoAudioThrows extends ServicioDispositivoAudio {
|
||||
/// Use [emitirDispositivo] to push device change events without needing
|
||||
/// a real platform channel.
|
||||
class FakeServicioDispositivoAudio extends ServicioDispositivoAudio {
|
||||
FakeServicioDispositivoAudio({this.permisoBluetoothConcedido = true});
|
||||
|
||||
/// Value returned by [solicitarPermisoBluetooth] (bt-device-identity
|
||||
/// Phase 2/4 testing strategy — defaults to granted so existing tests
|
||||
/// that don't care about the permission flow are unaffected).
|
||||
final bool permisoBluetoothConcedido;
|
||||
|
||||
/// Number of times [solicitarPermisoBluetooth] was called.
|
||||
int solicitarPermisoBluetoothCalls = 0;
|
||||
|
||||
final _controller = StreamController<DispositivoAudio>.broadcast();
|
||||
DispositivoAudio? _dispositivoActual;
|
||||
|
||||
@@ -468,6 +481,12 @@ class FakeServicioDispositivoAudio extends ServicioDispositivoAudio {
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<bool> solicitarPermisoBluetooth() async {
|
||||
solicitarPermisoBluetoothCalls++;
|
||||
return permisoBluetoothConcedido;
|
||||
}
|
||||
|
||||
/// Helper for tests: push a device change event.
|
||||
void emitirDispositivo(DispositivoAudio dispositivo) {
|
||||
_dispositivoActual = dispositivo;
|
||||
|
||||
@@ -64,6 +64,7 @@ void main() {
|
||||
Future<EstadoRadio> crearEstado({
|
||||
bool eqMultiDeviceEnabled = false,
|
||||
Map<String, PresetEcualizador> presetsDispositivo = const {},
|
||||
FakeServicioDispositivoAudio? dispositivoAudio,
|
||||
}) async {
|
||||
final estado = EstadoRadio(
|
||||
audio: FakeServicioAudio(),
|
||||
@@ -76,6 +77,7 @@ void main() {
|
||||
servicioGrabacion: _FakeGrabacion(),
|
||||
resolverArchivoCustom: _archivoCustomVacio,
|
||||
iniciarAutomaticamente: false,
|
||||
dispositivoAudio: dispositivoAudio,
|
||||
);
|
||||
await estado.ecualizador.cargarPersistido();
|
||||
return estado;
|
||||
@@ -99,6 +101,7 @@ void main() {
|
||||
Map<String, PresetEcualizador>? presetsDispositivo,
|
||||
Map<String, String>? nombresDispositivos,
|
||||
String? activeDeviceId,
|
||||
String nombrePlataforma = 'BT Speaker',
|
||||
}) async {
|
||||
final fakeDispositivo = activeDeviceId != null
|
||||
? (FakeServicioDispositivoAudio()
|
||||
@@ -106,7 +109,7 @@ void main() {
|
||||
DispositivoAudio(
|
||||
id: activeDeviceId,
|
||||
tipo: TipoDispositivo.bluetoothA2dp,
|
||||
nombre: 'BT Speaker',
|
||||
nombre: nombrePlataforma,
|
||||
),
|
||||
))
|
||||
: null;
|
||||
@@ -380,6 +383,134 @@ void main() {
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
// ── bt-device-identity Phase 4: display fix + permission trigger ─────────
|
||||
|
||||
group('_SeccionEcualizadorAvanzado — bt-device-identity Phase 4', () {
|
||||
// 4.1 — new behavior: the cached platform name (not '') now feeds
|
||||
// nombreVisible, so a device with no custom rename shows its real name.
|
||||
testWidgets('4.1 platform name displays with no custom rename', (
|
||||
tester,
|
||||
) async {
|
||||
setLargeSurface(tester);
|
||||
_suppressListTileInkAssertion();
|
||||
const deviceId = 'bt_a2dp:AA:BB:CC:DD:EE:FF';
|
||||
final estado = await crearEstadoConNombres(
|
||||
presetsDispositivo: {deviceId: PresetEcualizador.rock},
|
||||
activeDeviceId: deviceId,
|
||||
nombrePlataforma: 'AirPods Pro',
|
||||
);
|
||||
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);
|
||||
|
||||
expect(find.text('AirPods Pro'), findsOneWidget);
|
||||
expect(find.text(deviceId), findsNothing);
|
||||
});
|
||||
|
||||
// 4.4 — triangulation companion: custom rename still wins even though
|
||||
// the row now also has a cached platform name available.
|
||||
testWidgets('4.4 custom rename overrides platform name', (tester) async {
|
||||
setLargeSurface(tester);
|
||||
_suppressListTileInkAssertion();
|
||||
const deviceId = 'bt_a2dp:AA:BB:CC:DD:EE:FF';
|
||||
final estado = await crearEstadoConNombres(
|
||||
presetsDispositivo: {deviceId: PresetEcualizador.rock},
|
||||
activeDeviceId: deviceId,
|
||||
nombrePlataforma: 'AirPods Pro',
|
||||
nombresDispositivos: {deviceId: 'My Headphones'},
|
||||
);
|
||||
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);
|
||||
|
||||
expect(find.text('My Headphones'), findsOneWidget);
|
||||
expect(find.text('AirPods Pro'), findsNothing);
|
||||
});
|
||||
|
||||
// 4.5 — approval test (regression-lock): a device never seen on the
|
||||
// stream has no cached platform name, so legacy raw-id fallback holds.
|
||||
testWidgets('4.5 no platform name yet falls back to raw id', (
|
||||
tester,
|
||||
) async {
|
||||
setLargeSurface(tester);
|
||||
_suppressListTileInkAssertion();
|
||||
const deviceId = 'bt_a2dp:AA:BB:CC:DD:EE:FF';
|
||||
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);
|
||||
|
||||
expect(find.text(deviceId), findsOneWidget);
|
||||
});
|
||||
|
||||
// 4.6 — permission trigger point: turning the toggle ON requests
|
||||
// BLUETOOTH_CONNECT; turning it back OFF must not re-fire the request.
|
||||
testWidgets('4.6 permission call fires on device-management open', (
|
||||
tester,
|
||||
) async {
|
||||
setLargeSurface(tester);
|
||||
_suppressListTileInkAssertion();
|
||||
final fakeDispositivo = FakeServicioDispositivoAudio();
|
||||
final estado = await crearEstado(
|
||||
eqMultiDeviceEnabled: false,
|
||||
dispositivoAudio: fakeDispositivo,
|
||||
);
|
||||
addTearDown(estado.dispose);
|
||||
|
||||
await tester.pumpWidget(buildAjustes(estado));
|
||||
await pumpStable(tester);
|
||||
|
||||
await tester.scrollUntilVisible(
|
||||
find.text('Advanced Equalization Options'),
|
||||
300,
|
||||
scrollable: find.byType(Scrollable).first,
|
||||
);
|
||||
await pumpStable(tester);
|
||||
|
||||
expect(fakeDispositivo.solicitarPermisoBluetoothCalls, equals(0));
|
||||
|
||||
// Toggle ON: permission requested exactly once.
|
||||
await tester.tap(find.byType(Switch).last);
|
||||
await pumpStable(tester);
|
||||
|
||||
expect(estado.ecualizador.eqMultiDeviceEnabled, isTrue);
|
||||
expect(fakeDispositivo.solicitarPermisoBluetoothCalls, equals(1));
|
||||
|
||||
// Toggle OFF again: no further permission request.
|
||||
await tester.tap(find.byType(Switch).last);
|
||||
await pumpStable(tester);
|
||||
|
||||
expect(estado.ecualizador.eqMultiDeviceEnabled, isFalse);
|
||||
expect(fakeDispositivo.solicitarPermisoBluetoothCalls, equals(1));
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// ── Infrastructure ──────────────────────────────────────────────────────────
|
||||
|
||||
@@ -37,6 +37,16 @@ void main() {
|
||||
});
|
||||
}
|
||||
|
||||
void stubRequestBluetoothConnect(bool? response) {
|
||||
TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger
|
||||
.setMockMethodCallHandler(const MethodChannel(methodChannelName), (
|
||||
call,
|
||||
) async {
|
||||
if (call.method == 'requestBluetoothConnect') return response;
|
||||
return null;
|
||||
});
|
||||
}
|
||||
|
||||
test('obtenerDispositivoActual maps builtin_speaker (type=2)', () async {
|
||||
stubGetActiveDevice({
|
||||
'id': 'builtin_speaker',
|
||||
@@ -105,5 +115,36 @@ void main() {
|
||||
test('onDispositivoCambiado is a broadcast stream', () {
|
||||
expect(servicio.onDispositivoCambiado.isBroadcast, isTrue);
|
||||
});
|
||||
|
||||
// ── bt-device-identity Phase 2: permission contract ──────────────────────
|
||||
|
||||
test(
|
||||
'solicitarPermisoBluetooth invokes requestBluetoothConnect and '
|
||||
'returns true when granted',
|
||||
() async {
|
||||
stubRequestBluetoothConnect(true);
|
||||
final granted = await servicio.solicitarPermisoBluetooth();
|
||||
expect(granted, isTrue);
|
||||
},
|
||||
);
|
||||
|
||||
test(
|
||||
'solicitarPermisoBluetooth returns false when the platform denies',
|
||||
() async {
|
||||
stubRequestBluetoothConnect(false);
|
||||
final granted = await servicio.solicitarPermisoBluetooth();
|
||||
expect(granted, isFalse);
|
||||
},
|
||||
);
|
||||
|
||||
test(
|
||||
'solicitarPermisoBluetooth returns false when the platform channel '
|
||||
'returns null',
|
||||
() async {
|
||||
stubRequestBluetoothConnect(null);
|
||||
final granted = await servicio.solicitarPermisoBluetooth();
|
||||
expect(granted, isFalse);
|
||||
},
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -78,5 +78,17 @@ void main() {
|
||||
test('dispose closes the stream without error', () async {
|
||||
await expectLater(fake.dispose(), completes);
|
||||
});
|
||||
|
||||
// bt-device-identity Task 2.5: interface-completeness — every concrete
|
||||
// ServicioDispositivoAudio must implement solicitarPermisoBluetooth.
|
||||
test(
|
||||
'solicitarPermisoBluetooth increments the call counter and returns '
|
||||
'the configured value',
|
||||
() async {
|
||||
final granted = await fake.solicitarPermisoBluetooth();
|
||||
expect(granted, isTrue);
|
||||
expect(fake.solicitarPermisoBluetoothCalls, equals(1));
|
||||
},
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -24,6 +24,9 @@ class NullServicioDispositivoAudio extends ServicioDispositivoAudio {
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<bool> solicitarPermisoBluetooth() async => true;
|
||||
|
||||
@override
|
||||
Future<void> dispose() async {}
|
||||
}
|
||||
|
||||
@@ -229,4 +229,181 @@ void main() {
|
||||
expect(restored.nombresDispositivos['bt_a2dp:AA:BB'], equals('My Speaker'));
|
||||
});
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// bt-device-identity Phase 5: one-time guarded migration purge
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
group('ServicioEcualizador — placeholder migration purge (bt-device-identity Phase 5)', () {
|
||||
const placeholderKey = 'bt_a2dp:02:00:00:00:00:00';
|
||||
const stableKey = 'bt_a2dp:AA:BB:CC:DD:EE:FF';
|
||||
|
||||
test(
|
||||
'5.1 migration removes only exact placeholder entries from presetsPorDispositivo',
|
||||
() async {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
final servicio = ServicioEcualizador(prefs: prefs);
|
||||
await servicio.guardarPresetDispositivo(
|
||||
placeholderKey,
|
||||
PresetEcualizador.jazz,
|
||||
);
|
||||
await servicio.guardarPresetDispositivo(
|
||||
stableKey,
|
||||
PresetEcualizador.rock,
|
||||
);
|
||||
|
||||
await servicio.migrarClavesPlaceholder();
|
||||
final config = await servicio.cargar();
|
||||
|
||||
expect(
|
||||
config.presetsDispositivo.containsKey(placeholderKey),
|
||||
isFalse,
|
||||
);
|
||||
expect(
|
||||
config.presetsDispositivo[stableKey],
|
||||
equals(PresetEcualizador.rock),
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
test('5.2 matrix keys purge only the placeholder segment', () async {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
final servicio = ServicioEcualizador(prefs: prefs);
|
||||
await servicio.guardarPresetMatriz(
|
||||
'station1:$placeholderKey',
|
||||
PresetEcualizador.jazz,
|
||||
);
|
||||
await servicio.guardarPresetMatriz(
|
||||
'station1:$stableKey',
|
||||
PresetEcualizador.rock,
|
||||
);
|
||||
|
||||
await servicio.migrarClavesPlaceholder();
|
||||
final config = await servicio.cargar();
|
||||
|
||||
expect(
|
||||
config.presetsMatriz.containsKey('station1:$placeholderKey'),
|
||||
isFalse,
|
||||
);
|
||||
expect(
|
||||
config.presetsMatriz['station1:$stableKey'],
|
||||
equals(PresetEcualizador.rock),
|
||||
);
|
||||
});
|
||||
|
||||
test('5.3 near-miss keys are preserved (differs by one digit)', () async {
|
||||
const nearMissKey = 'bt_a2dp:02:00:00:00:00:01';
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
final servicio = ServicioEcualizador(prefs: prefs);
|
||||
await servicio.guardarPresetDispositivo(
|
||||
nearMissKey,
|
||||
PresetEcualizador.jazz,
|
||||
);
|
||||
|
||||
await servicio.migrarClavesPlaceholder();
|
||||
final config = await servicio.cargar();
|
||||
|
||||
expect(
|
||||
config.presetsDispositivo[nearMissKey],
|
||||
equals(PresetEcualizador.jazz),
|
||||
);
|
||||
});
|
||||
|
||||
test('5.4 migration runs once (second run is a true no-op)', () async {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
final servicio = ServicioEcualizador(prefs: prefs);
|
||||
await servicio.guardarPresetDispositivo(
|
||||
placeholderKey,
|
||||
PresetEcualizador.jazz,
|
||||
);
|
||||
await servicio.guardarPresetDispositivo(
|
||||
stableKey,
|
||||
PresetEcualizador.rock,
|
||||
);
|
||||
|
||||
await servicio.migrarClavesPlaceholder();
|
||||
|
||||
// Re-seed the placeholder key directly, bypassing the flag, to prove
|
||||
// the SECOND migration call is a true no-op (flag short-circuits)
|
||||
// rather than merely finding nothing left to purge.
|
||||
await servicio.guardarPresetDispositivo(
|
||||
placeholderKey,
|
||||
PresetEcualizador.bassBoost,
|
||||
);
|
||||
await servicio.migrarClavesPlaceholder();
|
||||
|
||||
final config = await servicio.cargar();
|
||||
|
||||
expect(
|
||||
config.presetsDispositivo[placeholderKey],
|
||||
equals(PresetEcualizador.bassBoost),
|
||||
);
|
||||
expect(
|
||||
config.presetsDispositivo[stableKey],
|
||||
equals(PresetEcualizador.rock),
|
||||
);
|
||||
});
|
||||
|
||||
test(
|
||||
'5.5 no placeholder entries when BLUETOOTH_CONNECT was never '
|
||||
'requested is a no-op',
|
||||
() async {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
final servicio = ServicioEcualizador(prefs: prefs);
|
||||
await servicio.guardarPresetDispositivo(
|
||||
stableKey,
|
||||
PresetEcualizador.rock,
|
||||
);
|
||||
|
||||
await servicio.migrarClavesPlaceholder();
|
||||
final config = await servicio.cargar();
|
||||
|
||||
expect(
|
||||
config.presetsDispositivo[stableKey],
|
||||
equals(PresetEcualizador.rock),
|
||||
);
|
||||
expect(config.presetsDispositivo.length, equals(1));
|
||||
},
|
||||
);
|
||||
|
||||
test('5.6 nombresDispositivos also purges the exact placeholder key', () async {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
final servicio = ServicioEcualizador(prefs: prefs);
|
||||
await servicio.guardarNombresDispositivos({
|
||||
placeholderKey: 'Ghost Device',
|
||||
stableKey: 'Living Room BT',
|
||||
});
|
||||
|
||||
await servicio.migrarClavesPlaceholder();
|
||||
final config = await servicio.cargar();
|
||||
|
||||
expect(config.nombresDispositivos.containsKey(placeholderKey), isFalse);
|
||||
expect(
|
||||
config.nombresDispositivos[stableKey],
|
||||
equals('Living Room BT'),
|
||||
);
|
||||
});
|
||||
|
||||
test('5.9 cargar() runs the migration automatically on first load', () async {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
final servicio = ServicioEcualizador(prefs: prefs);
|
||||
await servicio.guardarPresetDispositivo(
|
||||
placeholderKey,
|
||||
PresetEcualizador.jazz,
|
||||
);
|
||||
await servicio.guardarPresetDispositivo(
|
||||
stableKey,
|
||||
PresetEcualizador.rock,
|
||||
);
|
||||
|
||||
// No explicit migrarClavesPlaceholder() call: cargar() must run it.
|
||||
final config = await servicio.cargar();
|
||||
|
||||
expect(config.presetsDispositivo.containsKey(placeholderKey), isFalse);
|
||||
expect(
|
||||
config.presetsDispositivo[stableKey],
|
||||
equals(PresetEcualizador.rock),
|
||||
);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user