fix(paises,buscar): tappable ISO rows and a reconnect card
Item 24 / audit 5.1-5.3, 5.5 (t4:255-269): Paises' "Tus idiomas" was a Wrap of non-interactive Chips, and the full list was a plain ListTile with no ISO column and no onTap. Both now share one tappable row (ISO code, name, station count, chevron); the first "Tus idiomas" row gets the prototype's teal-tinted highlight. The one production call site wires the tap to filter Buscar by that country's code and pop back -- EstadoBusqueda.buscar(pais: ...) already accepts any ISO alpha-2 code, not just the ~10 presets in the filter sheet. Item 25 / audit 13.2 (t4:643-646): a reconnect card (rotating ring, station name, "Reconectando...", a stop affordance) replaces the complete absence of any reconnect signal outside a word in the mini player. Ships WITHOUT the prototype's attempt counter: the only live ControladorReconexion instance is a private field of PluriWaveAudioHandler inside servicio_audio.dart, a file this task requires stay byte-identical to main, and nothing else re-exposes it. Reconstructing a count from estadoStream's reconectando emissions would not be faithful (the stream can emit it many times per actual backoff attempt), so that was deliberately not attempted.
This commit is contained in:
@@ -7,11 +7,14 @@ import 'package:pluriwave/estado/estado_ecualizador.dart';
|
||||
import 'package:pluriwave/estado/estado_grabacion.dart';
|
||||
import 'package:pluriwave/estado/estado_radio.dart';
|
||||
import 'package:pluriwave/l10n/gen/app_localizations.dart';
|
||||
import 'package:pluriwave/modelos/pais_radio.dart';
|
||||
import 'package:pluriwave/pantallas/pantalla_buscar.dart';
|
||||
import 'package:pluriwave/pantallas/pantalla_favoritos.dart';
|
||||
import 'package:pluriwave/pantallas/pantalla_paises.dart';
|
||||
import 'package:pluriwave/servicios/servicio_audio.dart';
|
||||
import 'package:pluriwave/tema/pluriwave_theme.dart';
|
||||
import 'package:pluriwave/tema/pluriwave_tokens.dart';
|
||||
import 'package:pluriwave/widgets/fila_emisora_plana.dart';
|
||||
import 'package:pluriwave/widgets/tarjeta_emisora.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
@@ -541,6 +544,46 @@ void main() {
|
||||
expect(find.byType(PantallaPaises), findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets(
|
||||
'Item 24: tapping a country row in Paises pops back and filters '
|
||||
'Buscar by that ISO code',
|
||||
(tester) async {
|
||||
_setLargeSurfaceSize(tester);
|
||||
final estado = _crearEstado(
|
||||
radio: FakeServicioRadio(
|
||||
paises: const [
|
||||
PaisRadio(
|
||||
nombre: 'Kazakhstan',
|
||||
codigoIso: 'KZ',
|
||||
numeroEmisoras: 12,
|
||||
),
|
||||
],
|
||||
busqueda: [emisoraDemo(uuid: 'kz-1', nombre: 'Radio Kazajstan')],
|
||||
),
|
||||
);
|
||||
addTearDown(estado.dispose);
|
||||
await tester.runAsync(estado.inicializar);
|
||||
|
||||
await tester.pumpWidget(_conProviders(estado, _testApp()));
|
||||
await _pumpStableFrame(tester);
|
||||
|
||||
await tester.tap(find.byKey(const Key('explore-cell-paises')));
|
||||
await _pumpStableFrame(tester);
|
||||
expect(find.byType(PantallaPaises), findsOneWidget);
|
||||
|
||||
await tester.tap(find.text('Kazakhstan'));
|
||||
await _pumpStableFrame(tester);
|
||||
|
||||
expect(
|
||||
find.byType(PantallaPaises),
|
||||
findsNothing,
|
||||
reason: 'selecting a country pops back to Buscar',
|
||||
);
|
||||
expect(find.text('Radio Kazajstan'), findsOneWidget);
|
||||
expect(estado.busqueda.resultados.map((e) => e.uuid), contains('kz-1'));
|
||||
},
|
||||
);
|
||||
|
||||
testWidgets(
|
||||
'tapping Generos opens a picker sheet; selecting a genre closes it '
|
||||
'and filters the discovery grid (same capability, relocated)',
|
||||
@@ -621,6 +664,177 @@ void main() {
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
// Item 23 / audit 6.5 + 6.6 (t4:302-306): flat, background-less search-
|
||||
// result rows with a square thumbnail, a favourite toggle, and a circular
|
||||
// play affordance -- replacing the full glass TarjetaEmisora card. Rows
|
||||
// sit back-to-back (no 10px separator), matching t4:299's bare column.
|
||||
group('Item 23 -- flat search-result rows (audit 6.5, 6.6)', () {
|
||||
testWidgets(
|
||||
'each result is a flat FilaEmisoraPlana with a "genre - country - '
|
||||
'kbps" meta line, not the full glass card',
|
||||
(tester) async {
|
||||
_setLargeSurfaceSize(tester);
|
||||
final estado = _crearEstado(
|
||||
radio: FakeServicioRadio(
|
||||
busqueda: [
|
||||
emisoraDemo(
|
||||
uuid: 'r-1',
|
||||
nombre: 'Radio Uno',
|
||||
).copyWith(tags: 'Rock', pais: 'Spain', bitrate: 128),
|
||||
],
|
||||
),
|
||||
);
|
||||
addTearDown(estado.dispose);
|
||||
await tester.runAsync(estado.inicializar);
|
||||
|
||||
await tester.pumpWidget(_conProviders(estado, _testApp()));
|
||||
await _pumpStableFrame(tester);
|
||||
await tester.enterText(find.byType(SearchBar), 'radio');
|
||||
await tester.testTextInput.receiveAction(TextInputAction.done);
|
||||
await _pumpStableFrame(tester);
|
||||
|
||||
expect(find.byType(FilaEmisoraPlana), findsOneWidget);
|
||||
expect(
|
||||
find.byType(TarjetaEmisora),
|
||||
findsNothing,
|
||||
reason: 'audit 6.5 replaces the full glass card with a flat row',
|
||||
);
|
||||
expect(find.text('Rock · Spain · 128 kbps'), findsOneWidget);
|
||||
expect(find.byType(BotonFavoritoEmisora), findsOneWidget);
|
||||
expect(find.byType(BotonReproducirCircular), findsOneWidget);
|
||||
},
|
||||
);
|
||||
|
||||
testWidgets('rows sit back-to-back, with no 10px separator (t4:299)', (
|
||||
tester,
|
||||
) async {
|
||||
_setLargeSurfaceSize(tester);
|
||||
final estado = _crearEstado(
|
||||
radio: FakeServicioRadio(
|
||||
busqueda: [
|
||||
emisoraDemo(uuid: 'r-1', nombre: 'Radio Uno'),
|
||||
emisoraDemo(uuid: 'r-2', nombre: 'Radio Dos'),
|
||||
],
|
||||
),
|
||||
);
|
||||
addTearDown(estado.dispose);
|
||||
await tester.runAsync(estado.inicializar);
|
||||
|
||||
await tester.pumpWidget(_conProviders(estado, _testApp()));
|
||||
await _pumpStableFrame(tester);
|
||||
await tester.enterText(find.byType(SearchBar), 'radio');
|
||||
await tester.testTextInput.receiveAction(TextInputAction.done);
|
||||
await _pumpStableFrame(tester);
|
||||
|
||||
final filas = find.byType(FilaEmisoraPlana);
|
||||
expect(filas, findsNWidgets(2));
|
||||
final primeraAbajo = tester.getBottomLeft(filas.at(0)).dy;
|
||||
final segundaArriba = tester.getTopLeft(filas.at(1)).dy;
|
||||
expect(
|
||||
segundaArriba - primeraAbajo,
|
||||
closeTo(0, 0.5),
|
||||
reason:
|
||||
't4:299 rows have no separator; each keeps only its own '
|
||||
'8px padding',
|
||||
);
|
||||
});
|
||||
|
||||
testWidgets(
|
||||
'tapping the favourite toggle on a search result adds it to favorites',
|
||||
(tester) async {
|
||||
_setLargeSurfaceSize(tester);
|
||||
final estado = _crearEstado(
|
||||
radio: FakeServicioRadio(
|
||||
busqueda: [emisoraDemo(uuid: 'r-1', nombre: 'Radio Uno')],
|
||||
),
|
||||
);
|
||||
addTearDown(estado.dispose);
|
||||
await tester.runAsync(estado.inicializar);
|
||||
|
||||
await tester.pumpWidget(_conProviders(estado, _testApp()));
|
||||
await _pumpStableFrame(tester);
|
||||
await tester.enterText(find.byType(SearchBar), 'radio');
|
||||
await tester.testTextInput.receiveAction(TextInputAction.done);
|
||||
await _pumpStableFrame(tester);
|
||||
|
||||
expect(find.byIcon(Icons.favorite_outline_rounded), findsOneWidget);
|
||||
await tester.tap(find.byIcon(Icons.favorite_outline_rounded));
|
||||
await _pumpStableFrame(tester);
|
||||
|
||||
expect(await estado.esFavorito('r-1'), isTrue);
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
// Item 25 / audit 13.2 (t4:643-646): a reconnect card with a rotating
|
||||
// ring, the station name, and a stop affordance -- previously the ONLY
|
||||
// signal of a reconnect was a word in the mini player.
|
||||
//
|
||||
// Hazard: `pumpAndSettle()` never terminates while this card's rotating
|
||||
// ring animates -- every assertion below uses a bounded `pump()` once
|
||||
// `reconectando` is emitted, never `_pumpStableFrame`'s `pumpAndSettle`.
|
||||
group('Item 25 -- reconnect card (audit 13.2)', () {
|
||||
testWidgets(
|
||||
'shows the station name, "Reconectando...", and a stop button while '
|
||||
'reconnecting',
|
||||
(tester) async {
|
||||
_setLargeSurfaceSize(tester);
|
||||
final audio = FakeServicioAudio();
|
||||
final estado = _crearEstado(audio: audio);
|
||||
addTearDown(estado.dispose);
|
||||
await tester.runAsync(estado.inicializar);
|
||||
await audio.reproducir(emisoraDemo(uuid: 'r1', nombre: 'Radio Uno'));
|
||||
|
||||
await tester.pumpWidget(_conProviders(estado, _testApp()));
|
||||
await _pumpStableFrame(tester);
|
||||
|
||||
expect(
|
||||
find.byIcon(Icons.stop_circle_rounded),
|
||||
findsNothing,
|
||||
reason: 'not reconnecting yet -- the card must not show',
|
||||
);
|
||||
|
||||
audio.emitirEstado(EstadoReproduccion.reconectando);
|
||||
await tester.pump();
|
||||
await tester.pump();
|
||||
|
||||
expect(find.text('Radio Uno'), findsOneWidget);
|
||||
final l10n = _l10nDe(tester);
|
||||
expect(find.text(l10n.playbackStatusReconnecting), findsOneWidget);
|
||||
expect(find.byIcon(Icons.stop_circle_rounded), findsOneWidget);
|
||||
},
|
||||
);
|
||||
|
||||
testWidgets('tapping stop calls EstadoRadio.detenerReproduccion', (
|
||||
tester,
|
||||
) async {
|
||||
_setLargeSurfaceSize(tester);
|
||||
final audio = FakeServicioAudio();
|
||||
final estado = _crearEstado(audio: audio);
|
||||
addTearDown(estado.dispose);
|
||||
await tester.runAsync(estado.inicializar);
|
||||
await audio.reproducir(emisoraDemo(uuid: 'r1', nombre: 'Radio Uno'));
|
||||
|
||||
await tester.pumpWidget(_conProviders(estado, _testApp()));
|
||||
await _pumpStableFrame(tester);
|
||||
|
||||
audio.emitirEstado(EstadoReproduccion.reconectando);
|
||||
await tester.pump();
|
||||
await tester.pump();
|
||||
|
||||
await tester.tap(find.byIcon(Icons.stop_circle_rounded));
|
||||
await tester.pump();
|
||||
await tester.pump();
|
||||
|
||||
// `EstadoRadio.emisoraActual` deliberately keeps showing the last
|
||||
// selected station even once stopped (an existing, unrelated
|
||||
// property of `_emisoraSeleccionada`'s tracking) -- the real,
|
||||
// user-visible effect of tapping stop is that this card disappears,
|
||||
// since it only renders while `reconectando`.
|
||||
expect(find.byKey(const ValueKey('tarjeta-reconectando')), findsNothing);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
EstadoRadio _crearEstado({
|
||||
|
||||
@@ -66,9 +66,12 @@ void main() {
|
||||
|
||||
// Full alphabetical list: all 3 fetched countries render.
|
||||
expect(find.text('Argentina'), findsOneWidget);
|
||||
expect(find.text('France'), findsOneWidget);
|
||||
// 'Spain' renders twice: "Tus idiomas" (locale es -> representative
|
||||
// country ES) AND the full list below.
|
||||
// 'France' and 'Spain' each render twice: once in "Tus idiomas" (FR
|
||||
// and ES both back a supported locale) and once in the full list
|
||||
// below. Item 24's row now shows the name as its OWN Text (not
|
||||
// concatenated with the count into one Chip label), so both are
|
||||
// independently findable in each section.
|
||||
expect(find.text('France'), findsWidgets);
|
||||
expect(find.text('Spain'), findsWidgets);
|
||||
|
||||
// Each entry's parsed `stationcount` renders via the existing
|
||||
@@ -77,10 +80,127 @@ void main() {
|
||||
// as a plain number here, never throwing a cast error.
|
||||
expect(find.text(l10n.stationsCount(482)), findsWidgets);
|
||||
expect(find.text(l10n.stationsCount(120)), findsOneWidget);
|
||||
expect(find.text(l10n.stationsCount(75)), findsOneWidget);
|
||||
// Also doubled, for the same reason as 'France' above.
|
||||
expect(find.text(l10n.stationsCount(75)), findsWidgets);
|
||||
},
|
||||
);
|
||||
|
||||
// Item 24 / audit 5.1-5.3, 5.5 (t4:256-269): tappable ISO rows, not
|
||||
// non-interactive chips/plain ListTiles.
|
||||
group('Item 24 -- tappable ISO rows', () {
|
||||
testWidgets('rows show the ISO code and no longer use Chip or ListTile', (
|
||||
tester,
|
||||
) async {
|
||||
final estado = EstadoBusqueda(
|
||||
radio: FakeServicioRadio(
|
||||
paises: const [
|
||||
PaisRadio(nombre: 'Spain', codigoIso: 'ES', numeroEmisoras: 482),
|
||||
PaisRadio(
|
||||
nombre: 'Argentina',
|
||||
codigoIso: 'AR',
|
||||
numeroEmisoras: 120,
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
addTearDown(estado.dispose);
|
||||
|
||||
await tester.pumpWidget(buildScreen(estado));
|
||||
await pumpEstable(tester);
|
||||
|
||||
expect(
|
||||
find.byType(Chip),
|
||||
findsNothing,
|
||||
reason: 'audit 5.1 replaces the Wrap-of-Chip with tappable rows',
|
||||
);
|
||||
expect(
|
||||
find.byType(ListTile),
|
||||
findsNothing,
|
||||
reason:
|
||||
'audit 5.3 rows need an ISO-code column a plain ListTile '
|
||||
'has no slot for',
|
||||
);
|
||||
expect(find.text('AR'), findsOneWidget);
|
||||
// 'ES' renders in BOTH the "Tus idiomas" and "Todos" rows.
|
||||
expect(find.text('ES'), findsWidgets);
|
||||
expect(find.byIcon(Icons.chevron_right_rounded), findsWidgets);
|
||||
});
|
||||
|
||||
testWidgets('tapping a row invokes onPaisSeleccionado with that country', (
|
||||
tester,
|
||||
) async {
|
||||
final estado = EstadoBusqueda(
|
||||
radio: FakeServicioRadio(
|
||||
paises: const [
|
||||
PaisRadio(
|
||||
nombre: 'Argentina',
|
||||
codigoIso: 'AR',
|
||||
numeroEmisoras: 120,
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
addTearDown(estado.dispose);
|
||||
PaisRadio? seleccionado;
|
||||
|
||||
await tester.pumpWidget(
|
||||
ChangeNotifierProvider<EstadoBusqueda>.value(
|
||||
value: estado,
|
||||
child: MaterialApp(
|
||||
locale: const Locale('es'),
|
||||
localizationsDelegates: AppLocalizations.localizationsDelegates,
|
||||
supportedLocales: AppLocalizations.supportedLocales,
|
||||
home: PantallaPaises(
|
||||
onPaisSeleccionado: (pais) => seleccionado = pais,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
await pumpEstable(tester);
|
||||
|
||||
await tester.tap(find.text('Argentina'));
|
||||
await pumpEstable(tester);
|
||||
|
||||
expect(seleccionado?.codigoIso, 'AR');
|
||||
});
|
||||
|
||||
testWidgets(
|
||||
'the first "Tus idiomas" row is highlighted: teal-tinted background, '
|
||||
'bold name',
|
||||
(tester) async {
|
||||
final estado = EstadoBusqueda(
|
||||
radio: FakeServicioRadio(
|
||||
paises: const [
|
||||
PaisRadio(nombre: 'Spain', codigoIso: 'ES', numeroEmisoras: 482),
|
||||
],
|
||||
),
|
||||
);
|
||||
addTearDown(estado.dispose);
|
||||
|
||||
await tester.pumpWidget(buildScreen(estado));
|
||||
await pumpEstable(tester);
|
||||
|
||||
final nombre = tester.widget<Text>(find.text('Spain').first);
|
||||
expect(nombre.style?.fontWeight, FontWeight.w800);
|
||||
|
||||
final decorado = tester.widget<DecoratedBox>(
|
||||
find
|
||||
.ancestor(
|
||||
of: find.text('Spain').first,
|
||||
matching: find.byType(DecoratedBox),
|
||||
)
|
||||
.first,
|
||||
);
|
||||
final decoration = decorado.decoration as BoxDecoration;
|
||||
expect(
|
||||
decoration.color,
|
||||
isNot(Colors.transparent),
|
||||
reason: 't4:256 the first row is teal-tinted, not transparent',
|
||||
);
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
testWidgets(
|
||||
'cargarPaises no se re-dispara si ya hay datos en caché al reconstruir',
|
||||
(tester) async {
|
||||
|
||||
Reference in New Issue
Block a user