diff --git a/lib/pantallas/pantalla_favoritos.dart b/lib/pantallas/pantalla_favoritos.dart index 3753a0b..cbf87ca 100644 --- a/lib/pantallas/pantalla_favoritos.dart +++ b/lib/pantallas/pantalla_favoritos.dart @@ -174,12 +174,18 @@ class _PantallaFavoritosState extends State { return ReorderableListView( buildDefaultDragHandles: false, - padding: const EdgeInsets.fromLTRB( - PluriLayout.horizontal, - 4, - PluriLayout.horizontal, - PluriLayout.bottomChromeInset, - ), + // Issue 3 (feedback-pruebas): zero horizontal here, matching every + // other root's PluriLayout.pageListPadding convention (Alarmas, + // Ajustes, and this screen's OWN empty-state branch above). + // ReorderableListView.padding wraps header/children/footer UNIFORMLY, + // so a single horizontal value here can never be simultaneously right + // for PluriRootHeader (self-padded, wants none), the reorderable rows + // (want row tier, applied per item below) and the footer CTA (wants + // card tier, applied on the footer's own Padding below). The previous + // `PluriLayout.horizontal` doubled up on top of PluriRootHeader's own + // internal inset, pushing "Favorites" in by 36px instead of the 20px + // every other root uses for its title. + padding: const EdgeInsets.only(bottom: PluriLayout.bottomChromeInset), header: Padding( padding: const EdgeInsets.only(bottom: 12), child: Column( @@ -224,17 +230,39 @@ class _PantallaFavoritosState extends State { ], ), const SizedBox(height: 12), - _FilaChipsGrupos( - grupos: gruposVisibles, - favoritos: favoritos, - seleccionado: seleccionEfectiva, - onSeleccionar: (id) => setState(() => _grupoSeleccionadoId = id), + // Issue 3 (feedback-pruebas): t4:218 draws this chip strip at + // title-tier (20px) horizontal inset, directly on the page + // background -- it now needs its OWN inset since the list's + // padding no longer supplies one. + Padding( + padding: const EdgeInsets.symmetric( + horizontal: PluriLayout.titleHorizontal, + ), + child: _FilaChipsGrupos( + grupos: gruposVisibles, + favoritos: favoritos, + seleccionado: seleccionEfectiva, + onSeleccionar: + (id) => setState(() => _grupoSeleccionadoId = id), + ), ), ], ), ), footer: Padding( - padding: const EdgeInsets.only(top: 4), + // Issue 3 (feedback-pruebas): card tier (16, matching every other + // screen's dashed CTA) now that the list's own padding no longer + // supplies it, plus t4:234's 8px gap above the CTA + // (PluriLayout.compactGap) instead of the previous unwired literal + // 4 -- the ONLY state of this screen with a nonzero top gap before + // its own content used a value that matched neither this screen's + // own empty-state branch nor the prototype. + padding: const EdgeInsets.fromLTRB( + PluriLayout.horizontal, + PluriLayout.compactGap, + PluriLayout.horizontal, + 0, + ), child: _CtaEmisoraPersonalizada( onTap: _abrirFormularioEmisoraPersonalizada, ), @@ -244,14 +272,24 @@ class _PantallaFavoritosState extends State { _onReorder(filtrados, favoritos, oldIndex, newIndex), children: [ for (var i = 0; i < filtrados.length; i++) - _FilaFavorito( + // Issue 3 (feedback-pruebas): row tier (12), not card tier -- the + // key moves to this wrapper (ReorderableListView identifies each + // child by its own top-level key) since FilaEmisoraPlana rows are + // documented (audit 4.3) as flat, background-less rows, the same + // tier Buscar's results list already uses for the same widget. + Padding( key: ValueKey(filtrados[i].uuid), - index: i, - emisora: filtrados[i], - grupos: gruposVisibles, - grupoActual: gruposVisibles.firstWhere( - (g) => g.id == filtrados[i].grupoFavoritosId, - orElse: () => gruposVisibles.first, + padding: const EdgeInsets.symmetric( + horizontal: PluriLayout.rowHorizontal, + ), + child: _FilaFavorito( + index: i, + emisora: filtrados[i], + grupos: gruposVisibles, + grupoActual: gruposVisibles.firstWhere( + (g) => g.id == filtrados[i].grupoFavoritosId, + orElse: () => gruposVisibles.first, + ), ), ), ], @@ -343,7 +381,6 @@ class _FilaChipsGrupos extends StatelessWidget { class _FilaFavorito extends StatelessWidget { const _FilaFavorito({ - super.key, required this.index, required this.emisora, required this.grupos, diff --git a/test/pantallas/pantalla_favoritos_test.dart b/test/pantallas/pantalla_favoritos_test.dart index 623a606..eaa38eb 100644 --- a/test/pantallas/pantalla_favoritos_test.dart +++ b/test/pantallas/pantalla_favoritos_test.dart @@ -7,7 +7,9 @@ import 'package:pluriwave/l10n/gen/app_localizations.dart'; import 'package:pluriwave/pantallas/ajustes/pantalla_ajustes_grupos_favoritos.dart'; import 'package:pluriwave/pantallas/pantalla_favoritos.dart'; import 'package:pluriwave/widgets/fila_emisora_plana.dart'; +import 'package:pluriwave/widgets/pluri_layout.dart'; import 'package:pluriwave/widgets/pluri_push_scaffold.dart'; +import 'package:pluriwave/widgets/pluri_root_header.dart'; import 'package:pluriwave/widgets/tarjeta_emisora.dart'; import 'package:provider/provider.dart'; import 'package:shared_preferences/shared_preferences.dart'; @@ -546,6 +548,106 @@ void main() { expect(inactivo.backgroundColor, const Color(0xFF102532)); }); }); + + group('Issue 3 (feedback-pruebas): spacing tiers', () { + testWidgets('the header title sits at title-tier inset (20px) -- ' + 'ReorderableListView.padding used to double up on top of ' + "PluriRootHeader's own internal inset", (tester) async { + setLargeSurface(tester); + _suppressListTileInkAssertion(); + final estado = await crearEstadoConFavoritos(); + addTearDown(estado.dispose); + final l10n = lookupAppLocalizations(const Locale('en')); + + await tester.pumpWidget(buildScreen(estado)); + await pumpStable(tester); + + final titulo = find.descendant( + of: find.byType(PluriRootHeader), + matching: find.text(l10n.favoritesTitle), + ); + expect( + tester.getTopLeft(titulo).dx, + PluriLayout.titleHorizontal, + reason: + 'PluriRootHeader already supplies its own 20px inset; the ' + 'previous ReorderableListView.padding of 16 doubled up on top ' + 'of it, landing the title at 36px instead of 20px -- the ONE ' + "root screen whose header didn't match Alarmas/Ajustes", + ); + }); + + testWidgets( + 'the header sits at the SAME horizontal position whether the list is ' + 'empty or populated -- two mutually-exclusive states of the same ' + 'header must not read differently', + (tester) async { + setLargeSurface(tester); + final l10n = lookupAppLocalizations(const Locale('en')); + + final vacio = await crearEstadoVacio(); + addTearDown(vacio.dispose); + await tester.pumpWidget(buildScreen(vacio)); + await pumpStable(tester); + final dxVacio = + tester + .getTopLeft( + find.descendant( + of: find.byType(PluriRootHeader), + matching: find.text(l10n.favoritesTitle), + ), + ) + .dx; + + _suppressListTileInkAssertion(); + final conFavoritos = await crearEstadoConFavoritos(); + addTearDown(conFavoritos.dispose); + await tester.pumpWidget(buildScreen(conFavoritos)); + await pumpStable(tester); + final dxConFavoritos = + tester + .getTopLeft( + find.descendant( + of: find.byType(PluriRootHeader), + matching: find.text(l10n.favoritesTitle), + ), + ) + .dx; + + expect( + dxConFavoritos, + dxVacio, + reason: + 'the empty and populated branches of this screen must render ' + 'the SAME header inset -- they previously did not (0 vs 16 ' + 'extra px of list-level padding)', + ); + }, + ); + + testWidgets( + 'each favourite row uses row-tier horizontal inset (12), not the ' + 'card-tier constant a background-less row was never meant to carry', + (tester) async { + setLargeSurface(tester); + _suppressListTileInkAssertion(); + final estado = await crearEstadoConFavoritos(); + addTearDown(estado.dispose); + + await tester.pumpWidget(buildScreen(estado)); + await pumpStable(tester); + + expect( + tester.getTopLeft(find.byType(FilaEmisoraPlana).first).dx, + PluriLayout.rowHorizontal, + reason: + 'audit 4.3: background-less rows are row tier (12), matching ' + 'the same widget already fixed on Buscar -- not card tier ' + '(16)', + ); + }, + ); + }); } void setLargeSurface(WidgetTester tester) {