Files
pluriwave/test/widgets/pluri_root_header_test.dart
T
FreeTLab 015a20a823 feat(tutorial): add 9-screen help/tutorial carousel
Add PantallaTutorialAyuda, a PageView-based carousel covering saved
stations/groups, per-station equalizer, recording, adaptive alarms,
Android Auto favorites, auto-reconnect, snooze duration, custom
stations, and a closing summary with a "watch it again" reminder.

ServicioTutorialAyuda persists a one-time seen flag so the carousel
shows once via mostrarSiProcede, independent of entry point; the
final page's CTA label depends on the primerArranque constructor
parameter ("Empezar a escuchar" vs "Cerrar").

Translate the new copy into all 13 supported locales and update
helpSubtitle to describe the new entry point.
2026-08-01 11:34:53 +02:00

206 lines
7.3 KiB
Dart

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<Padding>(
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<double> 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);
},
);
});
}