fix(buscar): filter pills, Idioma chip, results eyebrow, states screen
Audit 6.2 (t4:292-293): active filter pills are brand-teal tinted with
a 15px close glyph and radius 10 -- was Material's own Chip theming.
Audit 6.3 (t4:294-295): an "Idioma" entry chip is now always reachable
once a search is active -- there was no standalone entry point for
language filtering before (only bundled inside "Filtros"). Opens the
same existing filter sheet rather than a new idioma-only picker.
Audit 6.4 (t4:299): the results-count line now uses eyebrowLabel
styling -- was labelLarge (14/w800).
Audit 13.3/13.4 (t4:649-651): the search-results loading skeleton
gains the missing "BUSCANDO EMISORAS..." eyebrow, and its row gap is
now 4px, not 10.
Audit 13.5/13.6 (t4:657-668): a new purpose-built _TarjetaSinResultados
replaces PluriEmptyState ONLY in the search-no-results branch (that
shared widget is left untouched for its other unrelated call sites --
favorites empty state, the discovery grid). The title now quotes the
typed query ("Sin resultados para <<jazzz>>"), and the clear-filters
pill sits INSIDE the card. New ARB key searchNoResultsForQueryTitle.
Item 6.1 (compact 60px active-search header replacing the persistent
glass pill) is NOT implemented: existing tests confirmed the quality
(bitrate) filter has NO entry point anywhere in the app besides the
header's "Filtros" pill (added first via _abrirYSeleccionarCalidad
while a country filter is ALREADY active, i.e. mid active-search).
Hiding PluriRootHeader during active search would make quality
filtering unreachable -- a real capability regression the task's own
"presentation changes, capability does not" principle forbids.
Re-plumbing where that filter lives is a bigger change than this
visual-fidelity pass should make unilaterally.
This commit is contained in:
@@ -253,10 +253,25 @@ class _PantallaBuscarState extends State<PantallaBuscar> {
|
|||||||
}),
|
}),
|
||||||
];
|
];
|
||||||
|
|
||||||
if (pills.isEmpty && estado.resultados.isEmpty) {
|
// Audit 6.3 (t4:294-295): "Idioma" is an always-reachable entry chip
|
||||||
return const SizedBox.shrink();
|
// once a search is active -- there was no standalone entry point for
|
||||||
}
|
// language filtering before this (only bundled inside "Filtros").
|
||||||
|
// Opens the SAME existing filter sheet (all 3 sections) rather than a
|
||||||
|
// new idioma-only picker: "Ordenar" is a genuinely separate control
|
||||||
|
// (not part of that sheet), and "Filtros" itself must stay reachable
|
||||||
|
// from the header regardless of active-filter state -- the quality
|
||||||
|
// (bitrate) filter has no OTHER entry point anywhere in the app, so
|
||||||
|
// this chip is additive, not a replacement.
|
||||||
|
final entryChips = <Widget>[
|
||||||
|
ActionChip(
|
||||||
|
label: Text(l10n.searchLanguageFilterLabel),
|
||||||
|
onPressed: _abrirFiltros,
|
||||||
|
),
|
||||||
|
];
|
||||||
|
|
||||||
|
// Audit 6.3: entryChips is never empty (always carries "Idioma"), so
|
||||||
|
// this row now always renders while a search is active -- the old
|
||||||
|
// "nothing to show" early return no longer applies.
|
||||||
return Padding(
|
return Padding(
|
||||||
padding: const EdgeInsets.fromLTRB(
|
padding: const EdgeInsets.fromLTRB(
|
||||||
PluriLayout.horizontal,
|
PluriLayout.horizontal,
|
||||||
@@ -267,18 +282,16 @@ class _PantallaBuscarState extends State<PantallaBuscar> {
|
|||||||
child: Column(
|
child: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
if (pills.isNotEmpty)
|
Wrap(spacing: 8, runSpacing: 8, children: [...pills, ...entryChips]),
|
||||||
Wrap(spacing: 8, runSpacing: 8, children: pills),
|
if (!estado.cargando && estado.resultados.isNotEmpty) ...[
|
||||||
if (pills.isNotEmpty && estado.resultados.isNotEmpty)
|
|
||||||
const SizedBox(height: 10),
|
const SizedBox(height: 10),
|
||||||
if (!estado.cargando && estado.resultados.isNotEmpty)
|
|
||||||
Row(
|
Row(
|
||||||
children: [
|
children: [
|
||||||
|
// Audit 6.4 (t4:299): eyebrow styling (11/w800/ls.06em) --
|
||||||
|
// was labelLarge (14/w800).
|
||||||
Text(
|
Text(
|
||||||
l10n.searchResultsCount(estado.resultados.length),
|
l10n.searchResultsCount(estado.resultados.length),
|
||||||
style: theme.textTheme.labelLarge?.copyWith(
|
style: context.pluriType.eyebrowLabel,
|
||||||
fontWeight: FontWeight.w800,
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
const Spacer(),
|
const Spacer(),
|
||||||
PopupMenuButton<OrdenEmisoras>(
|
PopupMenuButton<OrdenEmisoras>(
|
||||||
@@ -307,15 +320,27 @@ class _PantallaBuscarState extends State<PantallaBuscar> {
|
|||||||
],
|
],
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
],
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Audit 6.2 (t4:292-293): brand-teal tinted, radius 10, an inline 15px
|
||||||
|
/// close glyph -- was Material's own `Chip` theming.
|
||||||
Widget _pillFiltro(String label, VoidCallback onDeleted) {
|
Widget _pillFiltro(String label, VoidCallback onDeleted) {
|
||||||
return Chip(
|
return Chip(
|
||||||
label: Text(label),
|
label: Text(label),
|
||||||
|
labelStyle: const TextStyle(
|
||||||
|
fontWeight: FontWeight.w800,
|
||||||
|
color: Color(0xFFF2F7FA),
|
||||||
|
),
|
||||||
|
backgroundColor: PluriWaveTokens.brand.withValues(alpha: 0.2),
|
||||||
|
shape: RoundedRectangleBorder(
|
||||||
|
borderRadius: BorderRadius.circular(10),
|
||||||
|
side: BorderSide(color: PluriWaveTokens.brand.withValues(alpha: 0.45)),
|
||||||
|
),
|
||||||
onDeleted: onDeleted,
|
onDeleted: onDeleted,
|
||||||
deleteIcon: const Icon(Icons.close, size: 18),
|
deleteIcon: const Icon(Icons.close, size: 15),
|
||||||
visualDensity: VisualDensity.compact,
|
visualDensity: VisualDensity.compact,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -521,10 +546,21 @@ class _PantallaBuscarState extends State<PantallaBuscar> {
|
|||||||
return Padding(
|
return Padding(
|
||||||
padding: const EdgeInsets.all(PluriLayout.horizontal),
|
padding: const EdgeInsets.all(PluriLayout.horizontal),
|
||||||
child: Column(
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
|
// Audit 13.3 (t4:649): the "BUSCANDO EMISORAS…" eyebrow --
|
||||||
|
// never rendered before.
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.only(bottom: 10),
|
||||||
|
child: Text(
|
||||||
|
l10n.searchLoadingStationsLabel,
|
||||||
|
style: context.pluriType.eyebrowLabel,
|
||||||
|
),
|
||||||
|
),
|
||||||
for (var i = 0; i < 4; i++) ...[
|
for (var i = 0; i < 4; i++) ...[
|
||||||
const TarjetaEmisoraShimmer(esCompacta: true),
|
const TarjetaEmisoraShimmer(esCompacta: true),
|
||||||
if (i < 3) const SizedBox(height: 10),
|
// Audit 13.4 (t4:651): 4px between skeleton rows, not 10.
|
||||||
|
if (i < 3) const SizedBox(height: 4),
|
||||||
],
|
],
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
@@ -535,36 +571,40 @@ class _PantallaBuscarState extends State<PantallaBuscar> {
|
|||||||
|
|
||||||
if (resultados.isEmpty) {
|
if (resultados.isEmpty) {
|
||||||
final sinFiltros = _controller.text.isEmpty && _filtrosActivosCount == 0;
|
final sinFiltros = _controller.text.isEmpty && _filtrosActivosCount == 0;
|
||||||
return Column(
|
// Audit 13.5/13.6 (t4:657-668): a purpose-built card for the
|
||||||
children: [
|
// search-no-results state -- centred, `listSurface`, the title
|
||||||
SizedBox(
|
// QUOTES the typed query, and the clear-filters pill sits INSIDE
|
||||||
height: 260,
|
// the card. A NEW widget, not a restyle of the shared
|
||||||
child: PluriEmptyState(
|
// `PluriEmptyState` (used by several OTHER unrelated empty states
|
||||||
glyph: PluriIconGlyph.search,
|
// across the app -- favorites, the discovery grid -- which this
|
||||||
title:
|
// item does not touch).
|
||||||
|
final query = _controller.text.trim();
|
||||||
|
return Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: PluriLayout.horizontal),
|
||||||
|
child: _TarjetaSinResultados(
|
||||||
|
titulo:
|
||||||
sinFiltros
|
sinFiltros
|
||||||
? l10n.searchEmptyTitle
|
? l10n.searchEmptyTitle
|
||||||
|
: query.isNotEmpty
|
||||||
|
? l10n.searchNoResultsForQueryTitle(query)
|
||||||
: l10n.searchNoResultsTitle,
|
: l10n.searchNoResultsTitle,
|
||||||
subtitle:
|
subtitulo:
|
||||||
sinFiltros
|
sinFiltros
|
||||||
? l10n.searchEmptySubtitle
|
? l10n.searchEmptySubtitle
|
||||||
: l10n.searchNoResultsSubtitle,
|
: l10n.searchNoResultsSubtitle,
|
||||||
),
|
|
||||||
),
|
|
||||||
// task 6.6 / spec "One-Tap Clear-All-Filters on Empty Results":
|
// task 6.6 / spec "One-Tap Clear-All-Filters on Empty Results":
|
||||||
// only offered once 1+ pill-filters are active AND the search
|
// only offered once 1+ pill-filters are active AND the search
|
||||||
// came back empty — not merely "no query typed yet".
|
// came back empty — not merely "no query typed yet".
|
||||||
if (_filtrosActivosCount > 0)
|
accionQuitarFiltros:
|
||||||
Padding(
|
_filtrosActivosCount > 0
|
||||||
padding: const EdgeInsets.only(bottom: 16),
|
? (
|
||||||
child: OutlinedButton(
|
etiqueta: l10n.searchClearFiltersAction(
|
||||||
onPressed: _quitarTodosLosFiltros,
|
_filtrosActivosCount,
|
||||||
child: Text(
|
|
||||||
l10n.searchClearFiltersAction(_filtrosActivosCount),
|
|
||||||
),
|
),
|
||||||
|
onTap: _quitarTodosLosFiltros,
|
||||||
|
)
|
||||||
|
: null,
|
||||||
),
|
),
|
||||||
),
|
|
||||||
],
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1213,6 +1253,111 @@ class _ChipShimmer extends StatelessWidget {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Audit 13.5/13.6 (t4:657-668): search-no-results card -- centred,
|
||||||
|
/// `listSurface`, radius 22 (matches neither of the other 2 named token
|
||||||
|
/// radii, so this is a local one-off like `_CeldaExplorarPor`'s own),
|
||||||
|
/// with the clear-filters pill living INSIDE the card rather than below
|
||||||
|
/// it.
|
||||||
|
class _TarjetaSinResultados extends StatelessWidget {
|
||||||
|
const _TarjetaSinResultados({
|
||||||
|
required this.titulo,
|
||||||
|
required this.subtitulo,
|
||||||
|
this.accionQuitarFiltros,
|
||||||
|
});
|
||||||
|
|
||||||
|
final String titulo;
|
||||||
|
final String subtitulo;
|
||||||
|
final ({String etiqueta, VoidCallback onTap})? accionQuitarFiltros;
|
||||||
|
|
||||||
|
static const _radio = 22.0;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final accion = accionQuitarFiltros;
|
||||||
|
return DecoratedBox(
|
||||||
|
key: const ValueKey('search-no-results-card'),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: PluriWaveTokens.dark.listSurface,
|
||||||
|
borderRadius: BorderRadius.circular(_radio),
|
||||||
|
border: Border.all(color: Colors.white.withValues(alpha: 0.07)),
|
||||||
|
),
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 26),
|
||||||
|
child: Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
Icon(
|
||||||
|
Icons.search_off_rounded,
|
||||||
|
size: 40,
|
||||||
|
color: PluriWaveTokens.brand.withValues(alpha: 0.6),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 12),
|
||||||
|
Text(
|
||||||
|
titulo,
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
style: const TextStyle(fontSize: 16, fontWeight: FontWeight.w800),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 5),
|
||||||
|
Text(
|
||||||
|
subtitulo,
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 12.5,
|
||||||
|
height: 1.5,
|
||||||
|
color: Theme.of(
|
||||||
|
context,
|
||||||
|
).colorScheme.onSurface.withValues(alpha: 0.6),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
if (accion != null) ...[
|
||||||
|
const SizedBox(height: 14),
|
||||||
|
Material(
|
||||||
|
color: Colors.transparent,
|
||||||
|
child: InkWell(
|
||||||
|
borderRadius: BorderRadius.circular(12),
|
||||||
|
onTap: accion.onTap,
|
||||||
|
child: Container(
|
||||||
|
padding: const EdgeInsets.symmetric(
|
||||||
|
horizontal: 15,
|
||||||
|
vertical: 9,
|
||||||
|
),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: PluriWaveTokens.brand.withValues(alpha: 0.18),
|
||||||
|
borderRadius: BorderRadius.circular(12),
|
||||||
|
border: Border.all(
|
||||||
|
color: PluriWaveTokens.brand.withValues(alpha: 0.4),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
child: Row(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
const Icon(
|
||||||
|
Icons.filter_alt_off_rounded,
|
||||||
|
size: 17,
|
||||||
|
color: PluriWaveTokens.brand,
|
||||||
|
),
|
||||||
|
const SizedBox(width: 7),
|
||||||
|
Text(
|
||||||
|
accion.etiqueta,
|
||||||
|
style: const TextStyle(
|
||||||
|
fontSize: 12.5,
|
||||||
|
fontWeight: FontWeight.w800,
|
||||||
|
color: PluriWaveTokens.brand,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Audit 3.2 (t4 lines 167-170): one "Explorar por" grid cell — icon,
|
/// Audit 3.2 (t4 lines 167-170): one "Explorar por" grid cell — icon,
|
||||||
/// title (13.5/w800) and subtitle (11/55%). Radius 16 is a local one-off
|
/// title (13.5/w800) and subtitle (11/55%). Radius 16 is a local one-off
|
||||||
/// (like `_errorBanner`'s), matching neither of the 3 named token radii.
|
/// (like `_errorBanner`'s), matching neither of the 3 named token radii.
|
||||||
|
|||||||
@@ -89,4 +89,55 @@ void main() {
|
|||||||
expect(find.byType(TarjetaEmisoraShimmer), findsWidgets);
|
expect(find.byType(TarjetaEmisoraShimmer), findsWidgets);
|
||||||
expect(find.byType(CircularProgressIndicator), findsNothing);
|
expect(find.byType(CircularProgressIndicator), findsNothing);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
testWidgets('visual fidelity (audit 13.3/13.4): the "BUSCANDO EMISORAS..." '
|
||||||
|
'eyebrow renders above the loading rows, spaced 4px apart (t4:649-651)', (
|
||||||
|
tester,
|
||||||
|
) async {
|
||||||
|
final busqueda = _BusquedaCargando();
|
||||||
|
addTearDown(busqueda.dispose);
|
||||||
|
final estado = EstadoRadio(
|
||||||
|
audio: FakeServicioAudio(),
|
||||||
|
favoritos: FakeServicioFavoritos(),
|
||||||
|
radio: FakeServicioRadio(),
|
||||||
|
servicioEcualizador: FakeServicioEcualizador(),
|
||||||
|
servicioGrabacion: FakeServicioGrabacionRadio(),
|
||||||
|
resolverArchivoCustom: () async => throw UnimplementedError(),
|
||||||
|
iniciarAutomaticamente: false,
|
||||||
|
);
|
||||||
|
addTearDown(estado.dispose);
|
||||||
|
|
||||||
|
await tester.pumpWidget(
|
||||||
|
MultiProvider(
|
||||||
|
providers: [
|
||||||
|
ChangeNotifierProvider<EstadoRadio>.value(value: estado),
|
||||||
|
ListenableProvider<EstadoEcualizador>.value(
|
||||||
|
value: estado.ecualizador,
|
||||||
|
),
|
||||||
|
ListenableProvider<EstadoGrabacion>.value(value: estado.grabacion),
|
||||||
|
ListenableProvider<EstadoBusqueda>.value(value: busqueda),
|
||||||
|
],
|
||||||
|
child: MaterialApp(
|
||||||
|
locale: const Locale('es'),
|
||||||
|
localizationsDelegates: AppLocalizations.localizationsDelegates,
|
||||||
|
supportedLocales: AppLocalizations.supportedLocales,
|
||||||
|
home: const Scaffold(body: PantallaBuscar()),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
await tester.pump();
|
||||||
|
await tester.enterText(find.byType(SearchBar), 'jazz');
|
||||||
|
await tester.pump();
|
||||||
|
|
||||||
|
final l10n = AppLocalizations.of(
|
||||||
|
tester.element(find.byType(PantallaBuscar)),
|
||||||
|
);
|
||||||
|
expect(find.text(l10n.searchLoadingStationsLabel), findsOneWidget);
|
||||||
|
|
||||||
|
final filas = find.byType(TarjetaEmisoraShimmer);
|
||||||
|
expect(filas, findsWidgets);
|
||||||
|
final primeraAbajo = tester.getBottomLeft(filas.at(0)).dy;
|
||||||
|
final segundaArriba = tester.getTopLeft(filas.at(1)).dy;
|
||||||
|
expect(segundaArriba - primeraAbajo, 4);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -181,6 +181,101 @@ void main() {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
group('visual fidelity (audit 6.2/6.3/6.4)', () {
|
||||||
|
testWidgets(
|
||||||
|
'6.2: an active filter pill is brand-teal tinted with an inline '
|
||||||
|
'close glyph (t4:292-293)',
|
||||||
|
(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);
|
||||||
|
|
||||||
|
final chip = tester.widget<Chip>(
|
||||||
|
find.ancestor(
|
||||||
|
of: find.text(l10n.countrySpain),
|
||||||
|
matching: find.byType(Chip),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
expect(
|
||||||
|
chip.backgroundColor,
|
||||||
|
const Color(0xFF21D4D9).withValues(alpha: 0.2),
|
||||||
|
);
|
||||||
|
expect(
|
||||||
|
(chip.shape as RoundedRectangleBorder?)?.side.color,
|
||||||
|
const Color(0xFF21D4D9).withValues(alpha: 0.45),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
testWidgets(
|
||||||
|
'6.3: an "Idioma" entry chip is always reachable once a search is '
|
||||||
|
'active, opening the filters sheet (t4:294-295)',
|
||||||
|
(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);
|
||||||
|
|
||||||
|
final l10n = _l10nDe(tester);
|
||||||
|
expect(find.text(l10n.searchLanguageFilterLabel), findsOneWidget);
|
||||||
|
|
||||||
|
await tester.tap(find.text(l10n.searchLanguageFilterLabel));
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
|
||||||
|
expect(find.text(l10n.searchCountryFilterLabel), findsOneWidget);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
testWidgets(
|
||||||
|
'6.4: the results-count eyebrow uses eyebrowLabel styling (t4:299)',
|
||||||
|
(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);
|
||||||
|
|
||||||
|
final l10n = _l10nDe(tester);
|
||||||
|
final texto = tester.widget<Text>(
|
||||||
|
find.text(l10n.searchResultsCount(1)),
|
||||||
|
);
|
||||||
|
expect(texto.style?.fontSize, 11);
|
||||||
|
expect(texto.style?.fontWeight, FontWeight.w800);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
group('Buscar — Ordenar (client-side, WU6)', () {
|
group('Buscar — Ordenar (client-side, WU6)', () {
|
||||||
testWidgets(
|
testWidgets(
|
||||||
'cada opcion renderizada de Ordenar corresponde a un caso real de '
|
'cada opcion renderizada de Ordenar corresponde a un caso real de '
|
||||||
@@ -774,6 +869,44 @@ void main() {
|
|||||||
// Hazard: `pumpAndSettle()` never terminates while this card's rotating
|
// Hazard: `pumpAndSettle()` never terminates while this card's rotating
|
||||||
// ring animates -- every assertion below uses a bounded `pump()` once
|
// ring animates -- every assertion below uses a bounded `pump()` once
|
||||||
// `reconectando` is emitted, never `_pumpStableFrame`'s `pumpAndSettle`.
|
// `reconectando` is emitted, never `_pumpStableFrame`'s `pumpAndSettle`.
|
||||||
|
group('visual fidelity (audit 13.5/13.6)', () {
|
||||||
|
testWidgets(
|
||||||
|
'13.5/13.6: the no-results card quotes the query, and the "clear '
|
||||||
|
'filters" pill sits INSIDE the same card (t4:657-663)',
|
||||||
|
(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);
|
||||||
|
await _abrirYSeleccionarPais(tester, _l10nDe(tester).countrySpain);
|
||||||
|
await _pumpStableFrame(tester);
|
||||||
|
await tester.enterText(find.byType(SearchBar), 'jazzz');
|
||||||
|
await tester.testTextInput.receiveAction(TextInputAction.done);
|
||||||
|
await _pumpStableFrame(tester);
|
||||||
|
|
||||||
|
final l10n = _l10nDe(tester);
|
||||||
|
expect(
|
||||||
|
find.text(l10n.searchNoResultsForQueryTitle('jazzz')),
|
||||||
|
findsOneWidget,
|
||||||
|
);
|
||||||
|
|
||||||
|
final tarjeta = find.byKey(const ValueKey('search-no-results-card'));
|
||||||
|
expect(tarjeta, findsOneWidget);
|
||||||
|
expect(
|
||||||
|
find.descendant(
|
||||||
|
of: tarjeta,
|
||||||
|
matching: find.text(l10n.searchClearFiltersAction(1)),
|
||||||
|
),
|
||||||
|
findsOneWidget,
|
||||||
|
reason: 't4:667 the clear-filters pill sits INSIDE the card',
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
group('Item 25 -- reconnect card (audit 13.2)', () {
|
group('Item 25 -- reconnect card (audit 13.2)', () {
|
||||||
testWidgets(
|
testWidgets(
|
||||||
'shows the station name, "Reconectando...", and a stop button while '
|
'shows the station name, "Reconectando...", and a stop button while '
|
||||||
|
|||||||
Reference in New Issue
Block a user