import 'dart:io'; import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:pluriwave/estado/estado_busqueda.dart'; import 'package:pluriwave/estado/estado_ecualizador.dart'; import 'package:pluriwave/estado/estado_grabacion.dart'; import 'package:pluriwave/estado/estado_navegacion.dart'; import 'package:pluriwave/estado/estado_radio.dart'; import 'package:pluriwave/l10n/gen/app_localizations.dart'; import 'package:pluriwave/modelos/emisora.dart'; import 'package:pluriwave/pantallas/pantalla_inicio.dart'; import 'package:pluriwave/widgets/pluri_root_header.dart'; import 'package:pluriwave/widgets/pluri_station_art_fallback.dart'; import 'package:pluriwave/widgets/visualizador_audio.dart'; import 'package:provider/provider.dart'; import 'package:shared_preferences/shared_preferences.dart'; import '../helpers/fakes.dart'; void main() { setUp(() { SharedPreferences.setMockInitialValues({}); }); // WU6 correction: the 3 scenarios that used to live here ("muestra custom // ... favorito usa flujo existente", "permite reintentar manualmente", // "PantallaFavoritos muestra custom favorito tras recarga") all depended // on `_gridEmisoras`/`_errorBanner`, which task 6.5 relocates to // `PantallaBuscar` and DELETES from here (WU5's task 5.9 deferred this // cleanup explicitly). Their coverage moved, adapted, to // `pantalla_buscar_test.dart` — PantallaInicio no longer renders any // station list of its own (only the hero + the "Tus emisoras" favorites // preview), so there is nothing left on this screen for those scenarios // to exercise. testWidgets( 'WU5 ADR-7 anti-cache: the Escuchar hero reflects a station changed ' 'from OUTSIDE the widget tree (e.g. Android Auto / a notification ' 'action), proving it caches nothing of its own', (tester) async { _setLargeSurfaceSize(tester); final audio = FakeServicioAudio(); final estado = EstadoRadio( audio: audio, favoritos: FakeServicioFavoritos(), radio: FakeServicioRadio(), servicioEcualizador: FakeServicioEcualizador(), servicioGrabacion: FakeServicioGrabacionRadio(), resolverArchivoCustom: _archivoCustomVacio, iniciarAutomaticamente: false, ); addTearDown(estado.dispose); await tester.runAsync(estado.inicializar); final estacionA = emisoraDemo(uuid: 'a', nombre: 'Estacion A'); final estacionB = emisoraDemo(uuid: 'b', nombre: 'Estacion B'); await estado.reproducir(estacionA); await tester.pumpWidget( _conProviders(estado, _testApp(const PantallaInicio())), ); // Bounded pump, not _pumpStableFrame/pumpAndSettle: a "reproduciendo" // station makes VisualizadorAudio start an indeterminately-repeating // AnimationController (visualizador_audio.dart:77, `_controller.repeat()`) // for its animated-fallback waveform — the same class of hazard as an // indeterminate spinner, just via animation. pumpAndSettle() would // never return while it keeps scheduling frames. await _pumpBounded(tester); expect(find.text('Estacion A'), findsOneWidget); // Mutates the underlying ServicioAudio DIRECTLY, bypassing // EstadoRadio.reproducir() entirely — this is exactly the shape of // navegacion_auto.dart's out-of-band mutation (Android Auto's // playFromMediaId). EstadoRadio's own audio.estadoStream listener // (not this test) is what is expected to pick this up and update // emisoraActual. await audio.reproducir(estacionB); await _pumpBounded(tester); expect(find.text('Estacion B'), findsOneWidget); expect(find.text('Estacion A'), findsNothing); }, ); testWidgets('WU5: "Ver todas" switches to the Favoritos root via ' 'EstadoNavegacionRaiz.irA, without pushing a route', (tester) async { _setLargeSurfaceSize(tester); final favoritos = FakeServicioFavoritos(); final estado = EstadoRadio( audio: FakeServicioAudio(), favoritos: favoritos, radio: FakeServicioRadio(), servicioEcualizador: FakeServicioEcualizador(), servicioGrabacion: FakeServicioGrabacionRadio(), resolverArchivoCustom: _archivoCustomVacio, iniciarAutomaticamente: false, ); addTearDown(estado.dispose); await tester.runAsync(estado.inicializar); await favoritos.agregar(emisoraDemo(uuid: 'f1', nombre: 'Favorita Uno')); await estado.cargarFavoritos(); final navegacion = EstadoNavegacionRaiz(); final observer = _RecordingNavigatorObserver(); await tester.pumpWidget( _conProviders( estado, _testApp(const PantallaInicio(), observers: [observer]), navegacion: navegacion, ), ); await _pumpStableFrame(tester); final pushesAntesDeTocar = observer.pushCount; await tester.ensureVisible(find.text('Ver todas')); await _pumpStableFrame(tester); await tester.tap(find.text('Ver todas')); await _pumpStableFrame(tester); expect(navegacion.actual, RaizPluriWave.favoritos); expect( observer.pushCount, pushesAntesDeTocar, reason: 'switches tabs — must NOT push a new route', ); }); testWidgets( 'visual fidelity (audit 1.10): Tus emisoras is a 2-column grid showing ' 'up to 8 stations, not a 6-capped horizontal strip', (tester) async { _setLargeSurfaceSize(tester); final favoritos = FakeServicioFavoritos(); final estado = EstadoRadio( audio: FakeServicioAudio(), favoritos: favoritos, radio: FakeServicioRadio(), servicioEcualizador: FakeServicioEcualizador(), servicioGrabacion: FakeServicioGrabacionRadio(), resolverArchivoCustom: _archivoCustomVacio, iniciarAutomaticamente: false, ); addTearDown(estado.dispose); await tester.runAsync(estado.inicializar); for (var i = 0; i < 8; i++) { await favoritos.agregar( emisoraDemo(uuid: 'grid-$i', nombre: 'Grid Estacion $i'), ); } await estado.cargarFavoritos(); await tester.pumpWidget( _conProviders(estado, _testApp(const PantallaInicio())), ); await _pumpStableFrame(tester); for (var i = 0; i < 8; i++) { expect( find.text('Grid Estacion $i'), findsOneWidget, reason: 'prototype t4 lines 82-88: a grid of 8, the old strip capped ' 'at 6', ); } final grid = tester.widget(find.byType(GridView)); final delegate = grid.gridDelegate as SliverGridDelegateWithFixedCrossAxisCount; expect( delegate.crossAxisCount, 2, reason: 'prototype t4 line 82: grid-template-columns:1fr 1fr', ); }, ); testWidgets('visual fidelity (audit 1.3): the hero art is 132 square', ( tester, ) async { // The redesign shipped this at 84 — the prototype draws 132 (t4 line // 56). Nothing caught it because every existing test here asserts // behaviour, never dimensions. This is the guard that would have. _setLargeSurfaceSize(tester); final favoritos = FakeServicioFavoritos(); final estado = EstadoRadio( audio: FakeServicioAudio(), favoritos: favoritos, radio: FakeServicioRadio(), servicioEcualizador: FakeServicioEcualizador(), servicioGrabacion: FakeServicioGrabacionRadio(), resolverArchivoCustom: _archivoCustomVacio, iniciarAutomaticamente: false, ); addTearDown(estado.dispose); await tester.runAsync(estado.inicializar); final sonando = emisoraDemo(uuid: 'f1', nombre: 'Favorita Uno'); await favoritos.agregar(sonando); await estado.cargarFavoritos(); // The hero only renders when a station is active — `emisoraActual` is // what gates it. Without this the assertion below passes vacuously. await tester.runAsync(() => estado.reproducir(sonando)); await tester.pumpWidget( _conProviders(estado, _testApp(const PantallaInicio())), ); await _pumpBounded(tester); final cuadrados = tester .widgetList(find.byType(SizedBox)) .where((s) => s.width == 132 && s.height == 132); expect( cuadrados, isNotEmpty, reason: 'prototype t4 line 56 sizes the Escuchar hero artwork at 132', ); }); testWidgets('visual fidelity (audit 1.6): the hero shows "genre · country · ' 'bitrate kbps" built from the station model, nothing invented', ( tester, ) async { _setLargeSurfaceSize(tester); final favoritos = FakeServicioFavoritos(); final estado = EstadoRadio( audio: FakeServicioAudio(), favoritos: favoritos, radio: FakeServicioRadio(), servicioEcualizador: FakeServicioEcualizador(), servicioGrabacion: FakeServicioGrabacionRadio(), resolverArchivoCustom: _archivoCustomVacio, iniciarAutomaticamente: false, ); addTearDown(estado.dispose); await tester.runAsync(estado.inicializar); const completa = Emisora( uuid: 'meta-1', nombre: 'Radio Horizonte', url: 'https://stream.demo/radio', tags: 'Noticias', pais: 'España', bitrate: 128, ); await favoritos.agregar(completa); await estado.cargarFavoritos(); // The hero only renders when a station is active. await tester.runAsync(() => estado.reproducir(completa)); await tester.pumpWidget( _conProviders(estado, _testApp(const PantallaInicio())), ); await _pumpBounded(tester); expect( find.text('Noticias · España · 128 kbps'), findsOneWidget, reason: 'prototype t4 line 62: "Noticias · España · 128 kbps"', ); }); testWidgets( 'visual fidelity (audit 1.7): the hero visualizer renders 30 discrete ' 'bars, not the continuous waveform stroke', (tester) async { // Item 16 is exactly the widget the audit warned about // (visualizador_audio.dart:77, `_controller.repeat()`) — bounded pump // only, never pumpAndSettle. _setLargeSurfaceSize(tester); final favoritos = FakeServicioFavoritos(); final estado = EstadoRadio( audio: FakeServicioAudio(), favoritos: favoritos, radio: FakeServicioRadio(), servicioEcualizador: FakeServicioEcualizador(), servicioGrabacion: FakeServicioGrabacionRadio(), resolverArchivoCustom: _archivoCustomVacio, iniciarAutomaticamente: false, ); addTearDown(estado.dispose); await tester.runAsync(estado.inicializar); final sonando = emisoraDemo(uuid: 'f1', nombre: 'Favorita Uno'); await favoritos.agregar(sonando); await estado.cargarFavoritos(); await tester.runAsync(() => estado.reproducir(sonando)); await tester.pumpWidget( _conProviders(estado, _testApp(const PantallaInicio())), ); await _pumpBounded(tester); expect( find.byKey(const Key('visualizador-barra-0')), findsOneWidget, reason: 'prototype t4 line 66-68: 30 discrete bottom-anchored bars', ); expect( find.descendant( of: find.byType(VisualizadorAudio), matching: find.byType(CustomPaint), ), findsNothing, ); }, ); testWidgets( 'visual fidelity (audit 1.6): a station missing every meta field omits ' 'the line gracefully, no stray separators', (tester) async { _setLargeSurfaceSize(tester); final favoritos = FakeServicioFavoritos(); final estado = EstadoRadio( audio: FakeServicioAudio(), favoritos: favoritos, radio: FakeServicioRadio(), servicioEcualizador: FakeServicioEcualizador(), servicioGrabacion: FakeServicioGrabacionRadio(), resolverArchivoCustom: _archivoCustomVacio, iniciarAutomaticamente: false, ); addTearDown(estado.dispose); await tester.runAsync(estado.inicializar); final desnuda = emisoraDemo(uuid: 'meta-2', nombre: 'Radio Desnuda'); await favoritos.agregar(desnuda); await estado.cargarFavoritos(); await tester.runAsync(() => estado.reproducir(desnuda)); await tester.pumpWidget( _conProviders(estado, _testApp(const PantallaInicio())), ); await _pumpBounded(tester); expect(find.byKey(const ValueKey('escuchar-hero-meta')), findsNothing); }, ); testWidgets('visual fidelity (audit 1.11): a count badge sits next to "Tus ' 'emisoras", showing the TOTAL favorite count, not the 8-capped grid ' 'size', (tester) async { _setLargeSurfaceSize(tester); final favoritos = FakeServicioFavoritos(); final estado = EstadoRadio( audio: FakeServicioAudio(), favoritos: favoritos, radio: FakeServicioRadio(), servicioEcualizador: FakeServicioEcualizador(), servicioGrabacion: FakeServicioGrabacionRadio(), resolverArchivoCustom: _archivoCustomVacio, iniciarAutomaticamente: false, ); addTearDown(estado.dispose); await tester.runAsync(estado.inicializar); for (var i = 0; i < 11; i++) { await favoritos.agregar( emisoraDemo(uuid: 'badge-$i', nombre: 'Badge Estacion $i'), ); } await estado.cargarFavoritos(); await tester.pumpWidget( _conProviders(estado, _testApp(const PantallaInicio())), ); await _pumpStableFrame(tester); expect( find.text('11'), findsOneWidget, reason: 'prototype t4 line 80: the badge reflects how many favorites ' 'exist, even though the grid below caps display at 8', ); }); testWidgets( 'visual fidelity (audit 1.12): "Ver todas" renders as plain brand-teal ' 'text, not a Material TextButton', (tester) async { _setLargeSurfaceSize(tester); final favoritos = FakeServicioFavoritos(); final estado = EstadoRadio( audio: FakeServicioAudio(), favoritos: favoritos, radio: FakeServicioRadio(), servicioEcualizador: FakeServicioEcualizador(), servicioGrabacion: FakeServicioGrabacionRadio(), resolverArchivoCustom: _archivoCustomVacio, iniciarAutomaticamente: false, ); addTearDown(estado.dispose); await tester.runAsync(estado.inicializar); await favoritos.agregar(emisoraDemo(uuid: 'f1', nombre: 'Favorita Uno')); await estado.cargarFavoritos(); await tester.pumpWidget( _conProviders(estado, _testApp(const PantallaInicio())), ); await _pumpStableFrame(tester); expect(find.byType(TextButton), findsNothing); final texto = tester.widget(find.text('Ver todas')); expect(texto.style?.fontSize, 12); expect(texto.style?.fontWeight, FontWeight.w800); expect(texto.style?.color, const Color(0xFF21D4D9)); }, ); testWidgets( 'S7 visual fidelity: "Tus emisoras" uses the in-page section heading ' 'style (15/w800/ls-.2), not titleMedium(16)/w900', (tester) async { _setLargeSurfaceSize(tester); final favoritos = FakeServicioFavoritos(); final estado = EstadoRadio( audio: FakeServicioAudio(), favoritos: favoritos, radio: FakeServicioRadio(), servicioEcualizador: FakeServicioEcualizador(), servicioGrabacion: FakeServicioGrabacionRadio(), resolverArchivoCustom: _archivoCustomVacio, iniciarAutomaticamente: false, ); addTearDown(estado.dispose); await tester.runAsync(estado.inicializar); await favoritos.agregar(emisoraDemo(uuid: 'f1', nombre: 'Favorita Uno')); await estado.cargarFavoritos(); await tester.pumpWidget( _conProviders(estado, _testApp(const PantallaInicio())), ); await _pumpStableFrame(tester); final texto = tester.widget(find.text('Tus emisoras')); expect(texto.style?.fontSize, 15, reason: 'prototype t4 line 80'); expect(texto.style?.fontWeight, FontWeight.w800); expect(texto.style?.letterSpacing, -0.2); }, ); testWidgets( 'visual fidelity (audit 1.1): a full-bleed art/gradient/noise wash ' 'sits behind the whole screen, keyed so it can be found', (tester) async { _setLargeSurfaceSize(tester); final favoritos = FakeServicioFavoritos(); final estado = EstadoRadio( audio: FakeServicioAudio(), favoritos: favoritos, radio: FakeServicioRadio(), servicioEcualizador: FakeServicioEcualizador(), servicioGrabacion: FakeServicioGrabacionRadio(), resolverArchivoCustom: _archivoCustomVacio, iniciarAutomaticamente: false, ); addTearDown(estado.dispose); await tester.runAsync(estado.inicializar); final sonando = emisoraDemo(uuid: 'f1', nombre: 'Favorita Uno'); await favoritos.agregar(sonando); await estado.cargarFavoritos(); await tester.runAsync(() => estado.reproducir(sonando)); await tester.pumpWidget( _conProviders(estado, _testApp(const PantallaInicio())), ); await _pumpBounded(tester); expect( find.byKey(const ValueKey('escuchar-background-art')), findsOneWidget, reason: 'prototype t4 lines 49-51: full-bleed art at .5 opacity + a ' 'gradient + a noise texture', ); }, ); testWidgets( 'visual fidelity (audit 1.1): the background wash still renders with ' 'nothing playing (gradient/noise only, no station art to show)', (tester) async { _setLargeSurfaceSize(tester); final estado = EstadoRadio( audio: FakeServicioAudio(), favoritos: FakeServicioFavoritos(), radio: FakeServicioRadio(), servicioEcualizador: FakeServicioEcualizador(), servicioGrabacion: FakeServicioGrabacionRadio(), resolverArchivoCustom: _archivoCustomVacio, iniciarAutomaticamente: false, ); addTearDown(estado.dispose); await tester.runAsync(estado.inicializar); await tester.pumpWidget( _conProviders(estado, _testApp(const PantallaInicio())), ); await _pumpStableFrame(tester); expect( find.byKey(const ValueKey('escuchar-background-art')), findsOneWidget, ); }, ); testWidgets('visual fidelity (audit 1.2): Escuchar has its own 52px "Now ' 'listening" eyebrow header, not the shared 56px PluriRootHeader', ( tester, ) async { _setLargeSurfaceSize(tester); final estado = EstadoRadio( audio: FakeServicioAudio(), favoritos: FakeServicioFavoritos(), radio: FakeServicioRadio(), servicioEcualizador: FakeServicioEcualizador(), servicioGrabacion: FakeServicioGrabacionRadio(), resolverArchivoCustom: _archivoCustomVacio, iniciarAutomaticamente: false, ); addTearDown(estado.dispose); await tester.runAsync(estado.inicializar); await tester.pumpWidget( _conProviders(estado, _testApp(const PantallaInicio())), ); await _pumpStableFrame(tester); expect(find.byType(PluriRootHeader), findsNothing); expect( find.text('Escuchando ahora'), findsOneWidget, reason: 'prototype t4 line 53: "ESCUCHANDO AHORA"', ); expect(find.byIcon(Icons.bedtime_outlined), findsOneWidget); final cabeceras = tester .widgetList(find.byType(SizedBox)) .where((s) => s.height == 52); expect(cabeceras, isNotEmpty, reason: 'prototype t4 line 53: height:52px'); }); testWidgets( "visual fidelity (audit 1.2): the header's bedtime icon still opens " 'the shared sleep-timer sheet', (tester) async { _setLargeSurfaceSize(tester); final estado = EstadoRadio( audio: FakeServicioAudio(), favoritos: FakeServicioFavoritos(), radio: FakeServicioRadio(), servicioEcualizador: FakeServicioEcualizador(), servicioGrabacion: FakeServicioGrabacionRadio(), resolverArchivoCustom: _archivoCustomVacio, iniciarAutomaticamente: false, ); addTearDown(estado.dispose); await tester.runAsync(estado.inicializar); await tester.pumpWidget( _conProviders(estado, _testApp(const PantallaInicio())), ); await _pumpStableFrame(tester); await tester.tap(find.byIcon(Icons.bedtime_outlined)); await _pumpStableFrame(tester); expect(find.byType(ActionChip), findsWidgets); }, ); testWidgets('visual fidelity (audit 1.5): the station name is 24/w800/ls-.5/' 'height 1.12, not titleLarge(22) with no letter-spacing/height', ( tester, ) async { _setLargeSurfaceSize(tester); final favoritos = FakeServicioFavoritos(); final estado = EstadoRadio( audio: FakeServicioAudio(), favoritos: favoritos, radio: FakeServicioRadio(), servicioEcualizador: FakeServicioEcualizador(), servicioGrabacion: FakeServicioGrabacionRadio(), resolverArchivoCustom: _archivoCustomVacio, iniciarAutomaticamente: false, ); addTearDown(estado.dispose); await tester.runAsync(estado.inicializar); // Deliberately NOT added to favoritos: the "Tus emisoras" grid also // renders a station's bare name, and this assertion targets the // hero's OWN station-name Text specifically -- a favorited station // with the same name would render twice and make `find.text` throw. final sonando = emisoraDemo(uuid: 'f1', nombre: 'Radio Horizonte'); await tester.runAsync(() => estado.reproducir(sonando)); await tester.pumpWidget( _conProviders(estado, _testApp(const PantallaInicio())), ); await _pumpBounded(tester); final texto = tester.widget(find.text('Radio Horizonte')); expect(texto.style?.fontSize, 24, reason: 'prototype t4 line 60'); expect(texto.style?.fontWeight, FontWeight.w800); expect(texto.style?.letterSpacing, -0.5); expect(texto.style?.height, 1.12); }); testWidgets( 'visual fidelity (audit 1.8): the hero play button is 72x72 with a ' '38px icon, not 56x56 with a 28px icon', (tester) async { _setLargeSurfaceSize(tester); final favoritos = FakeServicioFavoritos(); final estado = EstadoRadio( audio: FakeServicioAudio(), favoritos: favoritos, radio: FakeServicioRadio(), servicioEcualizador: FakeServicioEcualizador(), servicioGrabacion: FakeServicioGrabacionRadio(), resolverArchivoCustom: _archivoCustomVacio, iniciarAutomaticamente: false, ); addTearDown(estado.dispose); await tester.runAsync(estado.inicializar); final sonando = emisoraDemo(uuid: 'f1', nombre: 'Favorita Uno'); await favoritos.agregar(sonando); await estado.cargarFavoritos(); await tester.runAsync(() => estado.reproducir(sonando)); await tester.pumpWidget( _conProviders(estado, _testApp(const PantallaInicio())), ); await _pumpBounded(tester); final botones = tester .widgetList(find.byType(SizedBox)) .where((s) => s.width == 72 && s.height == 72); expect(botones, isNotEmpty, reason: 'prototype t4 line 73'); final iconos = tester .widgetList(find.byType(Icon)) .where( (i) => i.icon == Icons.play_arrow_rounded || i.icon == Icons.pause_rounded, ); expect(iconos, isNotEmpty); expect(iconos.first.size, 38); }, ); testWidgets( 'visual fidelity (audit 1.9): the EQ/record/quality pill is centred, ' 'pill-shaped and uses expand_less at 17px -- not a left-aligned ' 'OutlinedButton with tune_rounded', (tester) async { _setLargeSurfaceSize(tester); final favoritos = FakeServicioFavoritos(); final estado = EstadoRadio( audio: FakeServicioAudio(), favoritos: favoritos, radio: FakeServicioRadio(), servicioEcualizador: FakeServicioEcualizador(), servicioGrabacion: FakeServicioGrabacionRadio(), resolverArchivoCustom: _archivoCustomVacio, iniciarAutomaticamente: false, ); addTearDown(estado.dispose); await tester.runAsync(estado.inicializar); final sonando = emisoraDemo(uuid: 'f1', nombre: 'Favorita Uno'); await favoritos.agregar(sonando); await estado.cargarFavoritos(); await tester.runAsync(() => estado.reproducir(sonando)); await tester.pumpWidget( _conProviders(estado, _testApp(const PantallaInicio())), ); await _pumpBounded(tester); expect(find.byType(OutlinedButton), findsNothing); expect( find.byIcon(Icons.expand_less_rounded), findsOneWidget, reason: 'prototype t4 line 78: expand_less at 17px', ); final icono = tester.widget(find.byIcon(Icons.expand_less_rounded)); expect(icono.size, 17); final pildoras = tester .widgetList(find.byType(DecoratedBox)) .map((d) => d.decoration) .whereType() .where((d) => d.borderRadius == BorderRadius.circular(999)); expect( pildoras, isNotEmpty, reason: 'prototype t4 line 78: border-radius:999px pill', ); }, ); testWidgets('issue 6 (feedback-pruebas): the Escuchar hero shows the shared ' 'PluriStationArtFallback for a station with no favicon, not a flat ' 'coloured square', (tester) async { _setLargeSurfaceSize(tester); final favoritos = FakeServicioFavoritos(); final estado = EstadoRadio( audio: FakeServicioAudio(), favoritos: favoritos, radio: FakeServicioRadio(), servicioEcualizador: FakeServicioEcualizador(), servicioGrabacion: FakeServicioGrabacionRadio(), resolverArchivoCustom: _archivoCustomVacio, iniciarAutomaticamente: false, ); addTearDown(estado.dispose); await tester.runAsync(estado.inicializar); final sonando = emisoraDemo(uuid: 'sin-arte', nombre: 'Sin Arte'); await tester.runAsync(() => estado.reproducir(sonando)); await tester.pumpWidget( _conProviders(estado, _testApp(const PantallaInicio())), ); await _pumpBounded(tester); expect( find.byType(PluriStationArtFallback), findsWidgets, reason: 'issue 6: the hero must reach the shared fallback, not its own ' 'flat primaryContainer square', ); }); testWidgets( 'issue 6 (feedback-pruebas): a "Tus emisoras" grid cell shows the shared ' 'PluriStationArtFallback for a favourite with no favicon', (tester) async { _setLargeSurfaceSize(tester); final favoritos = FakeServicioFavoritos(); final estado = EstadoRadio( audio: FakeServicioAudio(), favoritos: favoritos, radio: FakeServicioRadio(), servicioEcualizador: FakeServicioEcualizador(), servicioGrabacion: FakeServicioGrabacionRadio(), resolverArchivoCustom: _archivoCustomVacio, iniciarAutomaticamente: false, ); addTearDown(estado.dispose); await tester.runAsync(estado.inicializar); await favoritos.agregar( emisoraDemo(uuid: 'grid-sin-arte', nombre: 'Grid Sin Arte'), ); await estado.cargarFavoritos(); await tester.pumpWidget( _conProviders(estado, _testApp(const PantallaInicio())), ); await _pumpStableFrame(tester); expect( find.byType(PluriStationArtFallback), findsWidgets, reason: 'issue 6: the grid cell must reach the shared fallback, not its ' 'own flat primaryContainer square', ); }, ); } /// Mirrors the app.dart wiring: EstadoRadio owns the domain notifiers and /// the providers only expose the instances (no dispose callbacks). /// [navegacion] defaults to a fresh [EstadoNavegacionRaiz] — harmless to /// include for every test, only exercised by the "Ver todas" scenarios. Widget _conProviders( EstadoRadio estado, Widget child, { EstadoNavegacionRaiz? navegacion, }) { return MultiProvider( providers: [ ChangeNotifierProvider.value(value: estado), ListenableProvider.value(value: estado.ecualizador), ListenableProvider.value(value: estado.grabacion), ListenableProvider.value(value: estado.busqueda), ChangeNotifierProvider.value( value: navegacion ?? EstadoNavegacionRaiz(), ), ], child: child, ); } Widget _testApp(Widget body, {List observers = const []}) { return MaterialApp( locale: const Locale('es'), localizationsDelegates: AppLocalizations.localizationsDelegates, supportedLocales: AppLocalizations.supportedLocales, navigatorObservers: observers, home: Scaffold(body: body), ); } /// Counts route pushes so a test can assert "switched tabs, did not push". class _RecordingNavigatorObserver extends NavigatorObserver { int pushCount = 0; @override void didPush(Route route, Route? previousRoute) { pushCount++; } } Future _pumpStableFrame(WidgetTester tester) async { await tester.pump(); await tester.pumpAndSettle(const Duration(milliseconds: 100)); } /// WU5: bounded pump, safe when a "reproduciendo" station is rendered — /// `VisualizadorAudio` starts a repeating `AnimationController` for its /// animated-fallback waveform in that case, which `pumpAndSettle` (used by /// `_pumpStableFrame`) would wait on forever. Future _pumpBounded(WidgetTester tester) async { await tester.pump(); await tester.pump(const Duration(milliseconds: 100)); } void _setLargeSurfaceSize(WidgetTester tester) { tester.view.physicalSize = const Size(1440, 3200); tester.view.devicePixelRatio = 1.0; addTearDown(tester.view.resetPhysicalSize); addTearDown(tester.view.resetDevicePixelRatio); } Future _archivoCustomVacio() async => File('${Directory.current.path}/test/fixtures/emisoras_custom_vacio.json');