Files
pluriwave/test/pantallas/pantalla_inicio_test.dart
FreeTLab d81fabbe27 refactor(iap): make esPremium a required constructor parameter
EstadoAlarmas, EstadoGrabacion and EstadoRadio defaulted `esPremium` to
`() => true`, so any construction site that forgot to wire entitlement
compiled fine and silently ran ungated — failing OPEN to premium and
disabling the paywall with no test able to catch it.

The parameter is now required with no default. Production wiring in
app.dart was already correct and is unchanged; the 184 pre-existing test
call sites now pass `() => true` explicitly, which is exactly the old
implicit default, so every assertion is untouched.

EstadoRadio has no gate of its own but constructs EstadoGrabacion, so it
inherits the same contract.

The one test that existed to pin the old default is renamed to describe
what it still covers (the premium path through iniciar() with no
duracion); its assertions are unchanged.
2026-08-10 22:06:36 +02:00

859 lines
30 KiB
Dart

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(
esPremium: () => true,
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(
esPremium: () => true,
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(
esPremium: () => true,
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<GridView>(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(
esPremium: () => true,
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<SizedBox>(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(
esPremium: () => true,
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(
esPremium: () => true,
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(
esPremium: () => true,
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(
esPremium: () => true,
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(
esPremium: () => true,
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<Text>(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(
esPremium: () => true,
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<Text>(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(
esPremium: () => true,
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(
esPremium: () => true,
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(
esPremium: () => true,
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<SizedBox>(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(
esPremium: () => true,
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(
esPremium: () => true,
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<Text>(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(
esPremium: () => true,
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<SizedBox>(find.byType(SizedBox))
.where((s) => s.width == 72 && s.height == 72);
expect(botones, isNotEmpty, reason: 'prototype t4 line 73');
final iconos = tester
.widgetList<Icon>(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(
esPremium: () => true,
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<Icon>(find.byIcon(Icons.expand_less_rounded));
expect(icono.size, 17);
final pildoras = tester
.widgetList<DecoratedBox>(find.byType(DecoratedBox))
.map((d) => d.decoration)
.whereType<BoxDecoration>()
.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(
esPremium: () => true,
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(
esPremium: () => true,
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<EstadoRadio>.value(value: estado),
ListenableProvider<EstadoEcualizador>.value(value: estado.ecualizador),
ListenableProvider<EstadoGrabacion>.value(value: estado.grabacion),
ListenableProvider<EstadoBusqueda>.value(value: estado.busqueda),
ChangeNotifierProvider<EstadoNavegacionRaiz>.value(
value: navegacion ?? EstadoNavegacionRaiz(),
),
],
child: child,
);
}
Widget _testApp(Widget body, {List<NavigatorObserver> 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<dynamic> route, Route<dynamic>? previousRoute) {
pushCount++;
}
}
Future<void> _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<void> _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<File> _archivoCustomVacio() async =>
File('${Directory.current.path}/test/fixtures/emisoras_custom_vacio.json');