fix(buscar): rebuild the offline banner with offlineAccent

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.
This commit is contained in:
2026-07-29 23:40:38 +02:00
parent e1505d7aaa
commit 684e8e8ff9
29 changed files with 329 additions and 175 deletions
+202 -162
View File
@@ -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<DecoratedBox>(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<Icon>(
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);
},
);