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.
206 lines
7.3 KiB
Dart
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);
|
|
},
|
|
);
|
|
});
|
|
}
|