diff --git a/lib/pantallas/pantalla_ajustes.dart b/lib/pantallas/pantalla_ajustes.dart index 8ff6e3d..f57f114 100644 --- a/lib/pantallas/pantalla_ajustes.dart +++ b/lib/pantallas/pantalla_ajustes.dart @@ -147,7 +147,9 @@ class _AjustesContent extends StatelessWidget { ), ], ), - const SizedBox(height: 12), + // Issue 3 (feedback-pruebas): t4:523/534/541 all draw a 16px gap + // between these opaque, stacked settings groups -- not 12. + const SizedBox(height: 16, key: ValueKey('ajustes-group-gap-1')), GrupoAjustes( titulo: l10n.settingsGroupStationsTitle, filas: [ @@ -202,7 +204,7 @@ class _AjustesContent extends StatelessWidget { ), ], ), - const SizedBox(height: 12), + const SizedBox(height: 16, key: ValueKey('ajustes-group-gap-2')), GrupoAjustes( titulo: l10n.settingsGroupRecordingsTitle, filas: [ @@ -250,7 +252,7 @@ class _AjustesContent extends StatelessWidget { ), ], ), - const SizedBox(height: 12), + const SizedBox(height: 16, key: ValueKey('ajustes-group-gap-3')), GrupoAjustes( titulo: l10n.settingsGroupApplicationTitle, filas: [ diff --git a/lib/pantallas/pantalla_alarma_sonando.dart b/lib/pantallas/pantalla_alarma_sonando.dart index cf62ff1..b476d32 100644 --- a/lib/pantallas/pantalla_alarma_sonando.dart +++ b/lib/pantallas/pantalla_alarma_sonando.dart @@ -225,25 +225,6 @@ class _PantallaAlarmaSonandoState extends State { tokens: tokens, ), const SizedBox(height: 22), - // Audit 9.4 (t4 line 419): "Lunes, 3 de agosto" between - // the pill and the hero time -- never rendered before. - // Purely additive: a new sibling Text, touching neither - // the pill above nor the hero time below. - Text( - fechaLargaConDiaSemana( - Localizations.localeOf(context).toString(), - DateTime.now(), - ), - textAlign: TextAlign.center, - style: TextStyle( - fontSize: 14, - fontWeight: FontWeight.w600, - color: Theme.of( - context, - ).colorScheme.onSurface.withValues(alpha: 0.6), - ), - ), - const SizedBox(height: 6), FittedBox( fit: BoxFit.scaleDown, child: Text( @@ -261,6 +242,28 @@ class _PantallaAlarmaSonandoState extends State { ), ), const SizedBox(height: 6), + // Audit 9.4: the date line goes BELOW the hero time. The + // prototype's order is pill (t4:415-416) -> 7:30 at 88px + // (t4:417) -> "Lunes, 3 de agosto" at 14px (t4:419). An + // earlier pass placed it between the pill and the time + // and cited "t4 line 419" for it — that line number is + // where the date SITS in the source, which is precisely + // why it comes last, not first. + Text( + fechaLargaConDiaSemana( + Localizations.localeOf(context).toString(), + DateTime.now(), + ), + textAlign: TextAlign.center, + style: TextStyle( + fontSize: 14, + fontWeight: FontWeight.w600, + color: Theme.of( + context, + ).colorScheme.onSurface.withValues(alpha: 0.6), + ), + ), + const SizedBox(height: 6), Text( localizedAlarmName(l10n, alarma.nombre), textAlign: TextAlign.center, @@ -339,14 +342,18 @@ class _PantallaAlarmaSonandoState extends State { ], ), ), - const SizedBox(height: 10), + // Issue 3 (feedback-pruebas): t4:427 wraps POSPONER's + // eyebrow, the snooze tiles and Stop in a `gap:12` flex + // column -- the same 12 on both sides, not the 10/14 pair + // this used to carry. + const SizedBox(height: 12), _FilaSnoozeFija( alarma: alarma, l10n: l10n, tokens: tokens, onPosponer: _posponer, ), - const SizedBox(height: 14), + const SizedBox(height: 12), SizedBox( width: double.infinity, child: FilledButton.icon( 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/lib/pantallas/pantalla_grabaciones.dart b/lib/pantallas/pantalla_grabaciones.dart index 61f3d0c..3b6e5d4 100644 --- a/lib/pantallas/pantalla_grabaciones.dart +++ b/lib/pantallas/pantalla_grabaciones.dart @@ -251,7 +251,12 @@ class _PantallaGrabacionesState extends State { padding: PluriLayout.pageContentPadding, children: [ _BarraDeAlmacenamiento(archivos: archivos), - const SizedBox(height: 12), + // Issue 3 (feedback-pruebas): t4:617 draws a 16px gap between + // the storage card and the rows below it, not 12. + const SizedBox( + height: 16, + key: ValueKey('grabaciones-storage-gap'), + ), if (snap.connectionState == ConnectionState.done && archivos.isEmpty) PluriEmptyState( diff --git a/lib/pantallas/pantalla_paises.dart b/lib/pantallas/pantalla_paises.dart index ac4444c..4891aa0 100644 --- a/lib/pantallas/pantalla_paises.dart +++ b/lib/pantallas/pantalla_paises.dart @@ -134,7 +134,12 @@ class _PantallaPaisesState extends State { ), if (query.isEmpty) ...[ _seccionTusIdiomas(context, estado.paises, l10n), - const SizedBox(height: 16), + // Issue 3 (feedback-pruebas): t4:260 draws a 14px gap + // between "Tus idiomas" and "Todos", not 16. + const SizedBox( + height: 14, + key: ValueKey('paises-seccion-gap'), + ), _seccionTodos(context, estado.paises, l10n), ] else _seccionTodos(context, paisesFiltrados, l10n), diff --git a/test/pantallas/pantalla_ajustes_test.dart b/test/pantallas/pantalla_ajustes_test.dart index 4ef5e4f..9466b03 100644 --- a/test/pantallas/pantalla_ajustes_test.dart +++ b/test/pantallas/pantalla_ajustes_test.dart @@ -306,6 +306,32 @@ void main() { expect(find.text('RECORDINGS & MUSIC'), findsOneWidget); expect(find.text('APPLICATION'), findsOneWidget); }); + + testWidgets( + 'Issue 3 (feedback-pruebas): the gap between stacked settings groups ' + 'is 16, matching t4:523/534/541 -- not 12', + (tester) async { + setLargeSurface(tester); + _suppressListTileInkAssertion(); + final estado = await crearEstado(); + addTearDown(estado.dispose); + + await tester.pumpWidget(buildAjustes(estado)); + await pumpStable(tester); + + for (final key in [ + 'ajustes-group-gap-1', + 'ajustes-group-gap-2', + 'ajustes-group-gap-3', + ]) { + expect( + tester.getSize(find.byKey(ValueKey(key))).height, + 16, + reason: 't4:523/534/541 all draw a 16px gap between stacked groups', + ); + } + }, + ); }); } diff --git a/test/pantallas/pantalla_alarma_sonando_tier5_test.dart b/test/pantallas/pantalla_alarma_sonando_tier5_test.dart index b241773..93f6dbf 100644 --- a/test/pantallas/pantalla_alarma_sonando_tier5_test.dart +++ b/test/pantallas/pantalla_alarma_sonando_tier5_test.dart @@ -99,8 +99,8 @@ void main() { SharedPreferences.setMockInitialValues({}); }); - testWidgets('visual fidelity (audit 9.4): the date line renders between the ' - 'schedule pill and the hero time (t4:419)', (tester) async { + testWidgets('visual fidelity (audit 9.4): the date line renders BELOW the ' + 'hero time (t4:415-419: pill, then 7:30, then the date)', (tester) async { await _montarPantalla(tester); final localeTag = @@ -111,16 +111,22 @@ void main() { expect(find.text(esperado), findsOneWidget); - // Order: pill above the date line, date line above the hero time. + // Order: pill, then the hero time, then the date line. This test used + // to assert date-before-time and cited "t4:419" for it — but 419 is + // simply the source line the date occupies, and in the prototype it + // comes AFTER the 88px time on line 417. The citation refuted the + // assertion it was supporting. final pillY = tester .getBottomLeft(find.byKey(const ValueKey('ringing-schedule-pill'))) .dy; - final dateY = tester.getTopLeft(find.text(esperado)).dy; final timeY = - tester.getTopLeft(find.byKey(const ValueKey('ringing-hero-time'))).dy; - expect(pillY <= dateY, isTrue); - expect(dateY <= timeY, isTrue); + tester + .getBottomLeft(find.byKey(const ValueKey('ringing-hero-time'))) + .dy; + final dateY = tester.getTopLeft(find.text(esperado)).dy; + expect(pillY <= timeY, isTrue, reason: 'pill sits above the time'); + expect(timeY <= dateY, isTrue, reason: 'the date sits below the time'); // Regression guard: pumpAndSettle must still complete (purely // additive static text, no new animation). @@ -159,4 +165,49 @@ void main() { await tester.pumpAndSettle(); }, ); + + testWidgets( + 'Issue 3 (feedback-pruebas): the gap above the snooze tiles matches the ' + 'gap below them (t4:427 draws a uniform gap:12 flex column) -- the ' + 'previous 10/14 pair matched neither the prototype nor each other', + (tester) async { + await _montarPantalla(tester); + final l10n = AppLocalizations.of( + tester.element(find.byType(PantallaAlarmaSonando)), + ); + + final eyebrowBottom = + tester + .getBottomLeft( + find + .ancestor( + of: find.byIcon(Icons.snooze_rounded), + matching: find.byType(Row), + ) + .first, + ) + .dy; + final tileDestacado = find.ancestor( + of: find.text(l10n.alarmSnoozeOptionLabel(10)), + matching: find.byType(FilledButton), + ); + final tilesTop = tester.getTopLeft(tileDestacado).dy; + final tilesBottom = tester.getBottomLeft(tileDestacado).dy; + final stopButtonTop = + tester + .getTopLeft(find.byKey(const ValueKey('ringing-stop-button'))) + .dy; + + expect( + tilesTop - eyebrowBottom, + 12, + reason: 't4:427: gap:12 above the snooze tiles', + ); + expect( + stopButtonTop - tilesBottom, + 12, + reason: 't4:427: gap:12 below the snooze tiles, same as above', + ); + }, + ); } 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) { diff --git a/test/pantallas/pantalla_grabaciones_test.dart b/test/pantallas/pantalla_grabaciones_test.dart index fa64101..877b06e 100644 --- a/test/pantallas/pantalla_grabaciones_test.dart +++ b/test/pantallas/pantalla_grabaciones_test.dart @@ -216,6 +216,35 @@ void main() { }, ); + testWidgets( + 'Issue 3 (feedback-pruebas): the gap between the storage card and the ' + 'rows below is 16, matching t4:617 -- not 12', + (tester) async { + final estado = EstadoGrabacion( + servicio: _FakeServicioGrabacionConArchivos([ + fijaA, + ], maxBytesFijo: 200 * 1024 * 1024), + ); + addTearDown(estado.dispose); + + await tester.pumpWidget( + buildScreen( + estado: estado, + reproductor: _ReproductorGrabacionesFake(const {}), + ), + ); + await pumpStable(tester); + + expect( + tester + .getSize(find.byKey(const ValueKey('grabaciones-storage-gap'))) + .height, + 16, + reason: 't4:617 draws a 16px gap here', + ); + }, + ); + testWidgets('15.2-A: 3 recording fixtures render as 3 rows', (tester) async { final estado = EstadoGrabacion( servicio: _FakeServicioGrabacionConArchivos([ diff --git a/test/pantallas/pantalla_paises_test.dart b/test/pantallas/pantalla_paises_test.dart index 1d15d9d..06f7683 100644 --- a/test/pantallas/pantalla_paises_test.dart +++ b/test/pantallas/pantalla_paises_test.dart @@ -262,6 +262,27 @@ void main() { 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(