fix(escuchar,reproductor): add the section-heading token and screen 1-3 polish

Audit S7 plus the open items on screens 1-3. Escuchar gains the
prototype's own 52px "now listening" eyebrow header (t4:53), which is
structurally different from the 56px title row every other root uses
(t4:325) -- the earlier wiring test asserted PluriRootHeader on all five
roots, an over-broad premise now corrected to guard what actually holds:
no AppBar, and the sleep-timer action still reachable.
This commit is contained in:
2026-07-30 14:48:06 +02:00
parent 7ff156852f
commit 01615f9751
34 changed files with 1129 additions and 218 deletions
+294
View File
@@ -10,6 +10,7 @@ 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/visualizador_audio.dart';
import 'package:provider/provider.dart';
import 'package:shared_preferences/shared_preferences.dart';
@@ -404,6 +405,299 @@ void main() {
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<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(
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<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(
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<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(
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(
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',
);
},
);
}
/// Mirrors the app.dart wiring: EstadoRadio owns the domain notifiers and