feat(ecualizador): include equalizer on/off toggle in export/import
The backup envelope carried favorites, EQ presets, alarms and the multi-device toggle but not EstadoEcualizador's own on/off flag, so restoring a backup on another device silently kept that device's existing toggle state instead of the source device's. Bumps the backup schema to v4 (additive over v3): the flag is only written when explicitly provided, so old exports stay at v2/v3. Importing an old backup without the field leaves the current toggle untouched rather than defaulting it. Applying the imported value reuses EstadoEcualizador.cambiarActivo so it persists and pushes to the live audio engine exactly like a manual toggle.
This commit is contained in:
@@ -1569,6 +1569,83 @@ void main() {
|
||||
});
|
||||
});
|
||||
|
||||
group(
|
||||
'EstadoEcualizador — importarConfiguracion(activo:) '
|
||||
'(equalizer on/off export/import gap)',
|
||||
() {
|
||||
test(
|
||||
'activo: false turns the equalizer off — persisted AND pushed to '
|
||||
'the live audio engine (reuses cambiarActivo, not a bare field set)',
|
||||
() async {
|
||||
final servicio = FakeServicioEcualizador(activo: true);
|
||||
final audio = FakeServicioAudio();
|
||||
final eq = EstadoEcualizador(audio: audio, servicio: servicio);
|
||||
await eq.cargarPersistido();
|
||||
audio.cambiosEcualizadorActivo.clear();
|
||||
|
||||
await eq.importarConfiguracion(
|
||||
principal: PresetEcualizador.flat,
|
||||
porEmisora: {},
|
||||
activo: false,
|
||||
);
|
||||
|
||||
expect(eq.activo, isFalse);
|
||||
expect(audio.cambiosEcualizadorActivo, contains(false));
|
||||
expect(servicio.config.activo, isFalse);
|
||||
expect(servicio.guardarActivoLlamadas, 1);
|
||||
eq.dispose();
|
||||
},
|
||||
);
|
||||
|
||||
test(
|
||||
'activo: true turns the equalizer on — persisted AND pushed to '
|
||||
'the live audio engine',
|
||||
() async {
|
||||
final servicio = FakeServicioEcualizador(activo: false);
|
||||
final audio = FakeServicioAudio();
|
||||
final eq = EstadoEcualizador(audio: audio, servicio: servicio);
|
||||
await eq.cargarPersistido();
|
||||
audio.cambiosEcualizadorActivo.clear();
|
||||
|
||||
await eq.importarConfiguracion(
|
||||
principal: PresetEcualizador.flat,
|
||||
porEmisora: {},
|
||||
activo: true,
|
||||
);
|
||||
|
||||
expect(eq.activo, isTrue);
|
||||
expect(audio.cambiosEcualizadorActivo, contains(true));
|
||||
expect(servicio.config.activo, isTrue);
|
||||
expect(servicio.guardarActivoLlamadas, 1);
|
||||
eq.dispose();
|
||||
},
|
||||
);
|
||||
|
||||
test(
|
||||
'activo: null (old backup, no field) leaves the current toggle '
|
||||
'untouched and does not persist anything for it',
|
||||
() async {
|
||||
final servicio = FakeServicioEcualizador(activo: false);
|
||||
final audio = FakeServicioAudio();
|
||||
final eq = EstadoEcualizador(audio: audio, servicio: servicio);
|
||||
await eq.cargarPersistido();
|
||||
audio.cambiosEcualizadorActivo.clear();
|
||||
|
||||
await eq.importarConfiguracion(
|
||||
principal: PresetEcualizador.flat,
|
||||
porEmisora: {},
|
||||
// activo omitted — simulates a pre-v4 backup.
|
||||
);
|
||||
|
||||
expect(eq.activo, isFalse);
|
||||
expect(audio.cambiosEcualizadorActivo, isEmpty);
|
||||
expect(servicio.guardarActivoLlamadas, 0);
|
||||
eq.dispose();
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
group('EstadoEcualizador — bonded Bluetooth names', () {
|
||||
const deviceId = 'bt_a2dp:AA:BB:CC:DD:EE:FF';
|
||||
|
||||
|
||||
@@ -0,0 +1,171 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:pluriwave/estado/estado_radio.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
|
||||
import '../helpers/fakes.dart';
|
||||
import '../helpers/fakes_alarmas.dart';
|
||||
|
||||
/// Closes the last remaining export/import gap: `EstadoEcualizador._activo`
|
||||
/// (the equalizer's global ON/OFF toggle) was not part of the backup
|
||||
/// envelope at all, so restoring a backup on another device silently kept
|
||||
/// whatever that device's toggle happened to be. These tests exercise the
|
||||
/// flag end to end through `EstadoRadio.exportarConfig`/`importarConfig`,
|
||||
/// the real call sites `pantalla_ajustes_backup.dart` uses.
|
||||
void main() {
|
||||
TestWidgetsFlutterBinding.ensureInitialized();
|
||||
|
||||
late Directory tempDir;
|
||||
|
||||
setUp(() async {
|
||||
SharedPreferences.setMockInitialValues({});
|
||||
// A PRIVATE per-test file, never the shared `test/fixtures/` one — see
|
||||
// `estado_alarmas_import_test.dart` for why: `importarConfig`
|
||||
// unconditionally writes to whatever `resolverArchivoCustom` resolves to.
|
||||
tempDir = await Directory.systemTemp.createTemp(
|
||||
'pluriwave_eq_activo_export_test',
|
||||
);
|
||||
});
|
||||
|
||||
tearDown(() async {
|
||||
if (tempDir.existsSync()) {
|
||||
await tempDir.delete(recursive: true);
|
||||
}
|
||||
});
|
||||
|
||||
Future<EstadoRadio> crearRadio({bool ecualizadorActivo = true}) async {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
final archivoCustom = File('${tempDir.path}/emisoras_custom.json');
|
||||
if (!archivoCustom.existsSync()) {
|
||||
await archivoCustom.writeAsString('[]');
|
||||
}
|
||||
final radio = EstadoRadio(
|
||||
esPremium: () => true,
|
||||
audio: FakeServicioAudio(),
|
||||
favoritos: FakeServicioFavoritos(),
|
||||
radio: FakeServicioRadio(),
|
||||
servicioEcualizador: FakeServicioEcualizador(activo: ecualizadorActivo),
|
||||
servicioGrabacion: FakeServicioGrabacionRadioInactiva(),
|
||||
resolverArchivoCustom: () async => archivoCustom,
|
||||
prefs: prefs,
|
||||
iniciarAutomaticamente: false,
|
||||
);
|
||||
await radio.ecualizador.cargarPersistido();
|
||||
return radio;
|
||||
}
|
||||
|
||||
Map<String, dynamic> backupBase({
|
||||
required int version,
|
||||
bool? ecualizadorActivo,
|
||||
}) {
|
||||
final data = <String, dynamic>{
|
||||
'version': version,
|
||||
'gruposFavoritos': [],
|
||||
'favoritos': [],
|
||||
'emisorasCustom': [],
|
||||
'presetsEcualizador': {},
|
||||
'alarmas': null,
|
||||
'emisoraPreferidaUuid': null,
|
||||
'ordenListas': 'nombre',
|
||||
'timerSuenoPresetsSegundos': <int>[300, 600],
|
||||
};
|
||||
if (ecualizadorActivo != null) {
|
||||
data['ecualizadorActivo'] = ecualizadorActivo;
|
||||
}
|
||||
return data;
|
||||
}
|
||||
|
||||
group('EstadoRadio export/import — equalizer on/off toggle (v4)', () {
|
||||
test('exportarConfig includes the flag when the equalizer is ON', () async {
|
||||
final radio = await crearRadio(ecualizadorActivo: true);
|
||||
addTearDown(radio.dispose);
|
||||
|
||||
final exportado = await radio.exportarConfig();
|
||||
|
||||
expect(exportado['ecualizadorActivo'], isTrue);
|
||||
expect(exportado['version'], 4);
|
||||
});
|
||||
|
||||
test(
|
||||
'exportarConfig includes the flag when the equalizer is OFF',
|
||||
() async {
|
||||
final radio = await crearRadio(ecualizadorActivo: false);
|
||||
addTearDown(radio.dispose);
|
||||
|
||||
final exportado = await radio.exportarConfig();
|
||||
|
||||
expect(exportado['ecualizadorActivo'], isFalse);
|
||||
expect(exportado['version'], 4);
|
||||
},
|
||||
);
|
||||
|
||||
test('importarConfig(activo: false) turns the equalizer off — persisted '
|
||||
'and reflected in EstadoEcualizador.activo', () async {
|
||||
final radio = await crearRadio(ecualizadorActivo: true);
|
||||
addTearDown(radio.dispose);
|
||||
expect(radio.ecualizador.activo, isTrue);
|
||||
|
||||
await radio.importarConfig(
|
||||
backupBase(version: 4, ecualizadorActivo: false),
|
||||
);
|
||||
|
||||
expect(radio.ecualizador.activo, isFalse);
|
||||
});
|
||||
|
||||
test('importarConfig(activo: true) turns the equalizer on — persisted '
|
||||
'and reflected in EstadoEcualizador.activo', () async {
|
||||
final radio = await crearRadio(ecualizadorActivo: false);
|
||||
addTearDown(radio.dispose);
|
||||
expect(radio.ecualizador.activo, isFalse);
|
||||
|
||||
await radio.importarConfig(
|
||||
backupBase(version: 4, ecualizadorActivo: true),
|
||||
);
|
||||
|
||||
expect(radio.ecualizador.activo, isTrue);
|
||||
});
|
||||
|
||||
test('importing an OLD backup (no ecualizadorActivo field) does not throw '
|
||||
'and leaves the current toggle untouched', () async {
|
||||
final radio = await crearRadio(ecualizadorActivo: false);
|
||||
addTearDown(radio.dispose);
|
||||
expect(radio.ecualizador.activo, isFalse);
|
||||
|
||||
await radio.importarConfig(backupBase(version: 2));
|
||||
|
||||
expect(radio.ecualizador.activo, isFalse);
|
||||
});
|
||||
|
||||
test(
|
||||
'full round-trip: export -> import restores the equalizer toggle',
|
||||
() async {
|
||||
final origen = await crearRadio(ecualizadorActivo: false);
|
||||
addTearDown(origen.dispose);
|
||||
final exportado = await origen.exportarConfig();
|
||||
|
||||
final destino = await crearRadio(ecualizadorActivo: true);
|
||||
addTearDown(destino.dispose);
|
||||
expect(destino.ecualizador.activo, isTrue);
|
||||
|
||||
await destino.importarConfig(exportado);
|
||||
|
||||
expect(destino.ecualizador.activo, isFalse);
|
||||
},
|
||||
);
|
||||
|
||||
test('regression: other fields still round-trip exactly as before '
|
||||
'(ordenListas, timerSuenoPresetsSegundos)', () async {
|
||||
final origen = await crearRadio();
|
||||
addTearDown(origen.dispose);
|
||||
await origen.guardarTimerSuenoPresetsSegundos([120, 900]);
|
||||
final exportado = await origen.exportarConfig();
|
||||
|
||||
final destino = await crearRadio();
|
||||
addTearDown(destino.dispose);
|
||||
await destino.importarConfig(exportado);
|
||||
|
||||
expect(destino.ordenListas.name, origen.ordenListas.name);
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -414,6 +414,17 @@ class FakeServicioEcualizador extends ServicioEcualizador {
|
||||
);
|
||||
}
|
||||
|
||||
/// Overridden like every other write below: without this, calls fall
|
||||
/// through to the real `ServicioEcualizador.guardarConfiguracion`, which
|
||||
/// hits real SharedPreferences and needs a Flutter test binding — a
|
||||
/// footgun for any test that reaches `EstadoEcualizador.importarConfiguracion`
|
||||
/// (the backup-import path) through this Fake without also wiring
|
||||
/// `TestWidgetsFlutterBinding`/mock prefs.
|
||||
@override
|
||||
Future<void> guardarConfiguracion(ConfiguracionEcualizador config) async {
|
||||
_config = config;
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> guardarPorEmisora(String uuid, PresetEcualizador preset) async {
|
||||
final mapa = Map<String, PresetEcualizador>.from(_config.porEmisora);
|
||||
|
||||
@@ -232,4 +232,107 @@ void main() {
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
group('ServicioExportImport v4 — equalizer on/off toggle', () {
|
||||
test('v4 export includes ecualizadorActivo=true and schemaVersion 4', () {
|
||||
final config = servicio.construirExportacion(
|
||||
gruposFavoritos: gruposFavoritosFixture,
|
||||
favoritos: favoritosFixture,
|
||||
emisorasCustom: emisorasCustomFixture,
|
||||
presetPrincipal: PresetEcualizador.flat,
|
||||
presetsPorEmisora: {},
|
||||
alarmas: null,
|
||||
emisoraPreferidaUuid: null,
|
||||
ordenListas: 'calidad',
|
||||
timerSuenoPresetsSegundos: const [300],
|
||||
eqMultiDeviceEnabled: false,
|
||||
ecualizadorActivo: true,
|
||||
);
|
||||
|
||||
expect(config['version'], 4);
|
||||
expect(config['ecualizadorActivo'], isTrue);
|
||||
});
|
||||
|
||||
test('v4 export includes ecualizadorActivo=false and schemaVersion 4', () {
|
||||
final config = servicio.construirExportacion(
|
||||
gruposFavoritos: gruposFavoritosFixture,
|
||||
favoritos: favoritosFixture,
|
||||
emisorasCustom: emisorasCustomFixture,
|
||||
presetPrincipal: PresetEcualizador.flat,
|
||||
presetsPorEmisora: {},
|
||||
alarmas: null,
|
||||
emisoraPreferidaUuid: null,
|
||||
ordenListas: 'calidad',
|
||||
timerSuenoPresetsSegundos: const [300],
|
||||
eqMultiDeviceEnabled: false,
|
||||
ecualizadorActivo: false,
|
||||
);
|
||||
|
||||
expect(config['version'], 4);
|
||||
expect(config['ecualizadorActivo'], isFalse);
|
||||
});
|
||||
|
||||
test('v4 round-trip preserves the equalizer toggle exactly', () {
|
||||
final config = servicio.construirExportacion(
|
||||
gruposFavoritos: gruposFavoritosFixture,
|
||||
favoritos: favoritosFixture,
|
||||
emisorasCustom: emisorasCustomFixture,
|
||||
presetPrincipal: PresetEcualizador.jazz,
|
||||
presetsPorEmisora: {'fav-1': PresetEcualizador.rock},
|
||||
alarmas: alarmasFixture,
|
||||
emisoraPreferidaUuid: 'fav-1',
|
||||
ordenListas: 'calidad',
|
||||
timerSuenoPresetsSegundos: const [300, 1800],
|
||||
eqMultiDeviceEnabled: true,
|
||||
ecualizadorActivo: false,
|
||||
);
|
||||
|
||||
final json = servicio.exportar(config);
|
||||
final importado = servicio.importar(json);
|
||||
|
||||
expect(importado, isNotNull);
|
||||
expect(importado!['version'], 4);
|
||||
expect(importado['ecualizadorActivo'], isFalse);
|
||||
});
|
||||
|
||||
test('omitting ecualizadorActivo keeps the export at v3 (backward compat) '
|
||||
'and no key is written', () {
|
||||
final config = servicio.construirExportacion(
|
||||
gruposFavoritos: gruposFavoritosFixture,
|
||||
favoritos: favoritosFixture,
|
||||
emisorasCustom: emisorasCustomFixture,
|
||||
presetPrincipal: PresetEcualizador.flat,
|
||||
presetsPorEmisora: {},
|
||||
alarmas: null,
|
||||
emisoraPreferidaUuid: null,
|
||||
ordenListas: 'calidad',
|
||||
timerSuenoPresetsSegundos: const [300],
|
||||
eqMultiDeviceEnabled: true,
|
||||
// ecualizadorActivo omitted — emulates a v3 export.
|
||||
);
|
||||
|
||||
expect(config['version'], 3);
|
||||
expect(config.containsKey('ecualizadorActivo'), isFalse);
|
||||
});
|
||||
|
||||
test('omitting every v3/v4 extension keeps the export at v2 (backward '
|
||||
'compat)', () {
|
||||
final config = servicio.construirExportacion(
|
||||
gruposFavoritos: gruposFavoritosFixture,
|
||||
favoritos: favoritosFixture,
|
||||
emisorasCustom: emisorasCustomFixture,
|
||||
presetPrincipal: PresetEcualizador.flat,
|
||||
presetsPorEmisora: {},
|
||||
alarmas: null,
|
||||
emisoraPreferidaUuid: null,
|
||||
ordenListas: 'calidad',
|
||||
timerSuenoPresetsSegundos: const [300],
|
||||
// No v3/v4 extensions at all.
|
||||
);
|
||||
|
||||
expect(config['version'], 2);
|
||||
expect(config.containsKey('ecualizadorActivo'), isFalse);
|
||||
expect(config.containsKey('eqMultiDeviceEnabled'), isFalse);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user