From d1a911e58740573b728734d3c75ca47128473268 Mon Sep 17 00:00:00 2001 From: freetlab Date: Thu, 30 Jul 2026 18:54:31 +0200 Subject: [PATCH] fix(widgets): share station-art fallback across every surface TarjetaEmisora had the only good fallback for a station with no artwork -- a deterministic pick from 4 bundled illustrations with a gradient/glyph last resort. FilaEmisoraPlana's flat rows, the Escuchar hero, the "Tus emisoras" grid cell, the mini player and the full player each had their own, separate, flat primaryContainer square instead. Extract the good fallback into PluriStationArtFallback and use it from every one of those call sites. The selection formula (asset order, codeUnits-sum modulo) is preserved exactly, since navegacion_auto.dart mirrors the same formula independently for Android Auto's own drawable rotation. --- lib/pantallas/pantalla_inicio.dart | 35 +++---- lib/pantallas/pantalla_reproductor.dart | 18 ++-- lib/widgets/fila_emisora_plana.dart | 18 ++-- lib/widgets/mini_reproductor.dart | 18 ++-- lib/widgets/pluri_station_art_fallback.dart | 92 +++++++++++++++++++ lib/widgets/tarjeta_emisora.dart | 50 ++-------- test/pantallas/pantalla_inicio_test.dart | 71 ++++++++++++++ test/pantallas/pantalla_reproductor_test.dart | 27 ++++++ test/widgets/fila_emisora_plana_test.dart | 27 ++++++ test/widgets/mini_reproductor_barra_test.dart | 28 ++++++ .../pluri_station_art_fallback_test.dart | 86 +++++++++++++++++ .../tarjeta_emisora_thumbnail_test.dart | 19 ++++ 12 files changed, 395 insertions(+), 94 deletions(-) create mode 100644 lib/widgets/pluri_station_art_fallback.dart create mode 100644 test/widgets/pluri_station_art_fallback_test.dart diff --git a/lib/pantallas/pantalla_inicio.dart b/lib/pantallas/pantalla_inicio.dart index 4a0ecf6..b52608d 100644 --- a/lib/pantallas/pantalla_inicio.dart +++ b/lib/pantallas/pantalla_inicio.dart @@ -15,6 +15,7 @@ import '../widgets/pluri_glass_surface.dart'; import '../widgets/pluri_layout.dart'; import '../widgets/pluri_premium_widgets.dart'; import '../widgets/pluri_sleep_timer_sheet.dart'; +import '../widgets/pluri_station_art_fallback.dart'; import '../widgets/visualizador_audio.dart'; import 'pantalla_reproductor.dart'; @@ -568,9 +569,9 @@ class _ArteEscuchar extends StatelessWidget { imageUrl: emisora.favicon!, fit: BoxFit.cover, placeholder: (_, __) => _shimmerCuadrado(theme), - errorWidget: (_, __, ___) => _iconoFallback(theme), + errorWidget: (_, __, ___) => _iconoFallback(), ) - : _iconoFallback(theme), + : _iconoFallback(), ), ), ); @@ -582,14 +583,11 @@ class _ArteEscuchar extends StatelessWidget { child: Container(color: theme.colorScheme.surfaceContainerHighest), ); - Widget _iconoFallback(ThemeData theme) => Container( - color: theme.colorScheme.primaryContainer, - child: Icon( - Icons.radio_rounded, - size: 36, - color: theme.colorScheme.onPrimaryContainer, - ), - ); + // Issue 6 (feedback-pruebas): was a flat `primaryContainer` square with a + // bare `radio_rounded` icon — now the same shared fallback every other + // surface uses. + Widget _iconoFallback() => + PluriStationArtFallback(seed: emisora.uuid, iconSize: 36); } /// The hero's transport row — favorite / EQ toggle / stop / play-pause @@ -909,10 +907,10 @@ class _CeldaTusEmisoras extends StatelessWidget { imageUrl: emisora.favicon!, fit: BoxFit.cover, placeholder: (_, __) => _shimmer(theme), - errorWidget: (_, __, ___) => _iconoFallback(theme), + errorWidget: (_, __, ___) => _iconoFallback(), ); } - return _iconoFallback(theme); + return _iconoFallback(); } Widget _shimmer(ThemeData theme) => shimmer.Shimmer.fromColors( @@ -921,12 +919,9 @@ class _CeldaTusEmisoras extends StatelessWidget { child: Container(color: theme.colorScheme.surfaceContainerHighest), ); - Widget _iconoFallback(ThemeData theme) => Container( - color: theme.colorScheme.primaryContainer, - child: Icon( - Icons.radio_rounded, - size: 20, - color: theme.colorScheme.onPrimaryContainer, - ), - ); + // Issue 6 (feedback-pruebas): was a flat `primaryContainer` square with a + // bare `radio_rounded` icon — now the same shared fallback every other + // surface uses. + Widget _iconoFallback() => + PluriStationArtFallback(seed: emisora.uuid, iconSize: 20); } diff --git a/lib/pantallas/pantalla_reproductor.dart b/lib/pantallas/pantalla_reproductor.dart index 78d210a..451f9a4 100644 --- a/lib/pantallas/pantalla_reproductor.dart +++ b/lib/pantallas/pantalla_reproductor.dart @@ -20,6 +20,7 @@ import '../widgets/ecualizador_widget.dart'; import '../widgets/pluri_glass_surface.dart'; import '../widgets/pluri_premium_widgets.dart'; import '../widgets/pluri_push_scaffold.dart'; +import '../widgets/pluri_station_art_fallback.dart'; import '../widgets/visualizador_audio.dart'; /// WU14: restructured onto [PluriPushScaffold] (design ADR-2) — this screen @@ -291,10 +292,10 @@ class _ArteReproductor extends StatelessWidget { imageUrl: emisora.favicon!, fit: BoxFit.cover, placeholder: (_, __) => _shimmer(theme), - errorWidget: (_, __, ___) => _iconoFallback(theme), + errorWidget: (_, __, ___) => _iconoFallback(), ) else - _iconoFallback(theme), + _iconoFallback(), if (cargando) Container( color: Colors.black45, @@ -333,14 +334,11 @@ class _ArteReproductor extends StatelessWidget { child: Container(color: theme.colorScheme.surfaceContainerHighest), ); - Widget _iconoFallback(ThemeData theme) => Container( - color: theme.colorScheme.primaryContainer, - child: Icon( - Icons.radio_rounded, - size: 80, - color: theme.colorScheme.onPrimaryContainer, - ), - ); + // Issue 6 (feedback-pruebas): was a flat `primaryContainer` square with a + // bare `radio_rounded` icon — now the same shared fallback every other + // surface uses. + Widget _iconoFallback() => + PluriStationArtFallback(seed: emisora.uuid, iconSize: 80); } /// Audit 2.2 (t4 lines 108-109): a full-bleed blurred backdrop of the diff --git a/lib/widgets/fila_emisora_plana.dart b/lib/widgets/fila_emisora_plana.dart index 3ef4615..2d5ee6f 100644 --- a/lib/widgets/fila_emisora_plana.dart +++ b/lib/widgets/fila_emisora_plana.dart @@ -8,6 +8,7 @@ import '../l10n/display_names.dart'; import '../l10n/gen/app_localizations.dart'; import '../modelos/emisora.dart'; import '../tema/pluriwave_tokens.dart'; +import 'pluri_station_art_fallback.dart'; /// Item 23 / audit 4.3 + 6.5 (t4:226-232, 302-306): a flat, background-less /// station row — square thumbnail, name, meta line, and a caller-supplied @@ -234,10 +235,10 @@ class _ArteFilaEmisora extends StatelessWidget { imageUrl: emisora.favicon!, fit: BoxFit.cover, placeholder: (_, __) => _shimmer(theme), - errorWidget: (_, __, ___) => _iconoFallback(theme), + errorWidget: (_, __, ___) => _iconoFallback(), ); } - return _iconoFallback(theme); + return _iconoFallback(); } Widget _shimmer(ThemeData theme) => shimmer.Shimmer.fromColors( @@ -246,12 +247,9 @@ class _ArteFilaEmisora extends StatelessWidget { child: Container(color: theme.colorScheme.surfaceContainerHighest), ); - Widget _iconoFallback(ThemeData theme) => Container( - color: theme.colorScheme.primaryContainer, - child: Icon( - Icons.radio_rounded, - size: 22, - color: theme.colorScheme.onPrimaryContainer, - ), - ); + // Issue 6 (feedback-pruebas): was a flat `primaryContainer` square with a + // bare `radio_rounded` icon — now the same shared fallback every other + // surface uses. + Widget _iconoFallback() => + PluriStationArtFallback(seed: emisora.uuid, iconSize: 22); } diff --git a/lib/widgets/mini_reproductor.dart b/lib/widgets/mini_reproductor.dart index a7e126c..9a0fc2f 100644 --- a/lib/widgets/mini_reproductor.dart +++ b/lib/widgets/mini_reproductor.dart @@ -10,6 +10,7 @@ import '../modelos/emisora.dart'; import '../pantallas/pantalla_reproductor.dart'; import '../servicios/servicio_audio.dart'; import '../tema/pluriwave_theme.dart'; +import 'pluri_station_art_fallback.dart'; /// Barra inferior persistente con controles básicos de reproducción. /// Toca la barra para abrir PantallaReproductor completa. @@ -318,9 +319,9 @@ class _ArteMiniReproductor extends StatelessWidget { imageUrl: emisora.favicon!, fit: BoxFit.cover, placeholder: (_, __) => _shimmer(theme), - errorWidget: (_, __, ___) => _iconoFallback(theme), + errorWidget: (_, __, ___) => _iconoFallback(), ) - : _iconoFallback(theme), + : _iconoFallback(), ), ); } @@ -331,12 +332,9 @@ class _ArteMiniReproductor extends StatelessWidget { child: Container(color: theme.colorScheme.surfaceContainerHighest), ); - Widget _iconoFallback(ThemeData theme) => Container( - color: theme.colorScheme.primaryContainer, - child: Icon( - Icons.radio_rounded, - size: 20, - color: theme.colorScheme.onPrimaryContainer, - ), - ); + // Issue 6 (feedback-pruebas): was a flat `primaryContainer` square with a + // bare `radio_rounded` icon — now the same shared fallback every other + // surface uses. + Widget _iconoFallback() => + PluriStationArtFallback(seed: emisora.uuid, iconSize: 20); } diff --git a/lib/widgets/pluri_station_art_fallback.dart b/lib/widgets/pluri_station_art_fallback.dart new file mode 100644 index 0000000..f4cf67f --- /dev/null +++ b/lib/widgets/pluri_station_art_fallback.dart @@ -0,0 +1,92 @@ +import 'package:flutter/material.dart'; + +import '../l10n/gen/app_localizations.dart'; +import '../tema/pluriwave_theme.dart'; +import 'pluri_icon.dart'; + +/// Shared station-art fallback (feedback-pruebas issue 6). +/// +/// Before this widget existed, `TarjetaEmisora` had the only good fallback — +/// a deterministic pick from 4 bundled illustrations (`_fallbackArtFor`), +/// with a gradient + player glyph as a last resort if the asset itself ever +/// fails to decode. Every OTHER surface that can render a station without +/// artwork (`FilaEmisoraPlana`'s flat rows, the Escuchar hero, the "Tus +/// emisoras" grid cell, the mini player, the full player) had its own, +/// separate, much poorer copy: a flat `primaryContainer`-coloured square +/// with a bare `radio_rounded` icon. This widget is the ONE shared +/// implementation every one of those call sites now uses instead. +/// +/// The selection formula is pinned EXACTLY as `TarjetaEmisora` originally +/// had it (`seed.codeUnits.fold(0, (a, b) => a + b) % 4`, asset order +/// aurora/cosmic/pulse/nova) — `lib/servicios/navegacion_auto.dart` (a +/// protected, empty-diff file) independently mirrors this SAME formula for +/// Android Auto's own drawable-resource rotation +/// (`test/servicios/navegacion_auto_test.dart`'s `indiceArtePara`/ +/// `artUriPara` tests assert it inline, not by importing this widget) — +/// changing the order or the modulo here would silently desync the two +/// without either test suite noticing until an actual device compared them +/// side by side. +class PluriStationArtFallback extends StatelessWidget { + const PluriStationArtFallback({ + super.key, + required this.seed, + this.iconSize = 22, + }); + + /// Typically the station's `uuid` — the seed that deterministically picks + /// one of the 4 bundled arts below. + final String seed; + + /// Size of the centred player glyph drawn over the art. + final double iconSize; + + static const _arts = [ + 'assets/images/station_art_aurora.png', + 'assets/images/station_art_cosmic.png', + 'assets/images/station_art_pulse.png', + 'assets/images/station_art_nova.png', + ]; + + /// Exposed so other call sites (and tests) can assert which asset a given + /// seed resolves to without needing to render the widget. + static String artFor(String seed) { + final index = seed.codeUnits.fold(0, (a, b) => a + b) % _arts.length; + return _arts[index]; + } + + @override + Widget build(BuildContext context) { + return Stack( + fit: StackFit.expand, + children: [ + Image.asset( + artFor(seed), + fit: BoxFit.cover, + errorBuilder: + (_, __, ___) => DecoratedBox( + decoration: BoxDecoration( + gradient: LinearGradient( + begin: Alignment.topLeft, + end: Alignment.bottomRight, + colors: [ + context.pluriTokens.deepViolet, + context.pluriTokens.electricMagenta.withValues( + alpha: 0.8, + ), + ], + ), + ), + ), + ), + Center( + child: PluriIcon( + glyph: PluriIconGlyph.player, + variant: PluriIconVariant.activeGlow, + size: iconSize, + semanticLabel: AppLocalizations.of(context).stationIconLabel, + ), + ), + ], + ); + } +} diff --git a/lib/widgets/tarjeta_emisora.dart b/lib/widgets/tarjeta_emisora.dart index 40af058..40a6e41 100644 --- a/lib/widgets/tarjeta_emisora.dart +++ b/lib/widgets/tarjeta_emisora.dart @@ -10,6 +10,7 @@ import '../modelos/emisora.dart'; import '../tema/pluriwave_theme.dart'; import 'pluri_glass_surface.dart'; import 'pluri_icon.dart'; +import 'pluri_station_art_fallback.dart'; /// Tarjeta compacta para mostrar una emisora en listas y grids. /// Incluye botón de favorito visible en ambos modos. @@ -302,51 +303,12 @@ class _TarjetaEmisoraState extends State { ); } + // Issue 6 (feedback-pruebas): extracted into the shared + // `PluriStationArtFallback` so every surface that can render a station + // without artwork shows the SAME fallback — see that widget's own doc + // comment for why the selection formula must never drift. Widget _iconoFallback(double size) { - final art = _fallbackArtFor(widget.emisora.uuid); - return Stack( - fit: StackFit.expand, - children: [ - Image.asset( - art, - fit: BoxFit.cover, - errorBuilder: - (_, __, ___) => DecoratedBox( - decoration: BoxDecoration( - gradient: LinearGradient( - begin: Alignment.topLeft, - end: Alignment.bottomRight, - colors: [ - context.pluriTokens.deepViolet, - context.pluriTokens.electricMagenta.withValues( - alpha: 0.8, - ), - ], - ), - ), - ), - ), - Center( - child: PluriIcon( - glyph: PluriIconGlyph.player, - variant: PluriIconVariant.activeGlow, - size: size, - semanticLabel: AppLocalizations.of(context).stationIconLabel, - ), - ), - ], - ); - } - - String _fallbackArtFor(String seed) { - const arts = [ - 'assets/images/station_art_aurora.png', - 'assets/images/station_art_cosmic.png', - 'assets/images/station_art_pulse.png', - 'assets/images/station_art_nova.png', - ]; - final index = seed.codeUnits.fold(0, (a, b) => a + b) % arts.length; - return arts[index]; + return PluriStationArtFallback(seed: widget.emisora.uuid, iconSize: size); } } diff --git a/test/pantallas/pantalla_inicio_test.dart b/test/pantallas/pantalla_inicio_test.dart index 038f1bd..a7a980d 100644 --- a/test/pantallas/pantalla_inicio_test.dart +++ b/test/pantallas/pantalla_inicio_test.dart @@ -11,6 +11,7 @@ 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'; @@ -698,6 +699,76 @@ void main() { ); }, ); + + 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 diff --git a/test/pantallas/pantalla_reproductor_test.dart b/test/pantallas/pantalla_reproductor_test.dart index ab9d3fc..3d00439 100644 --- a/test/pantallas/pantalla_reproductor_test.dart +++ b/test/pantallas/pantalla_reproductor_test.dart @@ -12,6 +12,7 @@ import 'package:pluriwave/pantallas/pantalla_reproductor.dart'; import 'package:pluriwave/tema/pluriwave_tokens.dart'; import 'package:pluriwave/widgets/ecualizador_widget.dart'; import 'package:pluriwave/widgets/pluri_glass_surface.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'; @@ -704,4 +705,30 @@ void main() { }, ); }); + + group('issue 6 (feedback-pruebas): shared station-art fallback', () { + testWidgets( + 'a station with no favicon shows the shared PluriStationArtFallback, ' + 'not a flat coloured square', + (tester) async { + // `emisora` (the file-level fixture) never sets a favicon. + final estado = crearEstado(); + addTearDown(estado.dispose); + + await montarPantalla(tester, estado); + + final arte = find.byKey(const Key('player-hero-art')); + expect( + find.descendant( + of: arte, + matching: find.byType(PluriStationArtFallback), + ), + findsOneWidget, + reason: + 'issue 6: the full player must reach the shared fallback, ' + 'not its own flat primaryContainer square', + ); + }, + ); + }); } diff --git a/test/widgets/fila_emisora_plana_test.dart b/test/widgets/fila_emisora_plana_test.dart index dd9298a..2c18f35 100644 --- a/test/widgets/fila_emisora_plana_test.dart +++ b/test/widgets/fila_emisora_plana_test.dart @@ -4,6 +4,7 @@ import 'package:pluriwave/estado/estado_radio.dart'; import 'package:pluriwave/l10n/gen/app_localizations.dart'; import 'package:pluriwave/tema/pluriwave_tokens.dart'; import 'package:pluriwave/widgets/fila_emisora_plana.dart'; +import 'package:pluriwave/widgets/pluri_station_art_fallback.dart'; import 'package:pluriwave/widgets/tarjeta_emisora.dart'; import 'package:provider/provider.dart'; @@ -77,6 +78,32 @@ void main() { ); }); + testWidgets( + 'issue 6 (feedback-pruebas): a station with no favicon shows the shared ' + 'PluriStationArtFallback, not a flat coloured square', + (tester) async { + final estado = _estado(); + addTearDown(estado.dispose); + final emisora = emisoraDemo(uuid: 'sin-arte', nombre: 'Sin Arte'); + + await tester.pumpWidget( + _host(estado, FilaEmisoraPlana(emisora: emisora, meta: '')), + ); + await tester.pump(); + + expect( + find.descendant( + of: find.byKey(const ValueKey('fila-emisora-plana-arte')), + matching: find.byType(PluriStationArtFallback), + ), + findsOneWidget, + reason: + 'issue 6: the flat row must reach the shared fallback, not its ' + 'own flat primaryContainer square', + ); + }, + ); + testWidgets('omits the meta line entirely when empty (no stray gap)', ( tester, ) async { diff --git a/test/widgets/mini_reproductor_barra_test.dart b/test/widgets/mini_reproductor_barra_test.dart index bfa8fd0..d3c00bd 100644 --- a/test/widgets/mini_reproductor_barra_test.dart +++ b/test/widgets/mini_reproductor_barra_test.dart @@ -5,6 +5,7 @@ import 'package:pluriwave/l10n/gen/app_localizations.dart'; import 'package:pluriwave/tema/pluriwave_theme.dart'; import 'package:pluriwave/tema/pluriwave_tokens.dart'; import 'package:pluriwave/widgets/mini_reproductor.dart'; +import 'package:pluriwave/widgets/pluri_station_art_fallback.dart'; import 'package:pluriwave/widgets/visualizador_audio.dart'; import 'package:provider/provider.dart'; @@ -75,6 +76,33 @@ void main() { }, ); + testWidgets( + 'issue 6 (feedback-pruebas): a station with no favicon shows the shared ' + 'PluriStationArtFallback, not a flat coloured square', + (tester) async { + final estado = _estadoConEmisora(); + addTearDown(estado.dispose); + await estado.reproducir( + emisoraDemo(uuid: 'sin-arte', nombre: 'Sin Arte'), + ); + + await tester.pumpWidget(_hostFor(estado)); + await tester.pump(); + + final arte = find.byKey(const ValueKey('mini-reproductor-arte')); + expect( + find.descendant( + of: arte, + matching: find.byType(PluriStationArtFallback), + ), + findsOneWidget, + reason: + 'issue 6: the mini player must reach the shared fallback, not ' + 'its own flat primaryContainer square', + ); + }, + ); + testWidgets( 'is opaque -- no BackdropFilter -- unlike the former glass pill', (tester) async { diff --git a/test/widgets/pluri_station_art_fallback_test.dart b/test/widgets/pluri_station_art_fallback_test.dart new file mode 100644 index 0000000..2dddf39 --- /dev/null +++ b/test/widgets/pluri_station_art_fallback_test.dart @@ -0,0 +1,86 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:pluriwave/l10n/gen/app_localizations.dart'; +import 'package:pluriwave/widgets/pluri_icon.dart'; +import 'package:pluriwave/widgets/pluri_station_art_fallback.dart'; + +/// Issue 6 (feedback-pruebas): the shared station-art fallback extracted +/// from `TarjetaEmisora`'s original `_fallbackArtFor`. The formula and asset +/// order are pinned exactly — `navegacion_auto_test.dart`'s +/// `indiceArtePara`/`artUriPara` tests mirror the SAME formula independently +/// (protected file, empty diff vs main) and must keep agreeing with this +/// widget without either suite importing the other. +void main() { + Widget host(Widget child) { + return MaterialApp( + locale: const Locale('en'), + localizationsDelegates: AppLocalizations.localizationsDelegates, + supportedLocales: AppLocalizations.supportedLocales, + home: Scaffold(body: SizedBox(width: 60, height: 60, child: child)), + ); + } + + test('artFor reproduces the canonical aurora/cosmic/pulse/nova order ' + '(same formula as navegacion_auto.dart\'s indiceArtePara)', () { + // Single-letter seeds whose codeUnit % 4 covers all 4 indices, mirroring + // navegacion_auto_test.dart's own fixture exactly: 'd'(100)->0 aurora, + // 'a'(97)->1 cosmic, 'b'(98)->2 pulse, 'c'(99)->3 nova. + expect( + PluriStationArtFallback.artFor('d'), + 'assets/images/station_art_aurora.png', + ); + expect( + PluriStationArtFallback.artFor('a'), + 'assets/images/station_art_cosmic.png', + ); + expect( + PluriStationArtFallback.artFor('b'), + 'assets/images/station_art_pulse.png', + ); + expect( + PluriStationArtFallback.artFor('c'), + 'assets/images/station_art_nova.png', + ); + }); + + test('artFor is deterministic for a given seed (same station always picks ' + 'the same art)', () { + const seed = 'uuid-1234-abcd-real-looking'; + expect( + PluriStationArtFallback.artFor(seed), + PluriStationArtFallback.artFor(seed), + ); + }); + + testWidgets( + 'renders the deterministic station-art asset plus a centred player glyph', + (tester) async { + await tester.pumpWidget( + host(const PluriStationArtFallback(seed: 'd', iconSize: 22)), + ); + await tester.pump(); + + // Scoped by asset-name prefix, not `find.byType(Image).first` — the + // player glyph (`PluriIcon`) ALSO renders via its own `Image.asset` + // internally, so a bare type match would be ambiguous. + final arte = tester + .widgetList(find.byType(Image)) + .firstWhere( + (img) => + img.image is AssetImage && + (img.image as AssetImage).assetName.startsWith( + 'assets/images/station_art_', + ), + ); + expect( + (arte.image as AssetImage).assetName, + 'assets/images/station_art_aurora.png', + ); + + final icono = tester.widget(find.byType(PluriIcon)); + expect(icono.glyph, PluriIconGlyph.player); + expect(icono.variant, PluriIconVariant.activeGlow); + expect(icono.size, 22); + }, + ); +} diff --git a/test/widgets/tarjeta_emisora_thumbnail_test.dart b/test/widgets/tarjeta_emisora_thumbnail_test.dart index 34eda32..52f5dab 100644 --- a/test/widgets/tarjeta_emisora_thumbnail_test.dart +++ b/test/widgets/tarjeta_emisora_thumbnail_test.dart @@ -2,6 +2,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:pluriwave/estado/estado_radio.dart'; import 'package:pluriwave/l10n/gen/app_localizations.dart'; +import 'package:pluriwave/widgets/pluri_station_art_fallback.dart'; import 'package:pluriwave/widgets/tarjeta_emisora.dart'; import 'package:provider/provider.dart'; import 'package:shared_preferences/shared_preferences.dart'; @@ -73,6 +74,24 @@ void main() { expect(tester.getSize(thumbnailClip), const Size(48, 48)); }); + testWidgets( + 'issue 6 (feedback-pruebas): a station with no favicon renders via the ' + 'shared PluriStationArtFallback (extracted from this exact fallback)', + (tester) async { + await tester.pumpWidget( + host( + TarjetaEmisora( + emisora: emisoraDemo(uuid: 'sin-arte', nombre: 'Sin Arte'), + esCompacta: true, + ), + ), + ); + await tester.pump(); + + expect(find.byType(PluriStationArtFallback), findsOneWidget); + }, + ); + testWidgets('esCompacta shimmer placeholder is a square block, not a ' 'circle', (tester) async { await tester.pumpWidget(