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.
339 lines
12 KiB
Dart
339 lines
12 KiB
Dart
import 'package:flutter_test/flutter_test.dart';
|
|
import 'package:pluriwave/modelos/emisora.dart';
|
|
import 'package:pluriwave/modelos/grupo_favoritos.dart';
|
|
import 'package:pluriwave/modelos/preset_ecualizador.dart';
|
|
import 'package:pluriwave/servicios/servicio_export_import.dart';
|
|
|
|
void main() {
|
|
const servicio = ServicioExportImport();
|
|
|
|
// Shared fixture data used across multiple tests.
|
|
const gruposFavoritosFixture = [
|
|
GrupoFavoritos(id: 'g1', nombre: 'Rock', orden: 1),
|
|
GrupoFavoritos(
|
|
id: GrupoFavoritos.sinAsignarId,
|
|
nombre: 'Sin asignar',
|
|
orden: 0,
|
|
protegido: true,
|
|
),
|
|
];
|
|
const favoritosFixture = [
|
|
Emisora(
|
|
uuid: 'fav-1',
|
|
nombre: 'Radio Uno',
|
|
url: 'https://uno.example/stream',
|
|
grupoFavoritosId: 'g1',
|
|
bitrate: 192,
|
|
),
|
|
];
|
|
const emisorasCustomFixture = [
|
|
Emisora(
|
|
uuid: 'custom-1',
|
|
nombre: 'Mi Radio',
|
|
url: 'https://mia.example/stream',
|
|
),
|
|
];
|
|
const alarmasFixture = {
|
|
'alarmas': [
|
|
{'id': 'a1', 'hora': 7, 'minuto': 30, 'activa': true, 'snoozeMinutos': 5},
|
|
],
|
|
'vacaciones': [
|
|
{'desde': '2026-07-01', 'hasta': '2026-07-15'},
|
|
],
|
|
};
|
|
|
|
group('ServicioExportImport (S4-R4)', () {
|
|
test('v2 round-trip: exportar then importar yields a deep-equal config '
|
|
'(S4-R4-A, S6-R2 test #4)', () {
|
|
final config = servicio.construirExportacion(
|
|
gruposFavoritos: gruposFavoritosFixture,
|
|
favoritos: favoritosFixture,
|
|
emisorasCustom: emisorasCustomFixture,
|
|
presetPrincipal: PresetEcualizador.jazz,
|
|
presetsPorEmisora: {'fav-1': PresetEcualizador.rock},
|
|
// Raw alarm block exactly as ServicioAlarmas persists it (alarms +
|
|
// vacations + exceptions) — must pass through untouched.
|
|
alarmas: alarmasFixture,
|
|
emisoraPreferidaUuid: 'fav-1',
|
|
ordenListas: 'calidad',
|
|
timerSuenoPresetsSegundos: const [300, 600, 1800],
|
|
);
|
|
|
|
final json = servicio.exportar(config);
|
|
final importado = servicio.importar(json);
|
|
|
|
expect(importado, isNotNull);
|
|
expect(importado, equals(config));
|
|
expect(importado!['version'], 2);
|
|
// Alarm raw-JSON passthrough survives the round-trip intact.
|
|
expect(importado['alarmas'], equals(config['alarmas']));
|
|
// The protected "sin asignar" group is never exported.
|
|
final grupos = importado['gruposFavoritos'] as List;
|
|
expect(grupos, hasLength(1));
|
|
expect((grupos.single as Map)['id'], 'g1');
|
|
});
|
|
|
|
test('malformed JSON returns null without throwing (S4-R4)', () {
|
|
expect(servicio.importar('{not valid json'), isNull);
|
|
expect(servicio.importar(''), isNull);
|
|
// Valid JSON but not an object — also rejected gracefully.
|
|
expect(servicio.importar('[1, 2, 3]'), isNull);
|
|
});
|
|
});
|
|
|
|
group('ServicioExportImport v3 — multi-device EQ (Phase 6)', () {
|
|
test(
|
|
'6.1-A: v3 export includes schemaVersion 3 and device/matrix preset keys',
|
|
() {
|
|
final presetDispositivo = PresetEcualizador.rock;
|
|
final presetMatriz = PresetEcualizador.jazz;
|
|
|
|
final config = servicio.construirExportacion(
|
|
gruposFavoritos: gruposFavoritosFixture,
|
|
favoritos: favoritosFixture,
|
|
emisorasCustom: emisorasCustomFixture,
|
|
presetPrincipal: PresetEcualizador.flat,
|
|
presetsPorEmisora: {},
|
|
alarmas: null,
|
|
emisoraPreferidaUuid: null,
|
|
ordenListas: 'calidad',
|
|
timerSuenoPresetsSegundos: const [300],
|
|
presetsPorDispositivo: {
|
|
'bt_a2dp:AA:BB:CC:DD:EE:FF': presetDispositivo,
|
|
},
|
|
presetsMatriz: {'fav-1:bt_a2dp:AA:BB:CC:DD:EE:FF': presetMatriz},
|
|
eqMultiDeviceEnabled: true,
|
|
);
|
|
|
|
// v3 schema version.
|
|
expect(config['version'], 3);
|
|
|
|
// Device preset map present and correct.
|
|
expect(config['presetsPorDispositivo'], isA<Map>());
|
|
final dispositivoMap =
|
|
config['presetsPorDispositivo'] as Map<String, dynamic>;
|
|
expect(dispositivoMap, contains('bt_a2dp:AA:BB:CC:DD:EE:FF'));
|
|
|
|
// Matrix preset map present and correct.
|
|
expect(config['presetsMatriz'], isA<Map>());
|
|
final matrizMap = config['presetsMatriz'] as Map<String, dynamic>;
|
|
expect(matrizMap, contains('fav-1:bt_a2dp:AA:BB:CC:DD:EE:FF'));
|
|
|
|
// Toggle exported.
|
|
expect(config['eqMultiDeviceEnabled'], isTrue);
|
|
},
|
|
);
|
|
|
|
test(
|
|
'6.1-B: v3 round-trip preserves device and matrix presets exactly',
|
|
() {
|
|
final dispositivoPreset = PresetEcualizador.rock;
|
|
final matrizPreset = PresetEcualizador.jazz;
|
|
|
|
final config = servicio.construirExportacion(
|
|
gruposFavoritos: gruposFavoritosFixture,
|
|
favoritos: favoritosFixture,
|
|
emisorasCustom: emisorasCustomFixture,
|
|
presetPrincipal: PresetEcualizador.flat,
|
|
presetsPorEmisora: {'fav-1': PresetEcualizador.pop},
|
|
alarmas: alarmasFixture,
|
|
emisoraPreferidaUuid: 'fav-1',
|
|
ordenListas: 'calidad',
|
|
timerSuenoPresetsSegundos: const [300, 1800],
|
|
presetsPorDispositivo: {
|
|
'bt_a2dp:AA:BB:CC:DD:EE:FF': dispositivoPreset,
|
|
'wired_headset': PresetEcualizador.flat,
|
|
},
|
|
presetsMatriz: {'fav-1:bt_a2dp:AA:BB:CC:DD:EE:FF': matrizPreset},
|
|
eqMultiDeviceEnabled: true,
|
|
);
|
|
|
|
final json = servicio.exportar(config);
|
|
final importado = servicio.importar(json);
|
|
|
|
expect(importado, isNotNull);
|
|
expect(importado!['version'], 3);
|
|
|
|
// Device presets survive the round-trip.
|
|
final deviceMap =
|
|
importado['presetsPorDispositivo'] as Map<String, dynamic>;
|
|
expect(
|
|
deviceMap['bt_a2dp:AA:BB:CC:DD:EE:FF'],
|
|
equals(dispositivoPreset.toJson()),
|
|
);
|
|
expect(
|
|
deviceMap['wired_headset'],
|
|
equals(PresetEcualizador.flat.toJson()),
|
|
);
|
|
|
|
// Matrix presets survive the round-trip.
|
|
final matrizMap = importado['presetsMatriz'] as Map<String, dynamic>;
|
|
expect(
|
|
matrizMap['fav-1:bt_a2dp:AA:BB:CC:DD:EE:FF'],
|
|
equals(matrizPreset.toJson()),
|
|
);
|
|
|
|
// Toggle survives.
|
|
expect(importado['eqMultiDeviceEnabled'], isTrue);
|
|
},
|
|
);
|
|
|
|
test(
|
|
'6.1-C: v2 file (no device fields) imports cleanly — missing keys default to empty maps',
|
|
() {
|
|
// Simulate a v2 export file (no presetsPorDispositivo / presetsMatriz).
|
|
final v2Config = servicio.construirExportacion(
|
|
gruposFavoritos: gruposFavoritosFixture,
|
|
favoritos: favoritosFixture,
|
|
emisorasCustom: emisorasCustomFixture,
|
|
presetPrincipal: PresetEcualizador.jazz,
|
|
presetsPorEmisora: {'fav-1': PresetEcualizador.rock},
|
|
alarmas: null,
|
|
emisoraPreferidaUuid: null,
|
|
ordenListas: 'calidad',
|
|
timerSuenoPresetsSegundos: const [300],
|
|
// No device fields — emulates a v2 export from the old app.
|
|
);
|
|
|
|
// Verify the export has no device keys (a v2 file would not).
|
|
expect(v2Config.containsKey('presetsPorDispositivo'), isFalse);
|
|
expect(v2Config.containsKey('presetsMatriz'), isFalse);
|
|
expect(v2Config.containsKey('eqMultiDeviceEnabled'), isFalse);
|
|
|
|
// Import it — parsing should succeed.
|
|
final json = servicio.exportar(v2Config);
|
|
final importado = servicio.importar(json);
|
|
expect(importado, isNotNull);
|
|
|
|
// Missing device keys should gracefully resolve to null (caller uses ?? {}).
|
|
expect(importado!['presetsPorDispositivo'], isNull);
|
|
expect(importado['presetsMatriz'], isNull);
|
|
expect(importado['eqMultiDeviceEnabled'], isNull);
|
|
},
|
|
);
|
|
|
|
test(
|
|
'6.1-D: unknown schemaVersion (v4-future) does not crash importar()',
|
|
() {
|
|
// Simulate a future v4 export (unknown version, extra fields).
|
|
final futurista = <String, dynamic>{
|
|
'version': 99,
|
|
'presetsPorDispositivo': <String, dynamic>{},
|
|
'presetsMatriz': <String, dynamic>{},
|
|
'unknownFutureField': 'some-value',
|
|
};
|
|
final json = servicio.exportar(futurista);
|
|
// importar() should parse without throwing — version checking is the
|
|
// caller's responsibility (EstadoRadio.importarConfig).
|
|
expect(() => servicio.importar(json), returnsNormally);
|
|
final importado = servicio.importar(json);
|
|
expect(importado, isNotNull);
|
|
expect(importado!['version'], 99);
|
|
},
|
|
);
|
|
});
|
|
|
|
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);
|
|
});
|
|
});
|
|
}
|