import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:pluriwave/estado/estado_busqueda.dart'; import 'package:pluriwave/l10n/gen/app_localizations.dart'; import 'package:pluriwave/modelos/pais_radio.dart'; import 'package:pluriwave/pantallas/pantalla_paises.dart'; import 'package:pluriwave/widgets/pluri_glass_surface.dart'; import 'package:pluriwave/widgets/pluri_push_scaffold.dart'; import 'package:provider/provider.dart'; import '../helpers/fakes.dart'; /// WU7, `station-discovery-browse` spec — "Países Browser Over the Verified /// Countries Contract": "Tus idiomas" shortlist + the full alphabetical /// list, each entry showing its station count. `stationcount`'s "arrives as /// a JSON string" edge case is covered at its own layer boundary in /// `test/modelos/pais_radio_test.dart` — `FakeServicioRadio.obtenerPaises()` /// returns already-parsed `PaisRadio` instances here, so this file tests /// rendering only, not JSON parsing. void main() { Widget buildScreen(EstadoBusqueda estado) { return ChangeNotifierProvider.value( value: estado, child: MaterialApp( locale: const Locale('es'), localizationsDelegates: AppLocalizations.localizationsDelegates, supportedLocales: AppLocalizations.supportedLocales, home: const PantallaPaises(), ), ); } Future pumpEstable(WidgetTester tester) async { await tester.pump(); await tester.pumpAndSettle(); } testWidgets( 'renders inside a PluriPushScaffold; muestra "Tus idiomas" y la lista ' 'alfabética completa con el conteo de cada país', (tester) async { final estado = EstadoBusqueda( radio: FakeServicioRadio( paises: const [ PaisRadio(nombre: 'Spain', codigoIso: 'ES', numeroEmisoras: 482), PaisRadio( nombre: 'Argentina', codigoIso: 'AR', numeroEmisoras: 120, ), PaisRadio(nombre: 'France', codigoIso: 'FR', numeroEmisoras: 75), ], ), ); addTearDown(estado.dispose); await tester.pumpWidget(buildScreen(estado)); await pumpEstable(tester); expect(find.byType(PluriPushScaffold), findsOneWidget); final l10n = AppLocalizations.of( tester.element(find.byType(PantallaPaises)), ); expect(find.text(l10n.countriesYourLanguagesTitle), findsOneWidget); // Audit 5.4: "Todos" now carries the total count as part of the // same eyebrow string ("{title} · {count}"), not a bare title. expect(find.textContaining(l10n.countriesAllTitle), findsOneWidget); // Full alphabetical list: all 3 fetched countries render. expect(find.text('Argentina'), findsOneWidget); // '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 // `stationsCount` string — 482 came from the API as a STRING // (`PaisRadio.fromApi`'s job, verified separately) and must display // as a plain number here, never throwing a cast error. expect(find.text(l10n.stationsCount(482)), findsWidgets); expect(find.text(l10n.stationsCount(120)), 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.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(find.text('Spain').first); expect(nombre.style?.fontWeight, FontWeight.w800); final decorado = tester.widget( 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', ); }, ); }); group('visual fidelity (audit 5.4/5.7)', () { testWidgets( 'the section eyebrows sit OUTSIDE any card, styled as an eyebrow ' 'label, and "Todos" carries the total country count (t4:254/260)', (tester) async { final estado = EstadoBusqueda( radio: FakeServicioRadio( paises: const [ PaisRadio(nombre: 'Spain', codigoIso: 'ES', numeroEmisoras: 482), PaisRadio( nombre: 'Argentina', codigoIso: 'AR', numeroEmisoras: 120, ), PaisRadio(nombre: 'France', codigoIso: 'FR', numeroEmisoras: 75), ], ), ); addTearDown(estado.dispose); await tester.pumpWidget(buildScreen(estado)); await pumpEstable(tester); final l10n = AppLocalizations.of( tester.element(find.byType(PantallaPaises)), ); final eyebrow = tester.widget( find.text(l10n.countriesYourLanguagesTitle), ); expect(eyebrow.style?.fontSize, 11); expect(eyebrow.style?.fontWeight, FontWeight.w800); expect( find.ancestor( of: find.text(l10n.countriesYourLanguagesTitle), matching: find.byType(PluriGlassSurface), ), findsNothing, reason: 'the eyebrow must not be a descendant of any card', ); expect( find.textContaining('${l10n.countriesAllTitle} · 3'), findsOneWidget, ); }, ); testWidgets('the header carries a search action (t4:252)', (tester) async { final estado = EstadoBusqueda(radio: FakeServicioRadio(paises: const [])); addTearDown(estado.dispose); await tester.pumpWidget(buildScreen(estado)); await pumpEstable(tester); expect(find.byIcon(Icons.search_rounded), findsOneWidget); }); testWidgets('Issue 3 (feedback-pruebas): the gap between "Tus idiomas" and ' '"Todos" is 14, matching t4:260 -- not 16', (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); expect( tester.getSize(find.byKey(const ValueKey('paises-seccion-gap'))).height, 14, reason: 't4:260 draws a 14px gap between the two eyebrow sections', ); }); }); testWidgets( 'cargarPaises no se re-dispara si ya hay datos en caché al reconstruir', (tester) async { final radio = FakeServicioRadio( paises: const [ PaisRadio(nombre: 'Italy', codigoIso: 'IT', numeroEmisoras: 30), ], ); final estado = EstadoBusqueda(radio: radio); addTearDown(estado.dispose); await tester.pumpWidget(buildScreen(estado)); await pumpEstable(tester); expect(radio.obtenerPaisesCalls, 1); // Re-entering the screen (a fresh State, same EstadoBusqueda instance) // must not refetch — the in-memory cache guard lives on EstadoBusqueda, // not on the widget. await tester.pumpWidget(const SizedBox.shrink()); await pumpEstable(tester); await tester.pumpWidget(buildScreen(estado)); await pumpEstable(tester); expect(radio.obtenerPaisesCalls, 1); }, ); }