From 684e8e8ff95ab2f24c46cc66b6ed2de430b9cbf2 Mon Sep 17 00:00:00 2001 From: freetlab Date: Wed, 29 Jul 2026 23:40:38 +0200 Subject: [PATCH] fix(buscar): rebuild the offline banner with offlineAccent MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Audit 13.1 (t4 line 641): the banner rendered wifi_off in colorScheme.error (red) inside a generic glass card. Rebuilt using offlineAccent (#E8879A) — a token that existed since Tier 1 with no consumer — for the tint, border and icon, plus the prototype's title/ detail layout and a restyled Retry chip. New offlineBannerTitle key translated to all 13 locales. --- lib/l10n/app_ar.arb | 1 + lib/l10n/app_bn.arb | 1 + lib/l10n/app_de.arb | 1 + lib/l10n/app_en.arb | 1 + lib/l10n/app_es.arb | 1 + lib/l10n/app_fr.arb | 1 + lib/l10n/app_hi.arb | 1 + lib/l10n/app_id.arb | 1 + lib/l10n/app_it.arb | 1 + lib/l10n/app_ja.arb | 1 + lib/l10n/app_pt.arb | 1 + lib/l10n/app_ru.arb | 1 + lib/l10n/app_zh.arb | 1 + lib/l10n/gen/app_localizations.dart | 6 + lib/l10n/gen/app_localizations_ar.dart | 3 + lib/l10n/gen/app_localizations_bn.dart | 3 + lib/l10n/gen/app_localizations_de.dart | 3 + lib/l10n/gen/app_localizations_en.dart | 3 + lib/l10n/gen/app_localizations_es.dart | 3 + lib/l10n/gen/app_localizations_fr.dart | 3 + lib/l10n/gen/app_localizations_hi.dart | 3 + lib/l10n/gen/app_localizations_id.dart | 3 + lib/l10n/gen/app_localizations_it.dart | 3 + lib/l10n/gen/app_localizations_ja.dart | 3 + lib/l10n/gen/app_localizations_pt.dart | 3 + lib/l10n/gen/app_localizations_ru.dart | 3 + lib/l10n/gen/app_localizations_zh.dart | 3 + lib/pantallas/pantalla_buscar.dart | 82 ++++- test/pantallas/pantalla_buscar_test.dart | 364 +++++++++++++---------- 29 files changed, 329 insertions(+), 175 deletions(-) diff --git a/lib/l10n/app_ar.arb b/lib/l10n/app_ar.arb index abe22a8..c9b3ca5 100644 --- a/lib/l10n/app_ar.arb +++ b/lib/l10n/app_ar.arb @@ -306,6 +306,7 @@ "detectAction": "اكتشاف", "liveRadar": "رادار مباشر", "genresTitle": "الأنواع", + "offlineBannerTitle": "لا يوجد اتصال", "retryAction": "إعادة المحاولة", "noStationsAvailable": "لا توجد محطات متاحة", "noStationsAvailableSubtitle": "جرّب التحديث أو اختيار نوع آخر لالتقاط الإشارة من جديد.", diff --git a/lib/l10n/app_bn.arb b/lib/l10n/app_bn.arb index f3a86e0..8c2e783 100644 --- a/lib/l10n/app_bn.arb +++ b/lib/l10n/app_bn.arb @@ -306,6 +306,7 @@ "detectAction": "সনাক্ত করুন", "liveRadar": "লাইভ রাডার", "genresTitle": "ধরন", + "offlineBannerTitle": "সংযোগ নেই", "retryAction": "আবার চেষ্টা করুন", "noStationsAvailable": "কোনো স্টেশন উপলব্ধ নেই", "noStationsAvailableSubtitle": "সিগন্যাল আবার ধরতে রিফ্রেশ করুন বা অন্য ধরন বেছে নিন।", diff --git a/lib/l10n/app_de.arb b/lib/l10n/app_de.arb index 0677b54..5e42509 100644 --- a/lib/l10n/app_de.arb +++ b/lib/l10n/app_de.arb @@ -306,6 +306,7 @@ "detectAction": "Erkennen", "liveRadar": "Live-Radar", "genresTitle": "Genres", + "offlineBannerTitle": "Keine Verbindung", "retryAction": "Erneut versuchen", "noStationsAvailable": "Keine Sender verfügbar", "noStationsAvailableSubtitle": "Versuche zu aktualisieren oder ein anderes Genre auszuwählen, um wieder ein Signal zu empfangen.", diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index 18106d9..81d2f71 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -369,6 +369,7 @@ "detectAction": "Detect", "liveRadar": "Live radar", "genresTitle": "Genres", + "offlineBannerTitle": "No connection", "retryAction": "Retry", "noStationsAvailable": "No stations available", "noStationsAvailableSubtitle": "Try refreshing or choosing another genre to capture a signal again.", diff --git a/lib/l10n/app_es.arb b/lib/l10n/app_es.arb index 4c1dea3..5795fe9 100644 --- a/lib/l10n/app_es.arb +++ b/lib/l10n/app_es.arb @@ -369,6 +369,7 @@ "detectAction": "Detectar", "liveRadar": "Radar en directo", "genresTitle": "Géneros", + "offlineBannerTitle": "Sin conexión", "retryAction": "Reintentar", "noStationsAvailable": "No hay emisoras disponibles", "noStationsAvailableSubtitle": "Probá refrescar o elegir otro género para volver a capturar señal.", diff --git a/lib/l10n/app_fr.arb b/lib/l10n/app_fr.arb index a540e45..963d463 100644 --- a/lib/l10n/app_fr.arb +++ b/lib/l10n/app_fr.arb @@ -306,6 +306,7 @@ "detectAction": "Détecter", "liveRadar": "Radar en direct", "genresTitle": "Genres", + "offlineBannerTitle": "Aucune connexion", "retryAction": "Réessayer", "noStationsAvailable": "Aucune station disponible", "noStationsAvailableSubtitle": "Essayez d’actualiser ou de choisir un autre genre pour capter à nouveau un signal.", diff --git a/lib/l10n/app_hi.arb b/lib/l10n/app_hi.arb index 3868a88..9d9f26f 100644 --- a/lib/l10n/app_hi.arb +++ b/lib/l10n/app_hi.arb @@ -306,6 +306,7 @@ "detectAction": "पहचानें", "liveRadar": "लाइव रडार", "genresTitle": "शैलियाँ", + "offlineBannerTitle": "कोई कनेक्शन नहीं", "retryAction": "फिर कोशिश करें", "noStationsAvailable": "कोई स्टेशन उपलब्ध नहीं", "noStationsAvailableSubtitle": "सिग्नल फिर पकड़ने के लिए रीफ़्रेश करें या दूसरी शैली चुनें।", diff --git a/lib/l10n/app_id.arb b/lib/l10n/app_id.arb index a115be8..8953821 100644 --- a/lib/l10n/app_id.arb +++ b/lib/l10n/app_id.arb @@ -306,6 +306,7 @@ "detectAction": "Deteksi", "liveRadar": "Radar langsung", "genresTitle": "Genre", + "offlineBannerTitle": "Tidak ada koneksi", "retryAction": "Coba lagi", "noStationsAvailable": "Tidak ada stasiun tersedia", "noStationsAvailableSubtitle": "Coba segarkan atau pilih genre lain untuk menangkap sinyal kembali.", diff --git a/lib/l10n/app_it.arb b/lib/l10n/app_it.arb index 525950a..decc030 100644 --- a/lib/l10n/app_it.arb +++ b/lib/l10n/app_it.arb @@ -306,6 +306,7 @@ "detectAction": "Rileva", "liveRadar": "Radar live", "genresTitle": "Generi", + "offlineBannerTitle": "Nessuna connessione", "retryAction": "Riprova", "noStationsAvailable": "Nessuna emittente disponibile", "noStationsAvailableSubtitle": "Prova ad aggiornare o a scegliere un altro genere per captare di nuovo un segnale.", diff --git a/lib/l10n/app_ja.arb b/lib/l10n/app_ja.arb index 90a0691..3cb5cab 100644 --- a/lib/l10n/app_ja.arb +++ b/lib/l10n/app_ja.arb @@ -306,6 +306,7 @@ "detectAction": "検出", "liveRadar": "ライブレーダー", "genresTitle": "ジャンル", + "offlineBannerTitle": "接続なし", "retryAction": "再試行", "noStationsAvailable": "利用できる局がありません", "noStationsAvailableSubtitle": "再読み込みするか別のジャンルを選んで、もう一度電波をつかまえてください。", diff --git a/lib/l10n/app_pt.arb b/lib/l10n/app_pt.arb index 6dee0d4..bdbf31f 100644 --- a/lib/l10n/app_pt.arb +++ b/lib/l10n/app_pt.arb @@ -306,6 +306,7 @@ "detectAction": "Detectar", "liveRadar": "Radar ao vivo", "genresTitle": "Gêneros", + "offlineBannerTitle": "Sem conexão", "retryAction": "Tentar novamente", "noStationsAvailable": "Não há estações disponíveis", "noStationsAvailableSubtitle": "Tente atualizar ou escolher outro gênero para captar sinal novamente.", diff --git a/lib/l10n/app_ru.arb b/lib/l10n/app_ru.arb index 6759703..74ddbf9 100644 --- a/lib/l10n/app_ru.arb +++ b/lib/l10n/app_ru.arb @@ -306,6 +306,7 @@ "detectAction": "Определить", "liveRadar": "Радар в эфире", "genresTitle": "Жанры", + "offlineBannerTitle": "Нет подключения", "retryAction": "Повторить", "noStationsAvailable": "Нет доступных станций", "noStationsAvailableSubtitle": "Попробуйте обновить или выбрать другой жанр, чтобы снова поймать сигнал.", diff --git a/lib/l10n/app_zh.arb b/lib/l10n/app_zh.arb index 58df4c5..5591b9f 100644 --- a/lib/l10n/app_zh.arb +++ b/lib/l10n/app_zh.arb @@ -306,6 +306,7 @@ "detectAction": "检测", "liveRadar": "直播雷达", "genresTitle": "类型", + "offlineBannerTitle": "无连接", "retryAction": "重试", "noStationsAvailable": "没有可用电台", "noStationsAvailableSubtitle": "尝试刷新或选择其他类型,重新捕捉信号。", diff --git a/lib/l10n/gen/app_localizations.dart b/lib/l10n/gen/app_localizations.dart index b5883d4..4f57a64 100644 --- a/lib/l10n/gen/app_localizations.dart +++ b/lib/l10n/gen/app_localizations.dart @@ -1438,6 +1438,12 @@ abstract class AppLocalizations { /// **'Géneros'** String get genresTitle; + /// No description provided for @offlineBannerTitle. + /// + /// In es, this message translates to: + /// **'Sin conexión'** + String get offlineBannerTitle; + /// No description provided for @retryAction. /// /// In es, this message translates to: diff --git a/lib/l10n/gen/app_localizations_ar.dart b/lib/l10n/gen/app_localizations_ar.dart index e700259..8fdf136 100644 --- a/lib/l10n/gen/app_localizations_ar.dart +++ b/lib/l10n/gen/app_localizations_ar.dart @@ -766,6 +766,9 @@ class AppLocalizationsAr extends AppLocalizations { @override String get genresTitle => 'الأنواع'; + @override + String get offlineBannerTitle => 'لا يوجد اتصال'; + @override String get retryAction => 'إعادة المحاولة'; diff --git a/lib/l10n/gen/app_localizations_bn.dart b/lib/l10n/gen/app_localizations_bn.dart index b4f0856..aaee917 100644 --- a/lib/l10n/gen/app_localizations_bn.dart +++ b/lib/l10n/gen/app_localizations_bn.dart @@ -769,6 +769,9 @@ class AppLocalizationsBn extends AppLocalizations { @override String get genresTitle => 'ধরন'; + @override + String get offlineBannerTitle => 'সংযোগ নেই'; + @override String get retryAction => 'আবার চেষ্টা করুন'; diff --git a/lib/l10n/gen/app_localizations_de.dart b/lib/l10n/gen/app_localizations_de.dart index a1d749a..ecb6ffb 100644 --- a/lib/l10n/gen/app_localizations_de.dart +++ b/lib/l10n/gen/app_localizations_de.dart @@ -772,6 +772,9 @@ class AppLocalizationsDe extends AppLocalizations { @override String get genresTitle => 'Genres'; + @override + String get offlineBannerTitle => 'Keine Verbindung'; + @override String get retryAction => 'Erneut versuchen'; diff --git a/lib/l10n/gen/app_localizations_en.dart b/lib/l10n/gen/app_localizations_en.dart index 9d685ad..99c064d 100644 --- a/lib/l10n/gen/app_localizations_en.dart +++ b/lib/l10n/gen/app_localizations_en.dart @@ -765,6 +765,9 @@ class AppLocalizationsEn extends AppLocalizations { @override String get genresTitle => 'Genres'; + @override + String get offlineBannerTitle => 'No connection'; + @override String get retryAction => 'Retry'; diff --git a/lib/l10n/gen/app_localizations_es.dart b/lib/l10n/gen/app_localizations_es.dart index f8a72e1..a7679a0 100644 --- a/lib/l10n/gen/app_localizations_es.dart +++ b/lib/l10n/gen/app_localizations_es.dart @@ -770,6 +770,9 @@ class AppLocalizationsEs extends AppLocalizations { @override String get genresTitle => 'Géneros'; + @override + String get offlineBannerTitle => 'Sin conexión'; + @override String get retryAction => 'Reintentar'; diff --git a/lib/l10n/gen/app_localizations_fr.dart b/lib/l10n/gen/app_localizations_fr.dart index 92d0f1f..3559466 100644 --- a/lib/l10n/gen/app_localizations_fr.dart +++ b/lib/l10n/gen/app_localizations_fr.dart @@ -774,6 +774,9 @@ class AppLocalizationsFr extends AppLocalizations { @override String get genresTitle => 'Genres'; + @override + String get offlineBannerTitle => 'Aucune connexion'; + @override String get retryAction => 'Réessayer'; diff --git a/lib/l10n/gen/app_localizations_hi.dart b/lib/l10n/gen/app_localizations_hi.dart index efc0b7b..e5fc9fd 100644 --- a/lib/l10n/gen/app_localizations_hi.dart +++ b/lib/l10n/gen/app_localizations_hi.dart @@ -766,6 +766,9 @@ class AppLocalizationsHi extends AppLocalizations { @override String get genresTitle => 'शैलियाँ'; + @override + String get offlineBannerTitle => 'कोई कनेक्शन नहीं'; + @override String get retryAction => 'फिर कोशिश करें'; diff --git a/lib/l10n/gen/app_localizations_id.dart b/lib/l10n/gen/app_localizations_id.dart index d57e731..36e8e5d 100644 --- a/lib/l10n/gen/app_localizations_id.dart +++ b/lib/l10n/gen/app_localizations_id.dart @@ -766,6 +766,9 @@ class AppLocalizationsId extends AppLocalizations { @override String get genresTitle => 'Genre'; + @override + String get offlineBannerTitle => 'Tidak ada koneksi'; + @override String get retryAction => 'Coba lagi'; diff --git a/lib/l10n/gen/app_localizations_it.dart b/lib/l10n/gen/app_localizations_it.dart index ed3bf13..6b727a9 100644 --- a/lib/l10n/gen/app_localizations_it.dart +++ b/lib/l10n/gen/app_localizations_it.dart @@ -772,6 +772,9 @@ class AppLocalizationsIt extends AppLocalizations { @override String get genresTitle => 'Generi'; + @override + String get offlineBannerTitle => 'Nessuna connessione'; + @override String get retryAction => 'Riprova'; diff --git a/lib/l10n/gen/app_localizations_ja.dart b/lib/l10n/gen/app_localizations_ja.dart index 4670cb0..5ac92e8 100644 --- a/lib/l10n/gen/app_localizations_ja.dart +++ b/lib/l10n/gen/app_localizations_ja.dart @@ -740,6 +740,9 @@ class AppLocalizationsJa extends AppLocalizations { @override String get genresTitle => 'ジャンル'; + @override + String get offlineBannerTitle => '接続なし'; + @override String get retryAction => '再試行'; diff --git a/lib/l10n/gen/app_localizations_pt.dart b/lib/l10n/gen/app_localizations_pt.dart index 53ba6e4..a5e051b 100644 --- a/lib/l10n/gen/app_localizations_pt.dart +++ b/lib/l10n/gen/app_localizations_pt.dart @@ -769,6 +769,9 @@ class AppLocalizationsPt extends AppLocalizations { @override String get genresTitle => 'Gêneros'; + @override + String get offlineBannerTitle => 'Sem conexão'; + @override String get retryAction => 'Tentar novamente'; diff --git a/lib/l10n/gen/app_localizations_ru.dart b/lib/l10n/gen/app_localizations_ru.dart index 3eeb339..57d7b94 100644 --- a/lib/l10n/gen/app_localizations_ru.dart +++ b/lib/l10n/gen/app_localizations_ru.dart @@ -770,6 +770,9 @@ class AppLocalizationsRu extends AppLocalizations { @override String get genresTitle => 'Жанры'; + @override + String get offlineBannerTitle => 'Нет подключения'; + @override String get retryAction => 'Повторить'; diff --git a/lib/l10n/gen/app_localizations_zh.dart b/lib/l10n/gen/app_localizations_zh.dart index cc655d6..056783f 100644 --- a/lib/l10n/gen/app_localizations_zh.dart +++ b/lib/l10n/gen/app_localizations_zh.dart @@ -737,6 +737,9 @@ class AppLocalizationsZh extends AppLocalizations { @override String get genresTitle => '类型'; + @override + String get offlineBannerTitle => '无连接'; + @override String get retryAction => '重试'; diff --git a/lib/pantallas/pantalla_buscar.dart b/lib/pantallas/pantalla_buscar.dart index e6514a0..5324d8b 100644 --- a/lib/pantallas/pantalla_buscar.dart +++ b/lib/pantallas/pantalla_buscar.dart @@ -7,6 +7,7 @@ import '../estado/estado_radio.dart'; import '../l10n/gen/app_localizations.dart'; import '../modelos/emisora.dart'; import '../tema/pluri_animate.dart'; +import '../tema/pluriwave_theme.dart'; import '../widgets/pluri_glass_surface.dart'; import '../widgets/pluri_icon.dart'; import '../widgets/pluri_layout.dart'; @@ -847,26 +848,81 @@ class _PantallaBuscarState extends State { ); } + // Audit 13.1 (t4 line 641): the prototype's offline banner is a + // rgba(207,102,121,X) tinted card — this consolidates on the ONE + // `offlineAccent` token (`#E8879A`) the proposal named for this exact + // banner (`pluriwave_tokens.dart:77`), which existed unused until now, + // rather than introducing a second near-identical raw hex for the tint. Widget _errorBanner( BuildContext context, String error, ThemeData theme, AppLocalizations l10n, ) { + final tokens = context.pluriTokens; return Padding( - padding: const EdgeInsets.all(16), - child: PluriGlassSurface( - padding: const EdgeInsets.all(12), - child: Row( - children: [ - Icon(Icons.wifi_off, color: theme.colorScheme.error), - const SizedBox(width: 8), - Expanded(child: Text(error)), - TextButton( - onPressed: () => context.read().cargarPopulares(), - child: Text(l10n.retryAction), - ), - ], + padding: const EdgeInsets.symmetric(horizontal: 16), + child: DecoratedBox( + key: const ValueKey('offline-banner'), + decoration: BoxDecoration( + color: tokens.offlineAccent.withValues(alpha: 0.14), + borderRadius: BorderRadius.circular(16), + border: Border.all( + color: tokens.offlineAccent.withValues(alpha: 0.4), + ), + ), + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 15, vertical: 13), + child: Row( + children: [ + Icon(Icons.wifi_off, size: 22, color: tokens.offlineAccent), + const SizedBox(width: 11), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + children: [ + Text( + l10n.offlineBannerTitle, + style: const TextStyle( + fontSize: 13.5, + fontWeight: FontWeight.w800, + ), + ), + Text( + error, + style: TextStyle( + fontSize: 11.5, + color: theme.colorScheme.onSurface.withValues( + alpha: 0.65, + ), + ), + ), + ], + ), + ), + const SizedBox(width: 8), + TextButton( + style: TextButton.styleFrom( + backgroundColor: Colors.white.withValues(alpha: 0.1), + foregroundColor: theme.colorScheme.onSurface, + padding: const EdgeInsets.symmetric( + horizontal: 12, + vertical: 7, + ), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(10), + ), + textStyle: const TextStyle( + fontSize: 12, + fontWeight: FontWeight.w800, + ), + ), + onPressed: () => context.read().cargarPopulares(), + child: Text(l10n.retryAction), + ), + ], + ), ), ), ); diff --git a/test/pantallas/pantalla_buscar_test.dart b/test/pantallas/pantalla_buscar_test.dart index 6d71906..3348aaf 100644 --- a/test/pantallas/pantalla_buscar_test.dart +++ b/test/pantallas/pantalla_buscar_test.dart @@ -9,6 +9,7 @@ import 'package:pluriwave/estado/estado_radio.dart'; import 'package:pluriwave/l10n/gen/app_localizations.dart'; import 'package:pluriwave/pantallas/pantalla_buscar.dart'; import 'package:pluriwave/pantallas/pantalla_favoritos.dart'; +import 'package:pluriwave/tema/pluriwave_theme.dart'; import 'package:pluriwave/widgets/tarjeta_emisora.dart'; import 'package:provider/provider.dart'; import 'package:shared_preferences/shared_preferences.dart'; @@ -25,33 +26,30 @@ void main() { }); group('Buscar — landing state (empty query shows discovery content)', () { - testWidgets( - 'consulta vacia muestra cerca de vos, generos y tendencias; ' - 'el grid de resultados de busqueda no aparece', - (tester) async { - _setLargeSurfaceSize(tester); - final estado = _crearEstado( - radio: FakeServicioRadio( - populares: [emisoraDemo(uuid: 'pop-1', nombre: 'Populares Uno')], - tendencias: [emisoraDemo(uuid: 'tr-1', nombre: 'Tendencia Uno')], - ), - ); - addTearDown(estado.dispose); - await tester.runAsync(estado.inicializar); + testWidgets('consulta vacia muestra cerca de vos, generos y tendencias; ' + 'el grid de resultados de busqueda no aparece', (tester) async { + _setLargeSurfaceSize(tester); + final estado = _crearEstado( + radio: FakeServicioRadio( + populares: [emisoraDemo(uuid: 'pop-1', nombre: 'Populares Uno')], + tendencias: [emisoraDemo(uuid: 'tr-1', nombre: 'Tendencia Uno')], + ), + ); + addTearDown(estado.dispose); + await tester.runAsync(estado.inicializar); - await tester.pumpWidget(_conProviders(estado, _testApp())); - await _pumpStableFrame(tester); + await tester.pumpWidget(_conProviders(estado, _testApp())); + await _pumpStableFrame(tester); - final l10n = _l10nDe(tester); - expect(find.text(l10n.nearYou), findsOneWidget); - expect(find.text(l10n.liveRadar), findsOneWidget); - expect(find.text(l10n.genresTitle), findsOneWidget); - expect(find.text('Populares Uno'), findsOneWidget); - // The search-results empty state must NOT show — this is the - // landing state, not "you searched and got nothing". - expect(find.text(l10n.searchEmptyTitle), findsNothing); - }, - ); + final l10n = _l10nDe(tester); + expect(find.text(l10n.nearYou), findsOneWidget); + expect(find.text(l10n.liveRadar), findsOneWidget); + expect(find.text(l10n.genresTitle), findsOneWidget); + expect(find.text('Populares Uno'), findsOneWidget); + // The search-results empty state must NOT show — this is the + // landing state, not "you searched and got nothing". + expect(find.text(l10n.searchEmptyTitle), findsNothing); + }); testWidgets( 'escribir una consulta reemplaza el contenido de descubrimiento por ' @@ -110,38 +108,37 @@ void main() { }, ); - testWidgets( - 'quitar un pill vuelve a buscar sin ese filtro', - (tester) async { - _setLargeSurfaceSize(tester); - final estado = _crearEstado( - radio: FakeServicioRadio( - busqueda: [emisoraDemo(uuid: 'es-1', nombre: 'Radio Espana')], - ), - ); - addTearDown(estado.dispose); - await tester.runAsync(estado.inicializar); + testWidgets('quitar un pill vuelve a buscar sin ese filtro', ( + tester, + ) async { + _setLargeSurfaceSize(tester); + final estado = _crearEstado( + radio: FakeServicioRadio( + busqueda: [emisoraDemo(uuid: 'es-1', nombre: 'Radio Espana')], + ), + ); + addTearDown(estado.dispose); + await tester.runAsync(estado.inicializar); - await tester.pumpWidget(_conProviders(estado, _testApp())); - await _pumpStableFrame(tester); + await tester.pumpWidget(_conProviders(estado, _testApp())); + await _pumpStableFrame(tester); - final l10n = _l10nDe(tester); - await _abrirYSeleccionarPais(tester, l10n.countrySpain); - await _pumpStableFrame(tester); - expect(find.text(l10n.countrySpain), findsOneWidget); + final l10n = _l10nDe(tester); + await _abrirYSeleccionarPais(tester, l10n.countrySpain); + await _pumpStableFrame(tester); + expect(find.text(l10n.countrySpain), findsOneWidget); - final pillPais = find.ancestor( - of: find.text(l10n.countrySpain), - matching: find.byType(Chip), - ); - await tester.tap( - find.descendant(of: pillPais, matching: find.byIcon(Icons.close)), - ); - await _pumpStableFrame(tester); + final pillPais = find.ancestor( + of: find.text(l10n.countrySpain), + matching: find.byType(Chip), + ); + await tester.tap( + find.descendant(of: pillPais, matching: find.byIcon(Icons.close)), + ); + await _pumpStableFrame(tester); - expect(find.text(l10n.countrySpain), findsNothing); - }, - ); + expect(find.text(l10n.countrySpain), findsNothing); + }); testWidgets( 'dos filtros activos y cero resultados ofrecen quitar ambos de una ' @@ -207,128 +204,150 @@ void main() { }, ); - testWidgets( - 'elegir un criterio reordena la pagina actual sin una nueva ' - 'solicitud de red con parametro order', - (tester) async { - _setLargeSurfaceSize(tester); - final radio = FakeServicioRadio( - busqueda: [ - emisoraDemo(uuid: 'alta', nombre: 'Estacion Alta') - .copyWith(bitrate: 320, votes: 1), - emisoraDemo(uuid: 'popular', nombre: 'Estacion Popular') - .copyWith(bitrate: 64, votes: 900), - ], - ); - final estado = _crearEstado(radio: radio); - addTearDown(estado.dispose); - await tester.runAsync(estado.inicializar); + testWidgets('elegir un criterio reordena la pagina actual sin una nueva ' + 'solicitud de red con parametro order', (tester) async { + _setLargeSurfaceSize(tester); + final radio = FakeServicioRadio( + busqueda: [ + emisoraDemo( + uuid: 'alta', + nombre: 'Estacion Alta', + ).copyWith(bitrate: 320, votes: 1), + emisoraDemo( + uuid: 'popular', + nombre: 'Estacion Popular', + ).copyWith(bitrate: 64, votes: 900), + ], + ); + final estado = _crearEstado(radio: radio); + addTearDown(estado.dispose); + await tester.runAsync(estado.inicializar); - await tester.pumpWidget(_conProviders(estado, _testApp())); - await _pumpStableFrame(tester); - await tester.enterText(find.byType(SearchBar), 'estacion'); - await tester.testTextInput.receiveAction(TextInputAction.done); - await _pumpStableFrame(tester); + await tester.pumpWidget(_conProviders(estado, _testApp())); + await _pumpStableFrame(tester); + await tester.enterText(find.byType(SearchBar), 'estacion'); + await tester.testTextInput.receiveAction(TextInputAction.done); + await _pumpStableFrame(tester); - // Default global ordering is OrdenEmisoras.calidad (bitrate desc): - // "Estacion Alta" (320 kbps) must render above "Estacion Popular". - final yAltaAntes = tester.getTopLeft(find.text('Estacion Alta')).dy; - final yPopularAntes = tester.getTopLeft( - find.text('Estacion Popular'), - ).dy; - expect(yAltaAntes, lessThan(yPopularAntes)); + // Default global ordering is OrdenEmisoras.calidad (bitrate desc): + // "Estacion Alta" (320 kbps) must render above "Estacion Popular". + final yAltaAntes = tester.getTopLeft(find.text('Estacion Alta')).dy; + final yPopularAntes = tester.getTopLeft(find.text('Estacion Popular')).dy; + expect(yAltaAntes, lessThan(yPopularAntes)); - final llamadasAntes = radio.buscarCalls; + final llamadasAntes = radio.buscarCalls; - final l10n = _l10nDe(tester); - await tester.tap(find.byIcon(Icons.swap_vert_rounded)); - await tester.pumpAndSettle(); - await tester.tap(find.text(l10n.stationOrderByPopularity)); - await _pumpStableFrame(tester); + final l10n = _l10nDe(tester); + await tester.tap(find.byIcon(Icons.swap_vert_rounded)); + await tester.pumpAndSettle(); + await tester.tap(find.text(l10n.stationOrderByPopularity)); + await _pumpStableFrame(tester); - expect( - radio.buscarCalls, - llamadasAntes, - reason: 'sorting must not trigger a new /json request', - ); + expect( + radio.buscarCalls, + llamadasAntes, + reason: 'sorting must not trigger a new /json request', + ); - final yAltaDespues = tester.getTopLeft(find.text('Estacion Alta')).dy; - final yPopularDespues = tester.getTopLeft( - find.text('Estacion Popular'), - ).dy; - expect(yPopularDespues, lessThan(yAltaDespues)); - }, - ); + final yAltaDespues = tester.getTopLeft(find.text('Estacion Alta')).dy; + final yPopularDespues = + tester.getTopLeft(find.text('Estacion Popular')).dy; + expect(yPopularDespues, lessThan(yAltaDespues)); + }); }); group('Buscar — contenido relocalizado desde PantallaInicio (WU6 6.5)', () { - testWidgets( - 'el grid de descubrimiento muestra custom + populares; tocar ' - 'reproduce via EstadoRadio y el boton de favorito usa el flujo ' - 'existente', - (tester) async { - _setLargeSurfaceSize(tester); - final audio = FakeServicioAudio(); - final favoritos = FakeServicioFavoritos(); - final radio = FakeServicioRadio(); - final custom = emisoraDemo(uuid: 'custom-1', nombre: 'Custom Uno'); - final archivo = await _crearArchivoCustom([custom]); - final estado = _crearEstado( - audio: audio, - favoritos: favoritos, - radio: radio, - resolverArchivoCustom: () async => archivo, - ); - addTearDown(estado.dispose); - await tester.runAsync(estado.inicializar); + testWidgets('el grid de descubrimiento muestra custom + populares; tocar ' + 'reproduce via EstadoRadio y el boton de favorito usa el flujo ' + 'existente', (tester) async { + _setLargeSurfaceSize(tester); + final audio = FakeServicioAudio(); + final favoritos = FakeServicioFavoritos(); + final radio = FakeServicioRadio(); + final custom = emisoraDemo(uuid: 'custom-1', nombre: 'Custom Uno'); + final archivo = await _crearArchivoCustom([custom]); + final estado = _crearEstado( + audio: audio, + favoritos: favoritos, + radio: radio, + resolverArchivoCustom: () async => archivo, + ); + addTearDown(estado.dispose); + await tester.runAsync(estado.inicializar); - await tester.pumpWidget(_conProviders(estado, _testApp())); - await _pumpStableFrame(tester); + await tester.pumpWidget(_conProviders(estado, _testApp())); + await _pumpStableFrame(tester); - expect(find.text('Custom Uno'), findsOneWidget); + expect(find.text('Custom Uno'), findsOneWidget); - await tester.tap(find.text('Custom Uno')); - await _pumpStableFrame(tester); - expect( - audio.emisorasReproducidas.map((e) => e.uuid), - contains('custom-1'), - ); - expect(radio.ultimoUuidClick, 'custom-1'); + await tester.tap(find.text('Custom Uno')); + await _pumpStableFrame(tester); + expect( + audio.emisorasReproducidas.map((e) => e.uuid), + contains('custom-1'), + ); + expect(radio.ultimoUuidClick, 'custom-1'); - final tarjetaCustom = find.ancestor( - of: find.text('Custom Uno'), - matching: find.byType(TarjetaEmisora), - ); - final botonFavorito = - find - .descendant(of: tarjetaCustom, matching: find.byType(InkWell)) - .last; - expect(botonFavorito, findsOneWidget); + final tarjetaCustom = find.ancestor( + of: find.text('Custom Uno'), + matching: find.byType(TarjetaEmisora), + ); + final botonFavorito = + find + .descendant(of: tarjetaCustom, matching: find.byType(InkWell)) + .last; + expect(botonFavorito, findsOneWidget); - await tester.ensureVisible(botonFavorito); - await _pumpStableFrame(tester); - await tester.tap(botonFavorito); - await _pumpStableFrame(tester); + await tester.ensureVisible(botonFavorito); + await _pumpStableFrame(tester); + await tester.tap(botonFavorito); + await _pumpStableFrame(tester); - expect(favoritos.toggleCalls, 1); - expect(await favoritos.esFavorito(custom.uuid), isTrue); - }, - ); + expect(favoritos.toggleCalls, 1); + expect(await favoritos.esFavorito(custom.uuid), isTrue); + }); + + testWidgets('permite reintentar manualmente tras fallo inicial agotado', ( + tester, + ) async { + _setLargeSurfaceSize(tester); + final radio = FakeServicioRadio( + erroresPopularesPorLlamada: [Exception('sin red')], + popularesPorLlamada: [ + const [], + [emisoraDemo(uuid: 'api-1', nombre: 'API Uno')], + ], + tendenciasPorLlamada: [ + const [], + [emisoraDemo(uuid: 'trend-1', nombre: 'Trend Uno')], + ], + ); + final estado = _crearEstado(radio: radio); + addTearDown(estado.dispose); + await tester.runAsync(estado.inicializar); + + await tester.pumpWidget(_conProviders(estado, _testApp())); + await _pumpStableFrame(tester); + + expect(find.text('Sin conexión a la API de radio'), findsOneWidget); + final l10n = _l10nDe(tester); + expect(find.text(l10n.retryAction), findsOneWidget); + + await tester.tap(find.text(l10n.retryAction)); + await _pumpStableFrame(tester); + + expect(radio.obtenerPopularesCalls, 2); + expect(find.text('Sin conexión a la API de radio'), findsNothing); + expect(find.text('API Uno'), findsOneWidget); + }); testWidgets( - 'permite reintentar manualmente tras fallo inicial agotado', + 'visual fidelity (audit 13.1, proto t4 line 641): banner sin conexion ' + 'usa offlineAccent, no colorScheme.error', (tester) async { _setLargeSurfaceSize(tester); final radio = FakeServicioRadio( erroresPopularesPorLlamada: [Exception('sin red')], - popularesPorLlamada: [ - const [], - [emisoraDemo(uuid: 'api-1', nombre: 'API Uno')], - ], - tendenciasPorLlamada: [ - const [], - [emisoraDemo(uuid: 'trend-1', nombre: 'Trend Uno')], - ], ); final estado = _crearEstado(radio: radio); addTearDown(estado.dispose); @@ -337,16 +356,37 @@ void main() { await tester.pumpWidget(_conProviders(estado, _testApp())); await _pumpStableFrame(tester); - expect(find.text('Sin conexión a la API de radio'), findsOneWidget); + final banner = find.byKey(const ValueKey('offline-banner')); + expect(banner, findsOneWidget); + final contexto = tester.element(banner); + final tokens = contexto.pluriTokens; + final errorColor = Theme.of(contexto).colorScheme.error; + + final caja = tester.widget(banner); + final decoracion = caja.decoration as BoxDecoration; + expect( + decoracion.color, + tokens.offlineAccent.withValues(alpha: 0.14), + reason: + 'prototype t4 line 641: rgba(207,102,121,.14) — this app ' + 'consolidates on the offlineAccent token (#E8879A)', + ); + expect(decoracion.color, isNot(errorColor)); + expect( + decoracion.borderRadius, + BorderRadius.circular(16), + reason: 'prototype t4 line 641: border-radius:16px', + ); + + final icono = tester.widget( + find.descendant(of: banner, matching: find.byIcon(Icons.wifi_off)), + ); + expect(icono.color, tokens.offlineAccent); + expect(icono.color, isNot(errorColor)); + final l10n = _l10nDe(tester); + expect(find.text(l10n.offlineBannerTitle), findsOneWidget); expect(find.text(l10n.retryAction), findsOneWidget); - - await tester.tap(find.text(l10n.retryAction)); - await _pumpStableFrame(tester); - - expect(radio.obtenerPopularesCalls, 2); - expect(find.text('Sin conexión a la API de radio'), findsNothing); - expect(find.text('API Uno'), findsOneWidget); }, );