diff --git a/lib/pantallas/pantalla_alarmas.dart b/lib/pantallas/pantalla_alarmas.dart index 7a0a63d..89f431b 100644 --- a/lib/pantallas/pantalla_alarmas.dart +++ b/lib/pantallas/pantalla_alarmas.dart @@ -14,9 +14,7 @@ import '../tema/pluriwave_theme.dart'; import '../tema/pluriwave_tokens.dart'; import '../widgets/editor_hora_inline.dart'; import '../widgets/pluri_glass_surface.dart'; -import '../widgets/pluri_icon.dart'; import '../widgets/pluri_layout.dart'; -import '../widgets/pluri_premium_widgets.dart'; import '../widgets/pluri_push_scaffold.dart'; import '../widgets/pluri_root_header.dart'; import '../widgets/pluri_sleep_timer_sheet.dart'; @@ -39,20 +37,20 @@ class PantallaAlarmas extends StatelessWidget { // AppBar — this root now draws its own 56px title row instead of // relying on app.dart's removed shared chrome (which is also // where the sleep-timer action used to live). + // S2 (Tier 1 visual fidelity): PluriScreenHeader (the glass hero + // this used to be) is retired — it is not in the prototype at + // all. Its ONE functional bit, the create-alarm action, moves + // into PluriRootHeader's `actions` slot so it stays reachable. PluriRootHeader( title: l10n.alarmScreenTitle, onSleepTimer: () => showPluriSleepTimerSheet(context), - ), - PluriScreenHeader( - title: l10n.alarmScreenTitle, - subtitle: l10n.alarmScreenSubtitle, - glyph: PluriIconGlyph.alarm, - primaryActionLabel: l10n.createAlarmAction, - onPrimaryAction: () => _abrirEditor(context), - trailing: PluriStatusPill( - icon: Icons.alarm_on_rounded, - label: l10n.alarmsCount(estado.alarmas.length), - ), + actions: [ + FilledButton.tonalIcon( + onPressed: () => _abrirEditor(context), + icon: const Icon(Icons.auto_awesome_rounded, size: 18), + label: Text(l10n.createAlarmAction), + ), + ], ), Padding( padding: PluriLayout.pageContentPadding, diff --git a/lib/pantallas/pantalla_buscar.dart b/lib/pantallas/pantalla_buscar.dart index fdd6e26..e6514a0 100644 --- a/lib/pantallas/pantalla_buscar.dart +++ b/lib/pantallas/pantalla_buscar.dart @@ -142,25 +142,23 @@ class _PantallaBuscarState extends State { return ListView( padding: PluriLayout.pageListPadding, children: [ - // S1 (Tier 1 visual fidelity): the prototype has no global AppBar — - // this root now draws its own 56px title row instead of relying on - // app.dart's removed shared chrome (which is also where the - // sleep-timer action used to live). + // S1/S2 (Tier 1 visual fidelity): the prototype has no global + // AppBar and no PluriScreenHeader (the glass hero this used to be + // is retired everywhere) — this root draws its own 56px title row + // instead, carrying the filters entry point that used to live on + // the hero's `trailing` slot. PluriRootHeader( title: l10n.searchScreenTitle, onSleepTimer: () => showPluriSleepTimerSheet(context), - ), - PluriScreenHeader( - title: l10n.searchScreenTitle, - subtitle: l10n.searchScreenSubtitle, - glyph: PluriIconGlyph.search, - trailing: GestureDetector( - onTap: _abrirFiltros, - child: PluriStatusPill( - icon: Icons.tune_rounded, - label: l10n.searchFiltersLabel, + actions: [ + GestureDetector( + onTap: _abrirFiltros, + child: PluriStatusPill( + icon: Icons.tune_rounded, + label: l10n.searchFiltersLabel, + ), ), - ), + ], ), Padding( padding: const EdgeInsets.fromLTRB( diff --git a/lib/pantallas/pantalla_favoritos.dart b/lib/pantallas/pantalla_favoritos.dart index 4709990..46d4cb1 100644 --- a/lib/pantallas/pantalla_favoritos.dart +++ b/lib/pantallas/pantalla_favoritos.dart @@ -119,19 +119,13 @@ class _PantallaFavoritosState extends State { // AppBar — this root now draws its own 56px title row instead of // relying on app.dart's removed shared chrome (which is also // where the sleep-timer action used to live). + // S2 (Tier 1 visual fidelity): PluriScreenHeader (the glass hero + // this used to be) is retired — it is not in the prototype at + // all, and carried no functional action on this screen. PluriRootHeader( title: l10n.favoritesTitle, onSleepTimer: () => showPluriSleepTimerSheet(context), ), - PluriScreenHeader( - title: l10n.favoritesTitle, - subtitle: l10n.favoritesHeaderSubtitle, - glyph: PluriIconGlyph.favorites, - trailing: PluriStatusPill( - icon: Icons.favorite_rounded, - label: l10n.favoritesCollection, - ), - ), SizedBox( height: 320, child: PluriEmptyState( @@ -190,20 +184,12 @@ class _PantallaFavoritosState extends State { child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - // S1 (Tier 1 visual fidelity): see the empty-state branch above. + // S1/S2 (Tier 1 visual fidelity): see the empty-state branch + // above — PluriScreenHeader is retired everywhere. PluriRootHeader( title: l10n.favoritesTitle, onSleepTimer: () => showPluriSleepTimerSheet(context), ), - PluriScreenHeader( - title: l10n.favoritesTitle, - subtitle: l10n.favoritesHeaderSubtitle, - glyph: PluriIconGlyph.favorites, - trailing: PluriStatusPill( - icon: Icons.library_music_rounded, - label: l10n.favoritesSavedCount(favoritos.length), - ), - ), const SizedBox(height: 12), Row( children: [ diff --git a/lib/widgets/pluri_premium_widgets.dart b/lib/widgets/pluri_premium_widgets.dart index bc00507..4c89b92 100644 --- a/lib/widgets/pluri_premium_widgets.dart +++ b/lib/widgets/pluri_premium_widgets.dart @@ -1,217 +1,9 @@ import 'package:flutter/material.dart'; import '../tema/pluriwave_theme.dart'; -import '../tema/pluriwave_tokens.dart'; import 'pluri_glass_surface.dart'; import 'pluri_icon.dart'; -class PluriScreenHeader extends StatelessWidget { - const PluriScreenHeader({ - super.key, - required this.title, - required this.subtitle, - required this.glyph, - this.primaryActionLabel, - this.onPrimaryAction, - this.trailing, - }); - - final String title; - final String subtitle; - final PluriIconGlyph glyph; - final String? primaryActionLabel; - final VoidCallback? onPrimaryAction; - final Widget? trailing; - - @override - Widget build(BuildContext context) { - final t = context.pluriTokens; - final theme = Theme.of(context); - final width = MediaQuery.sizeOf(context).width; - final scale = MediaQuery.textScalerOf(context).scale(1); - final compact = width < 380 || scale >= 1.25; - final iconSize = compact ? 50.0 : 56.0; - - Widget glyphBadge() => Container( - width: iconSize, - height: iconSize, - decoration: BoxDecoration( - shape: BoxShape.circle, - gradient: LinearGradient( - colors: [ - PluriWaveTokens.brightCyan.withValues(alpha: 0.95), - t.electricMagenta, - t.warmCoral, - ], - ), - boxShadow: [ - BoxShadow(color: t.glowColor, blurRadius: 28, spreadRadius: 2), - ], - ), - child: Center( - child: PluriIcon( - glyph: glyph, - variant: PluriIconVariant.filled, - size: compact ? 25 : 28, - ), - ), - ); - - Widget textBlock() => Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - title, - maxLines: compact ? 2 : 1, - overflow: TextOverflow.ellipsis, - style: theme.textTheme.headlineSmall?.copyWith( - fontWeight: FontWeight.w900, - letterSpacing: -0.7, - height: 1.05, - ), - ), - const SizedBox(height: 6), - Text( - subtitle, - maxLines: compact ? 4 : 3, - overflow: TextOverflow.ellipsis, - style: theme.textTheme.bodyMedium?.copyWith( - color: theme.colorScheme.onSurface.withValues(alpha: 0.78), - height: 1.28, - ), - ), - if (primaryActionLabel != null) ...[ - const SizedBox(height: 12), - FilledButton.tonalIcon( - onPressed: onPrimaryAction, - icon: const Icon(Icons.auto_awesome_rounded, size: 18), - label: Text(primaryActionLabel!), - ), - ], - ], - ); - - Widget foreground() { - if (compact) { - return Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Row( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - glyphBadge(), - const SizedBox(width: 14), - Expanded(child: textBlock()), - ], - ), - if (trailing != null) ...[ - const SizedBox(height: 14), - Align(alignment: Alignment.centerLeft, child: trailing!), - ], - ], - ); - } - - return Row( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - glyphBadge(), - const SizedBox(width: 14), - Expanded(child: textBlock()), - if (trailing != null) ...[ - const SizedBox(width: 12), - ConstrainedBox( - constraints: const BoxConstraints(maxWidth: 220), - child: trailing!, - ), - ], - ], - ); - } - - return Padding( - padding: EdgeInsets.fromLTRB( - t.spacingMd, - t.spacingSm, - t.spacingMd, - t.spacingSm, - ), - child: PluriGlassSurface( - borderRadius: BorderRadius.circular(t.radiusLg + 8), - padding: EdgeInsets.symmetric( - horizontal: compact ? 16 : 20, - vertical: compact ? 18 : 20, - ), - child: Stack( - children: [ - Positioned.fill( - child: ClipRRect( - borderRadius: BorderRadius.circular(t.radiusLg + 8), - child: Opacity( - opacity: 0.24, - child: Image.asset( - 'assets/images/aurora_wave_banner.png', - fit: BoxFit.cover, - errorBuilder: (_, __, ___) => const SizedBox.shrink(), - ), - ), - ), - ), - Positioned.fill( - child: DecoratedBox( - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(t.radiusLg + 8), - gradient: LinearGradient( - begin: Alignment.centerLeft, - end: Alignment.centerRight, - colors: [ - Colors.black.withValues(alpha: 0.58), - Colors.black.withValues(alpha: 0.18), - ], - ), - ), - ), - ), - Positioned( - right: -36, - top: -42, - child: _Orb( - color: t.electricMagenta.withValues(alpha: 0.38), - size: 128, - ), - ), - Positioned( - right: 10, - top: 10, - child: Opacity( - opacity: 0.18, - child: Image.asset( - 'assets/icons/pluriwave_app_mark.png', - width: 120, - height: 120, - errorBuilder: (_, __, ___) => const SizedBox.shrink(), - ), - ), - ), - Positioned( - right: 44, - bottom: -54, - child: _Orb( - color: PluriWaveTokens.brightCyan.withValues(alpha: 0.22), - size: 116, - ), - ), - Padding( - padding: EdgeInsets.all(compact ? 2 : 4), - child: foreground(), - ), - ], - ), - ), - ); - } -} - class PluriStatusPill extends StatelessWidget { const PluriStatusPill({ super.key, @@ -313,24 +105,3 @@ class PluriEmptyState extends StatelessWidget { ); } } - -class _Orb extends StatelessWidget { - const _Orb({required this.color, required this.size}); - - final Color color; - final double size; - - @override - Widget build(BuildContext context) { - return IgnorePointer( - child: Container( - width: size, - height: size, - decoration: BoxDecoration( - shape: BoxShape.circle, - gradient: RadialGradient(colors: [color, color.withValues(alpha: 0)]), - ), - ), - ); - } -} diff --git a/lib/widgets/pluri_root_header.dart b/lib/widgets/pluri_root_header.dart index 89df579..cae32ae 100644 --- a/lib/widgets/pluri_root_header.dart +++ b/lib/widgets/pluri_root_header.dart @@ -2,6 +2,7 @@ import 'package:flutter/material.dart'; import '../l10n/gen/app_localizations.dart'; import '../tema/pluriwave_theme.dart'; +import 'pluri_layout.dart'; /// S1 (Tier 1 visual fidelity): the prototype (`t4`) draws no global /// `AppBar` — each root paints its own 56px title row inside its own @@ -15,11 +16,19 @@ class PluriRootHeader extends StatelessWidget { super.key, required this.title, required this.onSleepTimer, + this.actions = const [], }); final String title; final VoidCallback onSleepTimer; + /// S2 (Tier 1 visual fidelity): the now-retired `PluriScreenHeader` was + /// also the only home for a couple of roots' SINGLE functional action — + /// Alarmas' create-alarm button, Buscar's filters entry point. Rendered + /// before the shared bedtime button; empty by default (every other root + /// needs nothing extra here). + final List actions; + static const double height = 56; @override @@ -29,7 +38,15 @@ class PluriRootHeader extends StatelessWidget { return SizedBox( height: height, child: Padding( - padding: const EdgeInsets.fromLTRB(20, 0, 12, 0), + // S5: the prototype's own header padding is title-tier on the + // left, row-tier on the right (t4 e.g. Alarmas + // `padding:0 12px 0 20px`). + padding: const EdgeInsets.fromLTRB( + PluriLayout.titleHorizontal, + 0, + PluriLayout.rowHorizontal, + 0, + ), child: Row( children: [ Expanded( @@ -40,6 +57,7 @@ class PluriRootHeader extends StatelessWidget { style: type.sectionTitle, ), ), + ...actions, IconButton( icon: const Icon(Icons.bedtime_outlined), tooltip: l10n.sleepTimer, diff --git a/test/pantallas/pluri_screen_header_retired_test.dart b/test/pantallas/pluri_screen_header_retired_test.dart new file mode 100644 index 0000000..451ec5b --- /dev/null +++ b/test/pantallas/pluri_screen_header_retired_test.dart @@ -0,0 +1,173 @@ +import 'dart:io'; + +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:pluriwave/estado/estado_alarmas.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_idioma.dart'; +import 'package:pluriwave/estado/estado_radio.dart'; +import 'package:pluriwave/l10n/gen/app_localizations.dart'; +import 'package:pluriwave/pantallas/pantalla_ajustes.dart'; +import 'package:pluriwave/pantallas/pantalla_alarmas.dart'; +import 'package:pluriwave/pantallas/pantalla_buscar.dart'; +import 'package:pluriwave/pantallas/pantalla_favoritos.dart'; +import 'package:pluriwave/servicios/servicio_alarmas.dart'; +import 'package:pluriwave/widgets/pluri_root_header.dart'; +import 'package:provider/provider.dart'; +import 'package:shared_preferences/shared_preferences.dart'; + +import '../helpers/fakes.dart'; +import '../helpers/fakes_alarmas.dart'; + +/// S2 (Tier 1 visual fidelity): `PluriScreenHeader` — a 38-radius glass +/// hero with an aurora banner, a black scrim, two radial orbs, a 120px +/// watermark and a tri-gradient glyph badge — is not in the prototype at +/// all (`t4` never draws it). It is retired from all 4 roots that used it; +/// each root's ONLY title chrome is now `PluriRootHeader` (S1). Its +/// subtitle text was the one thing the hero rendered that nothing else on +/// these screens does — its absence is this suite's signal that the hero +/// is really gone, since the class itself is deleted and can no longer be +/// referenced by type from a test. +/// +/// Pre-existing project constraint (see `pantalla_ajustes_test.dart`): +/// PluriGlassSurface paints a background over ListTile's ink layer, which +/// Flutter flags as a warning-level assertion, not a correctness bug. +void _suppressListTileInkAssertion() { + final original = FlutterError.onError; + FlutterError.onError = (details) { + if (details.exceptionAsString().contains( + 'ListTile background color or ink splashes may be invisible', + )) { + return; + } + original?.call(details); + }; + addTearDown(() => FlutterError.onError = original); +} + +void main() { + setUp(() { + SharedPreferences.setMockInitialValues({}); + }); + + Future archivoCustomVacio() async => File( + '${Directory.current.path}/test/fixtures/emisoras_custom_vacio.json', + ); + + EstadoRadio crearEstadoRadio() => EstadoRadio( + audio: FakeServicioAudio(), + favoritos: FakeServicioFavoritos(), + radio: FakeServicioRadio(), + servicioEcualizador: FakeServicioEcualizador(), + servicioGrabacion: FakeServicioGrabacionRadioInactiva(), + resolverArchivoCustom: archivoCustomVacio, + iniciarAutomaticamente: false, + ); + + Widget testApp(EstadoRadio estado, Widget body, {EstadoAlarmas? alarmas}) { + return MultiProvider( + providers: [ + ChangeNotifierProvider.value(value: estado), + ListenableProvider.value(value: estado.ecualizador), + ListenableProvider.value(value: estado.grabacion), + ListenableProvider.value(value: estado.busqueda), + ChangeNotifierProvider.value(value: EstadoIdioma()), + if (alarmas != null) + ChangeNotifierProvider.value(value: alarmas), + ], + child: MaterialApp( + locale: const Locale('en'), + localizationsDelegates: AppLocalizations.localizationsDelegates, + supportedLocales: AppLocalizations.supportedLocales, + home: Scaffold(body: body), + ), + ); + } + + void setLargeSurface(WidgetTester tester) { + tester.view.physicalSize = const Size(1440, 3200); + tester.view.devicePixelRatio = 1.0; + addTearDown(tester.view.resetPhysicalSize); + addTearDown(tester.view.resetDevicePixelRatio); + } + + Future pumpStable(WidgetTester tester) async { + await tester.pump(); + await tester.pump(const Duration(milliseconds: 100)); + } + + testWidgets( + 'Buscar: the retired hero subtitle is gone, PluriRootHeader is the ' + 'only header, and the filters entry point is still reachable', + (tester) async { + setLargeSurface(tester); + final estado = crearEstadoRadio(); + addTearDown(estado.dispose); + final l10n = await AppLocalizations.delegate.load(const Locale('en')); + + await tester.pumpWidget(testApp(estado, const PantallaBuscar())); + await pumpStable(tester); + + expect(find.text(l10n.searchScreenSubtitle), findsNothing); + expect(find.byType(PluriRootHeader), findsOneWidget); + expect(find.text(l10n.searchFiltersLabel), findsWidgets); + }, + ); + + testWidgets('Favoritos (empty state): the retired hero subtitle is gone', ( + tester, + ) async { + setLargeSurface(tester); + final estado = crearEstadoRadio(); + addTearDown(estado.dispose); + final l10n = await AppLocalizations.delegate.load(const Locale('en')); + + await tester.pumpWidget(testApp(estado, const PantallaFavoritos())); + await pumpStable(tester); + + expect(find.text(l10n.favoritesHeaderSubtitle), findsNothing); + expect(find.byType(PluriRootHeader), findsOneWidget); + }); + + testWidgets( + 'Alarmas: the retired hero subtitle is gone, and the create-alarm ' + 'action is still reachable', + (tester) async { + setLargeSurface(tester); + final estado = crearEstadoRadio(); + final alarmas = EstadoAlarmas( + servicio: ServicioAlarmas(reloj: () => DateTime(2026, 6, 1, 6, 0)), + android: FakePuertoAlarmasAndroid(), + iniciarAutomaticamente: false, + ); + addTearDown(estado.dispose); + addTearDown(alarmas.dispose); + final l10n = await AppLocalizations.delegate.load(const Locale('en')); + + await tester.pumpWidget( + testApp(estado, const PantallaAlarmas(), alarmas: alarmas), + ); + await pumpStable(tester); + + expect(find.text(l10n.alarmScreenSubtitle), findsNothing); + expect(find.byType(PluriRootHeader), findsOneWidget); + expect(find.text(l10n.createAlarmAction), findsOneWidget); + }, + ); + + testWidgets('Ajustes: the retired hero subtitle is gone', (tester) async { + _suppressListTileInkAssertion(); + setLargeSurface(tester); + final estado = crearEstadoRadio(); + addTearDown(estado.dispose); + final l10n = await AppLocalizations.delegate.load(const Locale('en')); + + await tester.pumpWidget(testApp(estado, const PantallaAjustes())); + await pumpStable(tester); + + expect(find.text(l10n.settingsSubtitle), findsNothing); + expect(find.byType(PluriRootHeader), findsOneWidget); + }); +} diff --git a/test/pantallas/root_header_wiring_test.dart b/test/pantallas/root_header_wiring_test.dart index 6c13b33..eba2982 100644 --- a/test/pantallas/root_header_wiring_test.dart +++ b/test/pantallas/root_header_wiring_test.dart @@ -6,6 +6,7 @@ import 'package:pluriwave/estado/estado_alarmas.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_idioma.dart'; import 'package:pluriwave/estado/estado_radio.dart'; import 'package:pluriwave/l10n/gen/app_localizations.dart'; import 'package:pluriwave/pantallas/pantalla_ajustes.dart'; @@ -24,11 +25,9 @@ import '../helpers/fakes_alarmas.dart'; /// S1 (Tier 1 visual fidelity): `app.dart`'s `PluriWaveScaffold(appBar: /// AppBar(...))` is gone — every root now draws its own [PluriRootHeader] /// instead, matching the prototype (no screen in `t4` shows a global -/// `AppBar`). Each root still ALSO carries its pre-existing -/// `PluriScreenHeader` hero (S2 in this same Tier 1 batch replaces that -/// separately), so title assertions below are scoped to [PluriRootHeader]'s -/// own subtree — the title string itself still appears twice on screen -/// until S2 lands. +/// `AppBar`). [PluriRootHeader]'s own title text is asserted via +/// `find.descendant` throughout — see `pluri_screen_header_retired_test.dart` +/// for S2's separate assertion that the old glass-hero subtitle is gone. /// /// Pre-existing project constraint (see `pantalla_ajustes_test.dart`): /// PluriGlassSurface paints a background over ListTile's ink layer, which @@ -72,6 +71,7 @@ void main() { ListenableProvider.value(value: estado.ecualizador), ListenableProvider.value(value: estado.grabacion), ListenableProvider.value(value: estado.busqueda), + ChangeNotifierProvider.value(value: EstadoIdioma()), if (alarmas != null) ChangeNotifierProvider.value(value: alarmas), ], diff --git a/test/widgets/pluri_root_header_test.dart b/test/widgets/pluri_root_header_test.dart index 654ae0a..eba0885 100644 --- a/test/widgets/pluri_root_header_test.dart +++ b/test/widgets/pluri_root_header_test.dart @@ -2,6 +2,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:pluriwave/l10n/gen/app_localizations.dart'; import 'package:pluriwave/tema/pluriwave_theme.dart'; +import 'package:pluriwave/widgets/pluri_layout.dart'; import 'package:pluriwave/widgets/pluri_root_header.dart'; /// S1 (Tier 1 visual fidelity): the prototype draws no global `AppBar` — @@ -34,6 +35,19 @@ void main() { expect(size.height, 56); }); + testWidgets('S5: uses the title tier (20px) on the left, matching the ' + "prototype's own header padding (t4 e.g. Alarmas " + '`padding:0 12px 0 20px`)', (tester) async { + await tester.pumpWidget( + host(PluriRootHeader(title: 'Settings', onSleepTimer: () {})), + ); + + final padding = tester.widget(find.byType(Padding).first); + final insets = padding.padding as EdgeInsets; + expect(insets.left, PluriLayout.titleHorizontal); + expect(insets.right, PluriLayout.rowHorizontal); + }); + testWidgets('exposes a bedtime action that invokes onSleepTimer when ' 'tapped', (tester) async { var tapped = false; @@ -55,4 +69,32 @@ void main() { expect(find.byType(AppBar), findsNothing); }); + + testWidgets('S2: renders optional actions BEFORE the bedtime button — a few ' + "roots' single functional action (e.g. Alarmas' create-alarm button, " + "Buscar's filters entry point) that used to live on the now-retired " + 'PluriScreenHeader', (tester) async { + await tester.pumpWidget( + host( + PluriRootHeader( + title: 'Alarms', + onSleepTimer: () {}, + actions: [ + IconButton(icon: const Icon(Icons.add_rounded), onPressed: () {}), + ], + ), + ), + ); + + expect(find.byIcon(Icons.add_rounded), findsOneWidget); + expect(find.byIcon(Icons.bedtime_outlined), findsOneWidget); + + final actionRect = tester.getRect(find.byIcon(Icons.add_rounded)); + final bedtimeRect = tester.getRect(find.byIcon(Icons.bedtime_outlined)); + expect( + actionRect.left, + lessThan(bedtimeRect.left), + reason: 'actions render before (to the left of) the bedtime button', + ); + }); }