feat(buscar): add discovery landing state, filter pills, counter, and sort
This commit is contained in:
@@ -0,0 +1,480 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:pluriwave/estado/estado_busqueda.dart';
|
||||
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/pantallas/pantalla_buscar.dart';
|
||||
import 'package:pluriwave/pantallas/pantalla_favoritos.dart';
|
||||
import 'package:pluriwave/widgets/tarjeta_emisora.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
|
||||
import '../helpers/fakes.dart';
|
||||
|
||||
/// WU6, `station-discovery-browse` spec: Buscar's landing state (relocated
|
||||
/// discovery content from PantallaInicio, task 6.5), active-filter pills +
|
||||
/// results counter + clear-all-filters (task 6.6), and the client-side
|
||||
/// "Ordenar" control (task 6.7).
|
||||
void main() {
|
||||
setUp(() {
|
||||
SharedPreferences.setMockInitialValues({});
|
||||
});
|
||||
|
||||
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);
|
||||
|
||||
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);
|
||||
},
|
||||
);
|
||||
|
||||
testWidgets(
|
||||
'escribir una consulta reemplaza el contenido de descubrimiento por '
|
||||
'la vista de resultados',
|
||||
(tester) async {
|
||||
_setLargeSurfaceSize(tester);
|
||||
final estado = _crearEstado(
|
||||
radio: FakeServicioRadio(
|
||||
populares: [emisoraDemo(uuid: 'pop-1', nombre: 'Populares Uno')],
|
||||
busqueda: [emisoraDemo(uuid: 'res-1', nombre: 'Resultado Uno')],
|
||||
),
|
||||
);
|
||||
addTearDown(estado.dispose);
|
||||
await tester.runAsync(estado.inicializar);
|
||||
|
||||
await tester.pumpWidget(_conProviders(estado, _testApp()));
|
||||
await _pumpStableFrame(tester);
|
||||
|
||||
final l10n = _l10nDe(tester);
|
||||
expect(find.text(l10n.nearYou), findsOneWidget);
|
||||
|
||||
await tester.enterText(find.byType(SearchBar), 'resultado');
|
||||
await tester.testTextInput.receiveAction(TextInputAction.done);
|
||||
await _pumpStableFrame(tester);
|
||||
|
||||
expect(find.text(l10n.nearYou), findsNothing);
|
||||
expect(find.text('Resultado Uno'), findsOneWidget);
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
group('Buscar — active-filter pills y contador de resultados', () {
|
||||
testWidgets(
|
||||
'aplicar un filtro de pais muestra un pill removible y el contador '
|
||||
'refleja el conteo filtrado',
|
||||
(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);
|
||||
|
||||
final l10n = _l10nDe(tester);
|
||||
await _abrirYSeleccionarPais(tester, l10n.countrySpain);
|
||||
await _pumpStableFrame(tester);
|
||||
|
||||
expect(find.text(l10n.countrySpain), findsOneWidget);
|
||||
expect(find.byIcon(Icons.close), findsWidgets);
|
||||
expect(find.text(l10n.searchResultsCount(1)), findsOneWidget);
|
||||
},
|
||||
);
|
||||
|
||||
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);
|
||||
|
||||
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);
|
||||
|
||||
expect(find.text(l10n.countrySpain), findsNothing);
|
||||
},
|
||||
);
|
||||
|
||||
testWidgets(
|
||||
'dos filtros activos y cero resultados ofrecen quitar ambos de una '
|
||||
'sola vez',
|
||||
(tester) async {
|
||||
_setLargeSurfaceSize(tester);
|
||||
final estado = _crearEstado(radio: FakeServicioRadio(busqueda: []));
|
||||
addTearDown(estado.dispose);
|
||||
await tester.runAsync(estado.inicializar);
|
||||
|
||||
await tester.pumpWidget(_conProviders(estado, _testApp()));
|
||||
await _pumpStableFrame(tester);
|
||||
|
||||
final l10n = _l10nDe(tester);
|
||||
await _abrirYSeleccionarPais(tester, l10n.countrySpain);
|
||||
await _pumpStableFrame(tester);
|
||||
await _abrirYSeleccionarCalidad(tester, '320 kbps');
|
||||
await _pumpStableFrame(tester);
|
||||
|
||||
expect(find.text(l10n.countrySpain), findsOneWidget);
|
||||
final accionQuitar = find.text(l10n.searchClearFiltersAction(2));
|
||||
expect(accionQuitar, findsOneWidget);
|
||||
|
||||
await tester.tap(accionQuitar);
|
||||
await _pumpStableFrame(tester);
|
||||
|
||||
expect(find.text(l10n.countrySpain), findsNothing);
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
group('Buscar — Ordenar (client-side, WU6)', () {
|
||||
testWidgets(
|
||||
'cada opcion renderizada de Ordenar corresponde a un caso real de '
|
||||
'OrdenEmisoras (regresion: nunca una opcion decorativa)',
|
||||
(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);
|
||||
|
||||
await tester.tap(find.byIcon(Icons.swap_vert_rounded));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
final l10n = _l10nDe(tester);
|
||||
expect(
|
||||
find.byType(PopupMenuItem<OrdenEmisoras>),
|
||||
findsNWidgets(OrdenEmisoras.values.length),
|
||||
);
|
||||
expect(find.text(l10n.stationOrderByName), findsOneWidget);
|
||||
expect(find.text(l10n.stationOrderByQuality), findsOneWidget);
|
||||
expect(find.text(l10n.stationOrderByPopularity), findsOneWidget);
|
||||
},
|
||||
);
|
||||
|
||||
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);
|
||||
|
||||
// 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 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',
|
||||
);
|
||||
|
||||
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);
|
||||
|
||||
await tester.pumpWidget(_conProviders(estado, _testApp()));
|
||||
await _pumpStableFrame(tester);
|
||||
|
||||
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');
|
||||
|
||||
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);
|
||||
|
||||
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(
|
||||
'PantallaFavoritos muestra el custom marcado como favorito desde '
|
||||
'Buscar tras recargar',
|
||||
(tester) async {
|
||||
_setLargeSurfaceSize(tester);
|
||||
final favoritos = FakeServicioFavoritos();
|
||||
final custom = emisoraDemo(uuid: 'custom-1', nombre: 'Custom Uno');
|
||||
final archivo = await _crearArchivoCustom([custom]);
|
||||
final estado = _crearEstado(
|
||||
favoritos: favoritos,
|
||||
radio: FakeServicioRadio(
|
||||
populares: [emisoraDemo(uuid: 'api-1', nombre: 'API Uno')],
|
||||
),
|
||||
resolverArchivoCustom: () async => archivo,
|
||||
);
|
||||
addTearDown(estado.dispose);
|
||||
await tester.runAsync(estado.inicializar);
|
||||
|
||||
await tester.pumpWidget(_conProviders(estado, _testApp()));
|
||||
await _pumpStableFrame(tester);
|
||||
|
||||
final tarjetaCustom = find.ancestor(
|
||||
of: find.text('Custom Uno'),
|
||||
matching: find.byType(TarjetaEmisora),
|
||||
);
|
||||
final botonFavorito =
|
||||
find
|
||||
.descendant(of: tarjetaCustom, matching: find.byType(InkWell))
|
||||
.last;
|
||||
await tester.ensureVisible(botonFavorito);
|
||||
await _pumpStableFrame(tester);
|
||||
await tester.tap(botonFavorito);
|
||||
await _pumpStableFrame(tester);
|
||||
|
||||
await tester.pumpWidget(
|
||||
_conProviders(estado, _testApp(child: const PantallaFavoritos())),
|
||||
);
|
||||
await _pumpStableFrame(tester);
|
||||
|
||||
expect(await favoritos.esFavorito(custom.uuid), isTrue);
|
||||
expect(find.text('Custom Uno'), findsOneWidget);
|
||||
},
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
EstadoRadio _crearEstado({
|
||||
FakeServicioAudio? audio,
|
||||
FakeServicioFavoritos? favoritos,
|
||||
FakeServicioRadio? radio,
|
||||
Future<File> Function()? resolverArchivoCustom,
|
||||
}) {
|
||||
return EstadoRadio(
|
||||
audio: audio ?? FakeServicioAudio(),
|
||||
favoritos: favoritos ?? FakeServicioFavoritos(),
|
||||
radio: radio ?? FakeServicioRadio(),
|
||||
servicioEcualizador: FakeServicioEcualizador(),
|
||||
servicioGrabacion: FakeServicioGrabacionRadio(),
|
||||
resolverArchivoCustom: resolverArchivoCustom ?? _archivoCustomVacio,
|
||||
iniciarAutomaticamente: false,
|
||||
);
|
||||
}
|
||||
|
||||
Widget _conProviders(EstadoRadio estado, Widget child) {
|
||||
return MultiProvider(
|
||||
providers: [
|
||||
ChangeNotifierProvider<EstadoRadio>.value(value: estado),
|
||||
ListenableProvider<EstadoEcualizador>.value(value: estado.ecualizador),
|
||||
ListenableProvider<EstadoGrabacion>.value(value: estado.grabacion),
|
||||
ListenableProvider<EstadoBusqueda>.value(value: estado.busqueda),
|
||||
],
|
||||
child: child,
|
||||
);
|
||||
}
|
||||
|
||||
Widget _testApp({Widget child = const PantallaBuscar()}) {
|
||||
return MaterialApp(
|
||||
locale: const Locale('es'),
|
||||
localizationsDelegates: AppLocalizations.localizationsDelegates,
|
||||
supportedLocales: AppLocalizations.supportedLocales,
|
||||
home: Scaffold(body: child),
|
||||
);
|
||||
}
|
||||
|
||||
AppLocalizations _l10nDe(WidgetTester tester) {
|
||||
return AppLocalizations.of(tester.element(find.byType(PantallaBuscar)));
|
||||
}
|
||||
|
||||
Future<void> _abrirYSeleccionarPais(WidgetTester tester, String label) async {
|
||||
final l10n = _l10nDe(tester);
|
||||
await tester.tap(find.text(l10n.searchFiltersLabel).first);
|
||||
await tester.pumpAndSettle();
|
||||
await tester.tap(find.text(label).last);
|
||||
await tester.pumpAndSettle();
|
||||
}
|
||||
|
||||
Future<void> _abrirYSeleccionarCalidad(
|
||||
WidgetTester tester,
|
||||
String label,
|
||||
) async {
|
||||
final l10n = _l10nDe(tester);
|
||||
await tester.tap(find.text(l10n.searchFiltersLabel).first);
|
||||
await tester.pumpAndSettle();
|
||||
await tester.tap(find.text(label).last);
|
||||
await tester.pumpAndSettle();
|
||||
}
|
||||
|
||||
Future<void> _pumpStableFrame(WidgetTester tester) async {
|
||||
await tester.pump();
|
||||
await tester.pumpAndSettle(const Duration(milliseconds: 100));
|
||||
}
|
||||
|
||||
void _setLargeSurfaceSize(WidgetTester tester) {
|
||||
tester.view.physicalSize = const Size(1440, 3200);
|
||||
tester.view.devicePixelRatio = 1.0;
|
||||
addTearDown(tester.view.resetPhysicalSize);
|
||||
addTearDown(tester.view.resetDevicePixelRatio);
|
||||
}
|
||||
|
||||
Future<File> _crearArchivoCustom(List<dynamic> emisoras) async {
|
||||
final nombre =
|
||||
emisoras.isEmpty
|
||||
? 'emisoras_custom_vacio.json'
|
||||
: 'emisoras_custom_uno.json';
|
||||
return File('${Directory.current.path}/test/fixtures/$nombre');
|
||||
}
|
||||
|
||||
Future<File> _archivoCustomVacio() async => _crearArchivoCustom(const []);
|
||||
Reference in New Issue
Block a user