fix(chrome): retire PluriScreenHeader, it has no equivalent in the prototype

PluriScreenHeader was a 38-radius glass hero: an aurora banner at 24%
opacity, a black-to-transparent scrim, two radial orbs, a 120px app-mark
watermark and a 56px tri-gradient glyph badge. None of it is in the
prototype (t4) -- every root's title is plain text in its own 56px
row, which PluriRootHeader (S1) already provides. Delete the class
(and its now-orphaned _Orb helper) and its four call sites (Buscar,
Favoritos x2, Alarmas, Ajustes landed with S8).

Two of those call sites carried the hero's only functional bit besides
the title, so PluriRootHeader gains an `actions` slot (rendered before
the shared bedtime button) to keep them reachable:
  - Alarmas' create-alarm button (FilledButton.tonalIcon, unchanged
    shape, just relocated)
  - Buscar's filters entry point (the same tappable PluriStatusPill,
    just relocated)
Every other retired trailing pill (Ajustes' "Secure" status,
Favoritos' collection-count badges, Alarmas' alarm-count badge) was
purely decorative and matches the prototype by simply disappearing.

S2, Tier 1 visual-fidelity pass (audit id 2521).
This commit is contained in:
2026-07-29 21:29:49 +02:00
parent 7ebe0b77a4
commit 97e38becfe
8 changed files with 268 additions and 282 deletions
+11 -13
View File
@@ -14,9 +14,7 @@ import '../tema/pluriwave_theme.dart';
import '../tema/pluriwave_tokens.dart'; import '../tema/pluriwave_tokens.dart';
import '../widgets/editor_hora_inline.dart'; import '../widgets/editor_hora_inline.dart';
import '../widgets/pluri_glass_surface.dart'; import '../widgets/pluri_glass_surface.dart';
import '../widgets/pluri_icon.dart';
import '../widgets/pluri_layout.dart'; import '../widgets/pluri_layout.dart';
import '../widgets/pluri_premium_widgets.dart';
import '../widgets/pluri_push_scaffold.dart'; import '../widgets/pluri_push_scaffold.dart';
import '../widgets/pluri_root_header.dart'; import '../widgets/pluri_root_header.dart';
import '../widgets/pluri_sleep_timer_sheet.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 // AppBar — this root now draws its own 56px title row instead of
// relying on app.dart's removed shared chrome (which is also // relying on app.dart's removed shared chrome (which is also
// where the sleep-timer action used to live). // 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( PluriRootHeader(
title: l10n.alarmScreenTitle, title: l10n.alarmScreenTitle,
onSleepTimer: () => showPluriSleepTimerSheet(context), onSleepTimer: () => showPluriSleepTimerSheet(context),
), actions: [
PluriScreenHeader( FilledButton.tonalIcon(
title: l10n.alarmScreenTitle, onPressed: () => _abrirEditor(context),
subtitle: l10n.alarmScreenSubtitle, icon: const Icon(Icons.auto_awesome_rounded, size: 18),
glyph: PluriIconGlyph.alarm, label: Text(l10n.createAlarmAction),
primaryActionLabel: l10n.createAlarmAction, ),
onPrimaryAction: () => _abrirEditor(context), ],
trailing: PluriStatusPill(
icon: Icons.alarm_on_rounded,
label: l10n.alarmsCount(estado.alarmas.length),
),
), ),
Padding( Padding(
padding: PluriLayout.pageContentPadding, padding: PluriLayout.pageContentPadding,
+13 -15
View File
@@ -142,25 +142,23 @@ class _PantallaBuscarState extends State<PantallaBuscar> {
return ListView( return ListView(
padding: PluriLayout.pageListPadding, padding: PluriLayout.pageListPadding,
children: [ children: [
// S1 (Tier 1 visual fidelity): the prototype has no global AppBar — // S1/S2 (Tier 1 visual fidelity): the prototype has no global
// this root now draws its own 56px title row instead of relying on // AppBar and no PluriScreenHeader (the glass hero this used to be
// app.dart's removed shared chrome (which is also where the // is retired everywhere) — this root draws its own 56px title row
// sleep-timer action used to live). // instead, carrying the filters entry point that used to live on
// the hero's `trailing` slot.
PluriRootHeader( PluriRootHeader(
title: l10n.searchScreenTitle, title: l10n.searchScreenTitle,
onSleepTimer: () => showPluriSleepTimerSheet(context), onSleepTimer: () => showPluriSleepTimerSheet(context),
), actions: [
PluriScreenHeader( GestureDetector(
title: l10n.searchScreenTitle, onTap: _abrirFiltros,
subtitle: l10n.searchScreenSubtitle, child: PluriStatusPill(
glyph: PluriIconGlyph.search, icon: Icons.tune_rounded,
trailing: GestureDetector( label: l10n.searchFiltersLabel,
onTap: _abrirFiltros, ),
child: PluriStatusPill(
icon: Icons.tune_rounded,
label: l10n.searchFiltersLabel,
), ),
), ],
), ),
Padding( Padding(
padding: const EdgeInsets.fromLTRB( padding: const EdgeInsets.fromLTRB(
+5 -19
View File
@@ -119,19 +119,13 @@ class _PantallaFavoritosState extends State<PantallaFavoritos> {
// AppBar — this root now draws its own 56px title row instead of // AppBar — this root now draws its own 56px title row instead of
// relying on app.dart's removed shared chrome (which is also // relying on app.dart's removed shared chrome (which is also
// where the sleep-timer action used to live). // 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( PluriRootHeader(
title: l10n.favoritesTitle, title: l10n.favoritesTitle,
onSleepTimer: () => showPluriSleepTimerSheet(context), onSleepTimer: () => showPluriSleepTimerSheet(context),
), ),
PluriScreenHeader(
title: l10n.favoritesTitle,
subtitle: l10n.favoritesHeaderSubtitle,
glyph: PluriIconGlyph.favorites,
trailing: PluriStatusPill(
icon: Icons.favorite_rounded,
label: l10n.favoritesCollection,
),
),
SizedBox( SizedBox(
height: 320, height: 320,
child: PluriEmptyState( child: PluriEmptyState(
@@ -190,20 +184,12 @@ class _PantallaFavoritosState extends State<PantallaFavoritos> {
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ 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( PluriRootHeader(
title: l10n.favoritesTitle, title: l10n.favoritesTitle,
onSleepTimer: () => showPluriSleepTimerSheet(context), 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), const SizedBox(height: 12),
Row( Row(
children: [ children: [
-229
View File
@@ -1,217 +1,9 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import '../tema/pluriwave_theme.dart'; import '../tema/pluriwave_theme.dart';
import '../tema/pluriwave_tokens.dart';
import 'pluri_glass_surface.dart'; import 'pluri_glass_surface.dart';
import 'pluri_icon.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 { class PluriStatusPill extends StatelessWidget {
const PluriStatusPill({ const PluriStatusPill({
super.key, 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)]),
),
),
);
}
}
+19 -1
View File
@@ -2,6 +2,7 @@ import 'package:flutter/material.dart';
import '../l10n/gen/app_localizations.dart'; import '../l10n/gen/app_localizations.dart';
import '../tema/pluriwave_theme.dart'; import '../tema/pluriwave_theme.dart';
import 'pluri_layout.dart';
/// S1 (Tier 1 visual fidelity): the prototype (`t4`) draws no global /// S1 (Tier 1 visual fidelity): the prototype (`t4`) draws no global
/// `AppBar` — each root paints its own 56px title row inside its own /// `AppBar` — each root paints its own 56px title row inside its own
@@ -15,11 +16,19 @@ class PluriRootHeader extends StatelessWidget {
super.key, super.key,
required this.title, required this.title,
required this.onSleepTimer, required this.onSleepTimer,
this.actions = const <Widget>[],
}); });
final String title; final String title;
final VoidCallback onSleepTimer; 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<Widget> actions;
static const double height = 56; static const double height = 56;
@override @override
@@ -29,7 +38,15 @@ class PluriRootHeader extends StatelessWidget {
return SizedBox( return SizedBox(
height: height, height: height,
child: Padding( 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( child: Row(
children: [ children: [
Expanded( Expanded(
@@ -40,6 +57,7 @@ class PluriRootHeader extends StatelessWidget {
style: type.sectionTitle, style: type.sectionTitle,
), ),
), ),
...actions,
IconButton( IconButton(
icon: const Icon(Icons.bedtime_outlined), icon: const Icon(Icons.bedtime_outlined),
tooltip: l10n.sleepTimer, tooltip: l10n.sleepTimer,
@@ -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<File> 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<EstadoRadio>.value(value: estado),
ListenableProvider<EstadoEcualizador>.value(value: estado.ecualizador),
ListenableProvider<EstadoGrabacion>.value(value: estado.grabacion),
ListenableProvider<EstadoBusqueda>.value(value: estado.busqueda),
ChangeNotifierProvider<EstadoIdioma>.value(value: EstadoIdioma()),
if (alarmas != null)
ChangeNotifierProvider<EstadoAlarmas>.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<void> 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);
});
}
+5 -5
View File
@@ -6,6 +6,7 @@ import 'package:pluriwave/estado/estado_alarmas.dart';
import 'package:pluriwave/estado/estado_busqueda.dart'; import 'package:pluriwave/estado/estado_busqueda.dart';
import 'package:pluriwave/estado/estado_ecualizador.dart'; import 'package:pluriwave/estado/estado_ecualizador.dart';
import 'package:pluriwave/estado/estado_grabacion.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/estado/estado_radio.dart';
import 'package:pluriwave/l10n/gen/app_localizations.dart'; import 'package:pluriwave/l10n/gen/app_localizations.dart';
import 'package:pluriwave/pantallas/pantalla_ajustes.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: /// S1 (Tier 1 visual fidelity): `app.dart`'s `PluriWaveScaffold(appBar:
/// AppBar(...))` is gone — every root now draws its own [PluriRootHeader] /// AppBar(...))` is gone — every root now draws its own [PluriRootHeader]
/// instead, matching the prototype (no screen in `t4` shows a global /// instead, matching the prototype (no screen in `t4` shows a global
/// `AppBar`). Each root still ALSO carries its pre-existing /// `AppBar`). [PluriRootHeader]'s own title text is asserted via
/// `PluriScreenHeader` hero (S2 in this same Tier 1 batch replaces that /// `find.descendant` throughout — see `pluri_screen_header_retired_test.dart`
/// separately), so title assertions below are scoped to [PluriRootHeader]'s /// for S2's separate assertion that the old glass-hero subtitle is gone.
/// own subtree — the title string itself still appears twice on screen
/// until S2 lands.
/// ///
/// Pre-existing project constraint (see `pantalla_ajustes_test.dart`): /// Pre-existing project constraint (see `pantalla_ajustes_test.dart`):
/// PluriGlassSurface paints a background over ListTile's ink layer, which /// PluriGlassSurface paints a background over ListTile's ink layer, which
@@ -72,6 +71,7 @@ void main() {
ListenableProvider<EstadoEcualizador>.value(value: estado.ecualizador), ListenableProvider<EstadoEcualizador>.value(value: estado.ecualizador),
ListenableProvider<EstadoGrabacion>.value(value: estado.grabacion), ListenableProvider<EstadoGrabacion>.value(value: estado.grabacion),
ListenableProvider<EstadoBusqueda>.value(value: estado.busqueda), ListenableProvider<EstadoBusqueda>.value(value: estado.busqueda),
ChangeNotifierProvider<EstadoIdioma>.value(value: EstadoIdioma()),
if (alarmas != null) if (alarmas != null)
ChangeNotifierProvider<EstadoAlarmas>.value(value: alarmas), ChangeNotifierProvider<EstadoAlarmas>.value(value: alarmas),
], ],
+42
View File
@@ -2,6 +2,7 @@ import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_test/flutter_test.dart';
import 'package:pluriwave/l10n/gen/app_localizations.dart'; import 'package:pluriwave/l10n/gen/app_localizations.dart';
import 'package:pluriwave/tema/pluriwave_theme.dart'; import 'package:pluriwave/tema/pluriwave_theme.dart';
import 'package:pluriwave/widgets/pluri_layout.dart';
import 'package:pluriwave/widgets/pluri_root_header.dart'; import 'package:pluriwave/widgets/pluri_root_header.dart';
/// S1 (Tier 1 visual fidelity): the prototype draws no global `AppBar` — /// S1 (Tier 1 visual fidelity): the prototype draws no global `AppBar` —
@@ -34,6 +35,19 @@ void main() {
expect(size.height, 56); 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<Padding>(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 ' testWidgets('exposes a bedtime action that invokes onSleepTimer when '
'tapped', (tester) async { 'tapped', (tester) async {
var tapped = false; var tapped = false;
@@ -55,4 +69,32 @@ void main() {
expect(find.byType(AppBar), findsNothing); 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',
);
});
} }