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.
193 lines
6.1 KiB
Dart
193 lines
6.1 KiB
Dart
import 'dart:io';
|
|
|
|
import 'package:flutter_test/flutter_test.dart';
|
|
import 'package:path/path.dart' as p;
|
|
import 'package:pluriwave/modelos/emisora.dart';
|
|
import 'package:pluriwave/modelos/grupo_favoritos.dart';
|
|
import 'package:pluriwave/servicios/servicio_favoritos.dart';
|
|
import 'package:sqflite_common_ffi/sqflite_ffi.dart';
|
|
|
|
void main() {
|
|
late Directory tempDir;
|
|
|
|
setUpAll(() {
|
|
sqfliteFfiInit();
|
|
});
|
|
|
|
setUp(() async {
|
|
tempDir = await Directory.systemTemp.createTemp('pluriwave_favoritos_');
|
|
});
|
|
|
|
tearDown(() async {
|
|
if (await tempDir.exists()) {
|
|
await tempDir.delete(recursive: true);
|
|
}
|
|
});
|
|
|
|
ServicioFavoritos crearServicio() {
|
|
return ServicioFavoritos(
|
|
databaseFactory: databaseFactoryFfi,
|
|
databasePathProvider: () async => tempDir.path,
|
|
);
|
|
}
|
|
|
|
test(
|
|
'primera instalación crea esquema completo y guarda favoritos',
|
|
() async {
|
|
final servicio = crearServicio();
|
|
addTearDown(servicio.cerrar);
|
|
|
|
await servicio.agregar(_emisora('radio-1', 'Radio Uno'));
|
|
|
|
final favoritos = await servicio.obtenerTodos();
|
|
final grupos = await servicio.obtenerGrupos();
|
|
|
|
expect(favoritos, hasLength(1));
|
|
expect(favoritos.single.grupoFavoritosId, GrupoFavoritos.sinAsignarId);
|
|
expect(grupos, hasLength(1));
|
|
expect(grupos.single.esSinAsignar, isTrue);
|
|
},
|
|
);
|
|
|
|
test('migra esquema antiguo sin grupo ni columnas nuevas', () async {
|
|
final dbPath = p.join(tempDir.path, 'pluriwave.db');
|
|
final db = await databaseFactoryFfi.openDatabase(
|
|
dbPath,
|
|
options: OpenDatabaseOptions(
|
|
version: 1,
|
|
onCreate: (db, version) async {
|
|
await db.execute('''
|
|
CREATE TABLE favoritos (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
uuid TEXT NOT NULL UNIQUE,
|
|
nombre TEXT NOT NULL,
|
|
url TEXT NOT NULL
|
|
)
|
|
''');
|
|
await db.insert('favoritos', {
|
|
'uuid': 'legacy-1',
|
|
'nombre': 'Legacy Radio',
|
|
'url': 'https://example.com/legacy.mp3',
|
|
});
|
|
},
|
|
),
|
|
);
|
|
await db.close();
|
|
|
|
final servicio = crearServicio();
|
|
addTearDown(servicio.cerrar);
|
|
|
|
final favoritos = await servicio.obtenerTodos();
|
|
final grupos = await servicio.obtenerGrupos();
|
|
|
|
expect(favoritos, hasLength(1));
|
|
expect(favoritos.single.uuid, 'legacy-1');
|
|
expect(favoritos.single.grupoFavoritosId, GrupoFavoritos.sinAsignarId);
|
|
expect(grupos.singleWhere((g) => g.esSinAsignar).nombre, 'Sin asignar');
|
|
|
|
final grupo = await servicio.crearGrupo('Viajes');
|
|
await servicio.asignarGrupo('legacy-1', grupo.id);
|
|
expect((await servicio.obtenerTodos()).single.grupoFavoritosId, grupo.id);
|
|
});
|
|
|
|
test('eliminar grupo reasigna sus favoritos a Sin asignar', () async {
|
|
final servicio = crearServicio();
|
|
addTearDown(servicio.cerrar);
|
|
|
|
await servicio.agregar(_emisora('radio-1', 'Radio Uno'));
|
|
final grupo = await servicio.crearGrupo('Trabajo');
|
|
await servicio.asignarGrupo('radio-1', grupo.id);
|
|
|
|
await servicio.eliminarGrupo(grupo.id);
|
|
|
|
final favoritos = await servicio.obtenerTodos();
|
|
final grupos = await servicio.obtenerGrupos();
|
|
expect(favoritos.single.grupoFavoritosId, GrupoFavoritos.sinAsignarId);
|
|
expect(grupos.any((g) => g.id == grupo.id), isFalse);
|
|
});
|
|
|
|
// ── Primitivas de restauración (importarConfig) ────────────────────────────
|
|
//
|
|
// `agregar` es la primitiva de "marcar como favorita": fuerza `sin_asignar`
|
|
// y un `orden` nuevo a propósito, porque una emisora recién marcada no
|
|
// pertenece a ningún grupo. Reusarla para RESTAURAR una copia de seguridad
|
|
// destruía justo los dos campos que la copia traía. `restaurarFavorito` es
|
|
// la primitiva que faltaba.
|
|
|
|
test('restaurarFavorito preserva el grupo y el orden de la copia', () async {
|
|
final servicio = crearServicio();
|
|
addTearDown(servicio.cerrar);
|
|
|
|
await servicio.restaurarGrupo(
|
|
const GrupoFavoritos(id: 'grupo-rock', nombre: 'Rock', orden: 7),
|
|
);
|
|
|
|
await servicio.restaurarFavorito(
|
|
_emisora(
|
|
'radio-1',
|
|
'Radio Uno',
|
|
).copyWith(orden: 42, grupoFavoritosId: 'grupo-rock'),
|
|
);
|
|
|
|
final favoritos = await servicio.obtenerTodos();
|
|
expect(favoritos.single.grupoFavoritosId, 'grupo-rock');
|
|
expect(favoritos.single.orden, 42);
|
|
});
|
|
|
|
test('restaurarFavorito cae a Sin asignar cuando el grupo de la copia no '
|
|
'existe (copia editada a mano o restauración parcial)', () async {
|
|
final servicio = crearServicio();
|
|
addTearDown(servicio.cerrar);
|
|
|
|
await servicio.restaurarFavorito(
|
|
_emisora(
|
|
'radio-1',
|
|
'Radio Uno',
|
|
).copyWith(orden: 3, grupoFavoritosId: 'grupo-fantasma'),
|
|
);
|
|
|
|
final favoritos = await servicio.obtenerTodos();
|
|
expect(favoritos.single.grupoFavoritosId, GrupoFavoritos.sinAsignarId);
|
|
expect(favoritos.single.orden, 3);
|
|
});
|
|
|
|
test('restaurarGrupo hace upsert preservando id, nombre y orden, y NUNCA '
|
|
'duplica el grupo protegido', () async {
|
|
final servicio = crearServicio();
|
|
addTearDown(servicio.cerrar);
|
|
|
|
await servicio.restaurarGrupo(
|
|
const GrupoFavoritos(id: 'grupo-jazz', nombre: 'Jazz', orden: 5),
|
|
);
|
|
// Segunda pasada (reimportar la misma copia): upsert, no duplicado.
|
|
await servicio.restaurarGrupo(
|
|
const GrupoFavoritos(id: 'grupo-jazz', nombre: 'Jazz renombrado', orden: 5),
|
|
);
|
|
await servicio.restaurarGrupo(
|
|
const GrupoFavoritos(
|
|
id: GrupoFavoritos.sinAsignarId,
|
|
nombre: 'Unassigned',
|
|
orden: 0,
|
|
protegido: true,
|
|
),
|
|
);
|
|
|
|
final grupos = await servicio.obtenerGrupos();
|
|
final jazz = grupos.singleWhere((g) => g.id == 'grupo-jazz');
|
|
expect(jazz.nombre, 'Jazz renombrado');
|
|
expect(jazz.orden, 5);
|
|
// El protegido conserva su nombre local: no se importa desde la copia.
|
|
expect(grupos.singleWhere((g) => g.esSinAsignar).nombre, 'Sin asignar');
|
|
});
|
|
}
|
|
|
|
Emisora _emisora(String uuid, String nombre) {
|
|
return Emisora(
|
|
uuid: uuid,
|
|
nombre: nombre,
|
|
url: 'https://example.com/$uuid.mp3',
|
|
codec: 'MP3',
|
|
bitrate: 192,
|
|
);
|
|
}
|