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` — /// each root instead owns a plain 56px title row inside its own content /// (`t4`, e.g. Alarmas line 325 `height:56px`, Ajustes line 511, Explorar /// line 641). [PluriRootHeader] is that row, shared by all 5 roots so the /// sleep-timer action that used to live on `app.dart`'s single global /// `AppBar` stays reachable from every tab. void main() { // [topInset] simulates `MediaQuery.paddingOf(context).top` (status bar / // camera cutout) — the same technique already used by // `pantalla_alarma_sonando_scaffold_test.dart`'s `MediaQuery` override via // `MaterialApp.builder`. Widget host(Widget child, {double topInset = 0}) { return MaterialApp( theme: PluriWaveTheme.dark(), locale: const Locale('en'), localizationsDelegates: AppLocalizations.localizationsDelegates, supportedLocales: AppLocalizations.supportedLocales, builder: (context, app) => MediaQuery( data: MediaQuery.of( context, ).copyWith(padding: EdgeInsets.only(top: topInset)), child: app!, ), home: Scaffold(body: child), ); } testWidgets('renders the given title and is exactly 56px tall', ( tester, ) async { await tester.pumpWidget( host(PluriRootHeader(title: 'Settings', onSleepTimer: () {})), ); expect(find.text('Settings'), findsOneWidget); expect(PluriRootHeader.height, 56); final size = tester.getSize(find.byType(PluriRootHeader)); 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: () {})), ); // Keyed lookup (not `find.byType(Padding).first`): the top-inset fix // wraps this content padding in an outer `Padding(top: topInset)`, so // `.first` would no longer reliably resolve to the content row's own // padding. final padding = tester.widget( find.byKey(const ValueKey('pluri-root-header-content')), ); 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; await tester.pumpWidget( host(PluriRootHeader(title: 'Alarms', onSleepTimer: () => tapped = true)), ); expect(find.byIcon(Icons.bedtime_outlined), findsOneWidget); await tester.tap(find.byIcon(Icons.bedtime_outlined)); await tester.pump(); expect(tapped, isTrue); }); testWidgets('never builds a Material AppBar', (tester) async { await tester.pumpWidget( host(PluriRootHeader(title: 'Search signal', onSleepTimer: () {})), ); 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', ); }); group('fix top-inset: total height and content position track ' 'MediaQuery.paddingOf(context).top, instead of always sitting flush ' 'at y=0 under the status bar', () { // Vertical text centering inside the 56px content row means the // title's own top-left never sits exactly AT the header's top-left // (even at topInset=0) — so these tests compare each inset's title // position against the topInset=0 BASELINE, isolating exactly the // inset's own contribution instead of asserting a brittle absolute // offset. Future tituloTopPara(WidgetTester tester, double topInset) async { await tester.pumpWidget( host( PluriRootHeader(title: 'Settings', onSleepTimer: () {}), topInset: topInset, ), ); return tester.getTopLeft(find.text('Settings')).dy; } testWidgets( 'topInset=0 (e.g. desktop/no cutout): total height stays the plain ' '56px content height', (tester) async { await tester.pumpWidget( host(PluriRootHeader(title: 'Settings', onSleepTimer: () {})), ); expect(tester.getSize(find.byType(PluriRootHeader)).height, 56); }, ); testWidgets( 'topInset=24 (typical status bar): total height becomes 56+24=80, ' 'and the title shifts down by exactly the inset relative to the ' 'topInset=0 baseline', (tester) async { final base = await tituloTopPara(tester, 0); final conInset = await tituloTopPara(tester, 24); expect(tester.getSize(find.byType(PluriRootHeader)).height, 80); expect(conInset - base, 24); }, ); testWidgets( 'topInset=44 (taller status bar): total height becomes 56+44=100, ' 'and the title still shifts down by exactly the inset — never ' 'clipped', (tester) async { final base = await tituloTopPara(tester, 0); final conInset = await tituloTopPara(tester, 44); expect(tester.getSize(find.byType(PluriRootHeader)).height, 100); expect(conInset - base, 44); }, ); testWidgets( 'topInset=60 (notch/camera-cutout simulation): total height becomes ' '56+60=116, and the title still fully clears the inset — never ' 'overlapping it', (tester) async { final base = await tituloTopPara(tester, 0); final conInset = await tituloTopPara(tester, 60); expect(tester.getSize(find.byType(PluriRootHeader)).height, 116); expect(conInset - base, 60); }, ); testWidgets( 'PluriRootHeader.height stays the CONTENT height (56) regardless of ' 'inset — callers doing total-height math must separately add ' 'MediaQuery.paddingOf(context).top', (tester) async { await tester.pumpWidget( host( PluriRootHeader(title: 'Settings', onSleepTimer: () {}), topInset: 44, ), ); expect(PluriRootHeader.height, 56); expect(tester.getSize(find.byType(PluriRootHeader)).height, 100); }, ); }); }