import 'dart:convert'; import 'package:flutter_test/flutter_test.dart'; import 'package:pluriwave/modelos/emisora.dart'; import 'package:pluriwave/modelos/grupo_favoritos.dart'; import 'package:pluriwave/servicios/contexto_reproduccion.dart'; import 'package:shared_preferences/shared_preferences.dart'; /// Contexto de reproducción — el «en qué lista estoy» que sobrevive a que el /// proceso muera. /// /// Mismo molde headless-safe que `emisoras_destacadas.dart`: solo /// `shared_preferences` y modelos, jamás `EstadoRadio` ni un `ChangeNotifier`, /// porque este módulo tiene que leerse desde el motor sin árbol de widgets que /// levanta Android Auto. void main() { TestWidgetsFlutterBinding.ensureInitialized(); Emisora emisora(String uuid, {String grupo = GrupoFavoritos.sinAsignarId}) => Emisora( uuid: uuid, nombre: uuid, url: 'https://example.com/$uuid', grupoFavoritosId: grupo, ); group('ContextoSalto — serialización', () { test('la clave de persistencia queda fijada literalmente', () { // Un rename silencioso aquí no rompe nada en compilación y deja al // conductor sin contexto tras actualizar: se fija a propósito. expect(claveContextoSalto, 'contexto_salto_v1'); }); test('round trip de los tres tipos que llevan carga útil', () { for (final contexto in [ const ContextoSalto.grupo('g-rock'), const ContextoSalto.favoritos(), const ContextoSalto.misEmisoras(), const ContextoSalto.todas(), const ContextoSalto.destacadas(['a', 'b', 'c']), ]) { expect(ContextoSalto.desdeMapa(contexto.aMapa()), contexto); } }); test('un payload corrupto o ajeno devuelve null en vez de lanzar', () { expect(ContextoSalto.desdeMapa(const {}), isNull); expect(ContextoSalto.desdeMapa(const {'tipo': 'inventado'}), isNull); expect( ContextoSalto.desdeMapa(const {'tipo': 'grupoFavoritos'}), isNull, reason: 'un contexto de grupo sin id de grupo no resuelve a nada', ); expect( ContextoSalto.desdeMapa(const {'tipo': 'destacadas', 'uuids': 7}), isNull, ); }); }); group('persistencia', () { setUp(() => SharedPreferences.setMockInitialValues({})); test('round trip por disco', () async { final prefs = await SharedPreferences.getInstance(); await guardarContextoSalto( const ContextoSalto.grupo('g-rock'), prefs: prefs, ); expect( await contextoSaltoPersistido(prefs: prefs), const ContextoSalto.grupo('g-rock'), ); }); test('sin nada persistido devuelve null', () async { final prefs = await SharedPreferences.getInstance(); expect(await contextoSaltoPersistido(prefs: prefs), isNull); }); test('un JSON ilegible degrada a null, nunca lanza: esto se lee desde un ' 'botón del volante', () async { SharedPreferences.setMockInitialValues({ claveContextoSalto: 'esto no es json', }); final prefs = await SharedPreferences.getInstance(); expect(await contextoSaltoPersistido(prefs: prefs), isNull); SharedPreferences.setMockInitialValues({ claveContextoSalto: jsonEncode({'tipo': 'inventado'}), }); expect( await contextoSaltoPersistido( prefs: await SharedPreferences.getInstance(), ), isNull, ); }); }); group('resolverListaContexto — degradación del contexto recordado', () { final rock1 = emisora('rock1', grupo: 'g-rock'); final rock2 = emisora('rock2', grupo: 'g-rock'); final jazz1 = emisora('jazz1', grupo: 'g-jazz'); final suelta = emisora('suelta'); final favoritos = [rock1, jazz1, rock2, suelta]; const grupos = [ GrupoFavoritos(id: 'g-rock', nombre: 'Rock', orden: 1), GrupoFavoritos(id: 'g-jazz', nombre: 'Jazz', orden: 2), ]; List resolver( ContextoSalto contexto, Emisora actual, { List? favs, List? gruposVivos, List misEmisoras = const [], List todas = const [], List destacadas = const [], }) => resolverListaContexto( contexto: contexto, actual: actual, favoritos: favs ?? favoritos, misEmisoras: misEmisoras, todas: todas, destacadas: destacadas, grupos: gruposVivos ?? grupos, ); test('el grupo recordado se recorre con sus miembros VIVOS, no con el ' 'snapshot', () { final nuevo = emisora('rock3', grupo: 'g-rock'); expect( resolver( const ContextoSalto.grupo('g-rock'), rock1, favs: [rock1, jazz1, rock2, nuevo], ), [rock1, rock2, nuevo], ); }); test('el grupo recordado ya NO existe -> cae a todos los favoritos', () { expect( resolver( const ContextoSalto.grupo('g-borrado'), rock1, gruposVivos: const [ GrupoFavoritos(id: 'g-jazz', nombre: 'Jazz', orden: 2), ], ), favoritos, ); }); test('el grupo SIGUE VIVO con un solo miembro -> se honra igual: un grupo ' 'de una emisora sigue siendo el grupo que eligió el conductor', () { expect( resolver( const ContextoSalto.grupo('g-jazz'), jazz1, ), [jazz1], ); }); test('el grupo sigue vivo pero la emisora que suena ya NO pertenece a él ' '-> se PERMANECE en el grupo (el llamador coge su primera emisora)', () { expect( resolver(const ContextoSalto.grupo('g-rock'), jazz1), [rock1, rock2], ); }); test('el grupo sigue vivo pero se quedó VACÍO -> no hay primera emisora ' 'que coger, así que se ensancha a todos los favoritos', () { expect( resolver( const ContextoSalto.grupo('g-rock'), jazz1, favs: [jazz1, suelta], ), [jazz1, suelta], ); }); test('el grupo fue borrado y la emisora ya NO es favorita -> aun así se ' 'cae a los favoritos: el llamador elegirá una de ellas', () { expect( resolver( const ContextoSalto.grupo('g-borrado'), emisora('fuera'), gruposVivos: const [], ), favoritos, ); }); test('no quedan favoritos -> lista vacía: el comportamiento de siempre ' 'cuando no hay emisoras agregadas', () { expect( resolver( const ContextoSalto.grupo('g-rock'), emisora('fuera'), favs: const [], gruposVivos: const [], ), isEmpty, ); }); test('la emisora salió de favoritos por completo -> el contexto de ' 'FAVORITOS se descarta', () { expect( resolver(const ContextoSalto.favoritos(), emisora('fuera')), isEmpty, ); }); test('favoritos / misEmisoras / todas se resuelven contra su lista viva', () { expect(resolver(const ContextoSalto.favoritos(), rock1), favoritos); final propia = emisora('propia'); expect( resolver( const ContextoSalto.misEmisoras(), propia, misEmisoras: [propia, suelta], ), [propia, suelta], ); final catalogo = emisora('catalogo'); expect( resolver( const ContextoSalto.todas(), catalogo, todas: [catalogo, rock1], ), [catalogo, rock1], ); }); test('destacadas respeta el ORDEN CONGELADO, que es la razón de existir ' 'del snapshot: la lista viva se reordena sola en cada lectura', () { final fip = emisora('fip'); final soma = emisora('soma'); final ajena = emisora('ajena'); expect( resolver( const ContextoSalto.destacadas(['ajena', 'fip', 'soma']), ajena, destacadas: [fip, soma], ), [ajena, fip, soma], reason: 'la emisora que suena entra en la lista aunque no esté en el set ' 'curado; si no, ambos botones morirían', ); }); test('destacadas: un uuid del snapshot que ya no resuelve se descarta', () { final fip = emisora('fip'); final soma = emisora('soma'); expect( resolver( const ContextoSalto.destacadas(['fip', 'retirada', 'soma']), fip, destacadas: [fip, soma], ), [fip, soma], ); }); test('destacadas: si la emisora que suena no está en el snapshot el ' 'contexto se descarta', () { final fip = emisora('fip'); expect( resolver( const ContextoSalto.destacadas(['fip']), emisora('otra'), destacadas: [fip], ), isEmpty, ); }); }); group('uuidsCongeladosDestacadas', () { test('respeta el orden curado y antepone la emisora que suena cuando no ' 'pertenece al set', () { final fip = emisora('fip'); final soma = emisora('soma'); expect( uuidsCongeladosDestacadas(actual: fip, destacadas: [fip, soma]), ['fip', 'soma'], ); expect( uuidsCongeladosDestacadas( actual: emisora('ajena'), destacadas: [fip, soma], ), ['ajena', 'fip', 'soma'], ); }); }); }