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), 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 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.value(value: estado), ListenableProvider.value(value: estado.ecualizador), ListenableProvider.value(value: estado.grabacion), ListenableProvider.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 _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 _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 _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 _crearArchivoCustom(List emisoras) async { final nombre = emisoras.isEmpty ? 'emisoras_custom_vacio.json' : 'emisoras_custom_uno.json'; return File('${Directory.current.path}/test/fixtures/$nombre'); } Future _archivoCustomVacio() async => _crearArchivoCustom(const []);