feat: restaurar los grupos de favoritos al importar y recordar la lista del coche
Dos fallos reportados desde el uso real en el coche. Los grupos de favoritos no volvian al importar La exportacion nunca estuvo rota: los grupos viajaban desde siempre y la asignacion de cada emisora va dentro de cada favorito como grupo_id. El fallo estaba solo al importar. La restauracion reutilizaba ServicioFavoritos.agregar, que fuerza "sin asignar" a proposito, porque una emisora recien marcada como favorita no tiene grupo. Correcto para esa ruta, destructivo como primitiva de restauracion: los grupos volvian vacios y todo aterrizaba en Sin asignar. Se separa la ruta de restaurar, que respeta el grupo_id del backup y su orden. Ningun test lo detectaba porque el doble de pruebas era infiel: el fake conservaba el grupo que la implementacion real destruia, y no tenia restaurarGrupo, asi que esa ruta estaba sin cubrir. Los tests existentes lo esquivaban pasando siempre una lista de grupos vacia. Se corrige el doble. No hace falta subir la version del formato: el envoltorio ya llevaba todo. El coche perdia la lista al reconectar El contexto de reproduccion vivia solo en memoria, y el motor que arranca Android Auto es un proceso nuevo sin interfaz ni EstadoRadio, asi que al reconectar se reproducia la ultima emisora sin saber a que lista pertenecia y siguiente/anterior no hacian nada hasta entrar a favoritos a mano. Ahora se persiste el TIPO de contexto y, cuando aplica, el id del grupo, y se resuelve contra las listas vivas en cada salto: si el grupo cambia de contenido entre sesiones, el coche ve lo actual y no una foto vieja. Cadena de repliegue, decidida por el propietario: grupo vivo con la emisora dentro -> se recorre el grupo grupo vivo sin la emisora -> se permanece en el grupo, primera grupo vivo pero vacio -> se ensancha a favoritos grupo borrado -> favoritos sin favoritos -> comportamiento actual Honrar un grupo de una sola emisora exigia levantar dos barreras, no una: el resolutor y el propio _saltarEmisora, que se negaba a nombrar un grupo con menos de dos miembros. emisoraVecina queda intacta: su contrato de no saltar a ciegas es deliberado y se usa desde mas sitios, asi que el caso de "la emisora se salio del grupo" se trata en el flujo del salto. La puerta de entitlement no cambia: un conductor sin premium sigue recorriendo solo el conjunto gratuito, y un contexto congelado mientras pagaba se descarta en vez de recorrerse. Suite completa: 1501 pasan, 2 omitidos. flutter analyze mantiene los 5 avisos preexistentes.
This commit is contained in:
@@ -0,0 +1,177 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:pluriwave/estado/estado_radio.dart';
|
||||
import 'package:pluriwave/modelos/emisora.dart';
|
||||
import 'package:pluriwave/modelos/grupo_favoritos.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
|
||||
import '../helpers/fakes.dart';
|
||||
import '../helpers/fakes_alarmas.dart';
|
||||
|
||||
/// Copia de seguridad — ROUND TRIP de los grupos de favoritos y de la
|
||||
/// asignación emisora -> grupo.
|
||||
///
|
||||
/// Reportado desde el uso real: al restaurar una copia en otro dispositivo
|
||||
/// los grupos volvían VACÍOS y todas las emisoras aparecían en «Sin
|
||||
/// asignar». El sobre exportado siempre llevó ambas cosas (`gruposFavoritos`
|
||||
/// desde v2, y `grupo_id` dentro de cada entrada de `favoritos`, porque es
|
||||
/// una clave intrínseca de `Emisora.toMap()`); lo que fallaba era la
|
||||
/// APLICACIÓN del estado: `importarConfig` reusaba `ServicioFavoritos.agregar`,
|
||||
/// la primitiva de «marcar como favorita», que fuerza `sin_asignar` y un
|
||||
/// `orden` nuevo a propósito.
|
||||
///
|
||||
/// Por eso estos tests prueban el VIAJE COMPLETO (origen -> exportar ->
|
||||
/// destino limpio -> importar), no la forma del sobre: la forma ya estaba
|
||||
/// bien y aun así el usuario perdía sus grupos.
|
||||
void main() {
|
||||
TestWidgetsFlutterBinding.ensureInitialized();
|
||||
|
||||
late Directory tempDir;
|
||||
|
||||
setUp(() async {
|
||||
SharedPreferences.setMockInitialValues({});
|
||||
tempDir = await Directory.systemTemp.createTemp(
|
||||
'pluriwave_export_grupos_test',
|
||||
);
|
||||
});
|
||||
|
||||
tearDown(() async {
|
||||
if (tempDir.existsSync()) {
|
||||
await tempDir.delete(recursive: true);
|
||||
}
|
||||
});
|
||||
|
||||
var contadorArchivos = 0;
|
||||
|
||||
Future<EstadoRadio> crearRadio() async {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
// Un archivo POR instancia: `importarConfig` escribe siempre en el que
|
||||
// resuelva `resolverArchivoCustom`, y origen y destino no pueden
|
||||
// compartirlo.
|
||||
final archivoCustom = File(
|
||||
'${tempDir.path}/emisoras_custom_${contadorArchivos++}.json',
|
||||
);
|
||||
if (!archivoCustom.existsSync()) {
|
||||
await archivoCustom.writeAsString('[]');
|
||||
}
|
||||
final radio = EstadoRadio(
|
||||
esPremium: () => true,
|
||||
audio: FakeServicioAudio(),
|
||||
favoritos: FakeServicioFavoritos(),
|
||||
radio: FakeServicioRadio(),
|
||||
servicioEcualizador: FakeServicioEcualizador(),
|
||||
servicioGrabacion: FakeServicioGrabacionRadioInactiva(),
|
||||
resolverArchivoCustom: () async => archivoCustom,
|
||||
prefs: prefs,
|
||||
iniciarAutomaticamente: false,
|
||||
);
|
||||
await radio.ecualizador.cargarPersistido();
|
||||
return radio;
|
||||
}
|
||||
|
||||
Emisora emisora(String uuid, String nombre) => Emisora(
|
||||
uuid: uuid,
|
||||
nombre: nombre,
|
||||
url: 'https://example.com/$uuid.mp3',
|
||||
);
|
||||
|
||||
group('EstadoRadio export/import — grupos de favoritos', () {
|
||||
test('round trip: los grupos y la asignación de CADA emisora sobreviven '
|
||||
'al viaje origen -> copia -> destino limpio', () async {
|
||||
final origen = await crearRadio();
|
||||
await origen.toggleFavorito(emisora('rock-1', 'Rock Uno'));
|
||||
await origen.toggleFavorito(emisora('rock-2', 'Rock Dos'));
|
||||
await origen.toggleFavorito(emisora('jazz-1', 'Jazz Uno'));
|
||||
await origen.toggleFavorito(emisora('suelta', 'Sin grupo'));
|
||||
await origen.crearGrupoFavoritos('Rock');
|
||||
await origen.crearGrupoFavoritos('Jazz');
|
||||
final rock = origen.gruposFavoritos.firstWhere((g) => g.nombre == 'Rock');
|
||||
final jazz = origen.gruposFavoritos.firstWhere((g) => g.nombre == 'Jazz');
|
||||
await origen.asignarGrupoFavorito('rock-1', rock.id);
|
||||
await origen.asignarGrupoFavorito('rock-2', rock.id);
|
||||
await origen.asignarGrupoFavorito('jazz-1', jazz.id);
|
||||
|
||||
final copia = await origen.exportarConfig();
|
||||
|
||||
final destino = await crearRadio();
|
||||
await destino.importarConfig(copia);
|
||||
|
||||
// Los grupos vuelven, con su nombre y su orden.
|
||||
final gruposDestino = destino.gruposFavoritos;
|
||||
expect(
|
||||
gruposDestino.map((g) => g.id),
|
||||
containsAll(<String>[rock.id, jazz.id]),
|
||||
);
|
||||
expect(gruposDestino.firstWhere((g) => g.id == rock.id).nombre, 'Rock');
|
||||
expect(gruposDestino.firstWhere((g) => g.id == jazz.id).nombre, 'Jazz');
|
||||
|
||||
// Y la asignación de CADA emisora vuelve con ellos.
|
||||
String grupoDe(String uuid) =>
|
||||
destino.listaFavoritos.firstWhere((e) => e.uuid == uuid)
|
||||
.grupoFavoritosId;
|
||||
expect(grupoDe('rock-1'), rock.id);
|
||||
expect(grupoDe('rock-2'), rock.id);
|
||||
expect(grupoDe('jazz-1'), jazz.id);
|
||||
expect(grupoDe('suelta'), GrupoFavoritos.sinAsignarId);
|
||||
});
|
||||
|
||||
test('una copia ANTIGUA sin `gruposFavoritos` importa limpiamente y deja '
|
||||
'intactos los grupos que ya existen en el dispositivo', () async {
|
||||
final destino = await crearRadio();
|
||||
await destino.crearGrupoFavoritos('Mío');
|
||||
final propio = destino.gruposFavoritos.firstWhere(
|
||||
(g) => g.nombre == 'Mío',
|
||||
);
|
||||
await destino.toggleFavorito(emisora('local-1', 'Local Uno'));
|
||||
await destino.asignarGrupoFavorito('local-1', propio.id);
|
||||
|
||||
// v1: ni `gruposFavoritos` ni `alarmas` ni preferencias. La regla del
|
||||
// sobre es que un campo AUSENTE no toca ese estado.
|
||||
await destino.importarConfig(<String, dynamic>{
|
||||
'version': 1,
|
||||
'favoritos': <Map<String, dynamic>>[],
|
||||
'emisorasCustom': <Map<String, dynamic>>[],
|
||||
'presetsEcualizador': <String, dynamic>{},
|
||||
});
|
||||
|
||||
expect(destino.gruposFavoritos.any((g) => g.id == propio.id), isTrue);
|
||||
expect(
|
||||
destino.gruposFavoritos.firstWhere((g) => g.id == propio.id).nombre,
|
||||
'Mío',
|
||||
);
|
||||
expect(
|
||||
destino.listaFavoritos.firstWhere((e) => e.uuid == 'local-1')
|
||||
.grupoFavoritosId,
|
||||
propio.id,
|
||||
);
|
||||
});
|
||||
|
||||
test('los grupos importados quedan visibles SIN reiniciar: importarConfig '
|
||||
'recarga la lista en memoria y notifica', () async {
|
||||
final origen = await crearRadio();
|
||||
await origen.toggleFavorito(emisora('rock-1', 'Rock Uno'));
|
||||
await origen.crearGrupoFavoritos('Rock');
|
||||
final rock = origen.gruposFavoritos.firstWhere((g) => g.nombre == 'Rock');
|
||||
await origen.asignarGrupoFavorito('rock-1', rock.id);
|
||||
final copia = await origen.exportarConfig();
|
||||
|
||||
final destino = await crearRadio();
|
||||
var notificaciones = 0;
|
||||
destino.addListener(() => notificaciones++);
|
||||
|
||||
await destino.importarConfig(copia);
|
||||
|
||||
expect(notificaciones, greaterThan(0));
|
||||
expect(destino.gruposFavoritos.any((g) => g.id == rock.id), isTrue);
|
||||
expect(
|
||||
destino.listaFavoritos.single.grupoFavoritosId,
|
||||
rock.id,
|
||||
reason:
|
||||
'la vista de favoritos agrupa por `grupoFavoritosId`: si la lista '
|
||||
'en memoria no se recarga tras restaurar los grupos, la pantalla '
|
||||
'sigue mostrando todo en «Sin asignar» hasta reiniciar',
|
||||
);
|
||||
});
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user