feat(tokens): add design tokens, type scale, push scaffold, and root nav state
This commit is contained in:
+13
-8
@@ -8,6 +8,7 @@ import 'estado/estado_grabacion.dart';
|
||||
import 'estado/estado_radio.dart';
|
||||
import 'estado/estado_alarmas.dart';
|
||||
import 'estado/estado_idioma.dart';
|
||||
import 'estado/estado_navegacion.dart';
|
||||
import 'l10n/display_names.dart';
|
||||
import 'l10n/gen/app_localizations.dart';
|
||||
import 'modelos/alarma_musical.dart';
|
||||
@@ -70,6 +71,9 @@ class PluriWaveApp extends StatelessWidget {
|
||||
ChangeNotifierProvider(
|
||||
create: (_) => EstadoIdioma(sharedPreferences: prefs),
|
||||
),
|
||||
// Design ADR-8: root-to-root navigation state. `_PaginaPrincipal`
|
||||
// watches this instead of owning `_indice` locally.
|
||||
ChangeNotifierProvider(create: (_) => EstadoNavegacionRaiz()),
|
||||
],
|
||||
child: Consumer<EstadoIdioma>(
|
||||
builder:
|
||||
@@ -98,7 +102,6 @@ class _PaginaPrincipal extends StatefulWidget {
|
||||
|
||||
class _PaginaPrincipalState extends State<_PaginaPrincipal>
|
||||
with WidgetsBindingObserver {
|
||||
int _indice = 0;
|
||||
StreamSubscription<String>? _errorSubscription;
|
||||
StreamSubscription<EventoAlarmaAndroid>? _alarmaSubscription;
|
||||
StreamSubscription<AlarmaMusical>? _alarmaVencidaSubscription;
|
||||
@@ -206,6 +209,8 @@ class _PaginaPrincipalState extends State<_PaginaPrincipal>
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final l10n = AppLocalizations.of(context);
|
||||
final navegacion = context.watch<EstadoNavegacionRaiz>();
|
||||
final indice = navegacion.indice;
|
||||
|
||||
return PluriWaveScaffold(
|
||||
appBar: AppBar(
|
||||
@@ -236,8 +241,8 @@ class _PaginaPrincipalState extends State<_PaginaPrincipal>
|
||||
),
|
||||
),
|
||||
child: KeyedSubtree(
|
||||
key: ValueKey<int>(_indice),
|
||||
child: _paginas[_indice],
|
||||
key: ValueKey<int>(indice),
|
||||
child: _paginas[indice],
|
||||
),
|
||||
),
|
||||
),
|
||||
@@ -252,8 +257,8 @@ class _PaginaPrincipalState extends State<_PaginaPrincipal>
|
||||
const MiniReproductor(),
|
||||
PluriBottomNavigation(
|
||||
items: _navItems(l10n),
|
||||
selectedIndex: _indice,
|
||||
onSelected: (i) => setState(() => _indice = i),
|
||||
selectedIndex: indice,
|
||||
onSelected: (i) => navegacion.irA(RaizPluriWave.values[i]),
|
||||
),
|
||||
],
|
||||
),
|
||||
@@ -307,7 +312,7 @@ class _PaginaPrincipalState extends State<_PaginaPrincipal>
|
||||
if (evento.accion.endsWith('.SKIP_NEXT')) {
|
||||
await estado.saltarProxima(alarma.id);
|
||||
if (!mounted) return;
|
||||
setState(() => _indice = 3);
|
||||
context.read<EstadoNavegacionRaiz>().irA(RaizPluriWave.alarmas);
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(
|
||||
@@ -330,7 +335,7 @@ class _PaginaPrincipalState extends State<_PaginaPrincipal>
|
||||
ejecucion,
|
||||
);
|
||||
if (!mounted) return;
|
||||
setState(() => _indice = 3);
|
||||
context.read<EstadoNavegacionRaiz>().irA(RaizPluriWave.alarmas);
|
||||
// posponerProximaDesdePreaviso no longer throws on a native scheduling
|
||||
// failure — it records the failure into EstadoAlarmas.error instead.
|
||||
// Branch on it here so the user sees the real outcome instead of an
|
||||
@@ -347,7 +352,7 @@ class _PaginaPrincipalState extends State<_PaginaPrincipal>
|
||||
return;
|
||||
}
|
||||
if (evento.accion.endsWith('.PRE_NOTICE')) {
|
||||
setState(() => _indice = 3);
|
||||
context.read<EstadoNavegacionRaiz>().irA(RaizPluriWave.alarmas);
|
||||
return;
|
||||
}
|
||||
await _mostrarAlarmaSonando(alarma);
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
import 'package:flutter/foundation.dart';
|
||||
|
||||
/// The five root tabs, in their declared (and displayed) order. Declaration
|
||||
/// order IS the tab order — the single source shared by `_paginas` and
|
||||
/// `_navItems` in app.dart (Design ADR-8).
|
||||
enum RaizPluriWave { escuchar, buscar, favoritos, alarmas, ajustes }
|
||||
|
||||
/// Root-to-root navigation state (Design ADR-8). The single source of truth
|
||||
/// for which of the 5 root tabs is active. Switching roots is a plain
|
||||
/// notifier update — it never pushes a route.
|
||||
class EstadoNavegacionRaiz extends ChangeNotifier {
|
||||
RaizPluriWave _actual = RaizPluriWave.escuchar;
|
||||
|
||||
RaizPluriWave get actual => _actual;
|
||||
|
||||
/// Declaration order doubles as the bottom-nav index.
|
||||
int get indice => _actual.index;
|
||||
|
||||
void irA(RaizPluriWave raiz) {
|
||||
if (raiz == _actual) return;
|
||||
_actual = raiz;
|
||||
notifyListeners();
|
||||
}
|
||||
}
|
||||
+1
-1
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"@@locale": "en",
|
||||
"appTitle": "PluriWave",
|
||||
"navHome": "Home",
|
||||
"navHome": "Listen",
|
||||
"navSearch": "Search",
|
||||
"navFavorites": "Favorites",
|
||||
"navAlarms": "Alarms",
|
||||
|
||||
+1
-1
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"@@locale": "es",
|
||||
"appTitle": "PluriWave",
|
||||
"navHome": "Inicio",
|
||||
"navHome": "Escuchar",
|
||||
"navSearch": "Buscar",
|
||||
"navFavorites": "Favoritos",
|
||||
"navAlarms": "Alarmas",
|
||||
|
||||
@@ -129,7 +129,7 @@ abstract class AppLocalizations {
|
||||
/// No description provided for @navHome.
|
||||
///
|
||||
/// In es, this message translates to:
|
||||
/// **'Inicio'**
|
||||
/// **'Escuchar'**
|
||||
String get navHome;
|
||||
|
||||
/// No description provided for @navSearch.
|
||||
|
||||
@@ -12,7 +12,7 @@ class AppLocalizationsEn extends AppLocalizations {
|
||||
String get appTitle => 'PluriWave';
|
||||
|
||||
@override
|
||||
String get navHome => 'Home';
|
||||
String get navHome => 'Listen';
|
||||
|
||||
@override
|
||||
String get navSearch => 'Search';
|
||||
|
||||
@@ -12,7 +12,7 @@ class AppLocalizationsEs extends AppLocalizations {
|
||||
String get appTitle => 'PluriWave';
|
||||
|
||||
@override
|
||||
String get navHome => 'Inicio';
|
||||
String get navHome => 'Escuchar';
|
||||
|
||||
@override
|
||||
String get navSearch => 'Buscar';
|
||||
|
||||
@@ -3,16 +3,21 @@ import 'package:google_fonts/google_fonts.dart';
|
||||
|
||||
import 'pluriwave_motion.dart';
|
||||
import 'pluriwave_tokens.dart';
|
||||
import 'pluriwave_typography.dart';
|
||||
|
||||
abstract final class PluriWaveTheme {
|
||||
static ThemeData dark() {
|
||||
const tokens = PluriWaveTokens.dark;
|
||||
final baseTextTheme = GoogleFonts.plusJakartaSansTextTheme(
|
||||
ThemeData.dark().textTheme,
|
||||
);
|
||||
final typography = PluriWaveTypography.from(baseTextTheme);
|
||||
final colorScheme = const ColorScheme.dark().copyWith(
|
||||
primary: tokens.electricMagenta,
|
||||
secondary: const Color(0xFF7EE4C2),
|
||||
secondary: tokens.liveGreen,
|
||||
tertiary: tokens.warmCoral,
|
||||
surface: const Color(0xFF0D1B24),
|
||||
surfaceContainerLow: const Color(0xFF102532),
|
||||
surfaceContainerLow: tokens.listSurface,
|
||||
surfaceContainerHighest: const Color(0xFF1B3942),
|
||||
onSurface: const Color(0xFFF2F7FA),
|
||||
onPrimary: Colors.white,
|
||||
@@ -22,10 +27,12 @@ abstract final class PluriWaveTheme {
|
||||
useMaterial3: true,
|
||||
colorScheme: colorScheme,
|
||||
scaffoldBackgroundColor: tokens.deepViolet,
|
||||
textTheme: GoogleFonts.plusJakartaSansTextTheme(
|
||||
ThemeData.dark().textTheme,
|
||||
),
|
||||
extensions: const <ThemeExtension<dynamic>>[tokens, PluriWaveMotion.dark],
|
||||
textTheme: baseTextTheme,
|
||||
extensions: <ThemeExtension<dynamic>>[
|
||||
tokens,
|
||||
PluriWaveMotion.dark,
|
||||
typography,
|
||||
],
|
||||
appBarTheme: const AppBarTheme(
|
||||
centerTitle: false,
|
||||
backgroundColor: Colors.transparent,
|
||||
@@ -69,4 +76,8 @@ extension PluriWaveThemeContextX on BuildContext {
|
||||
|
||||
PluriWaveMotion get pluriMotion =>
|
||||
Theme.of(this).extension<PluriWaveMotion>() ?? PluriWaveMotion.dark;
|
||||
|
||||
PluriWaveTypography get pluriType =>
|
||||
Theme.of(this).extension<PluriWaveTypography>() ??
|
||||
PluriWaveTypography.from(Theme.of(this).textTheme);
|
||||
}
|
||||
|
||||
@@ -11,6 +11,9 @@ class PluriWaveTokens extends ThemeExtension<PluriWaveTokens> {
|
||||
required this.glassSurface,
|
||||
required this.glassBorder,
|
||||
required this.glowColor,
|
||||
required this.listSurface,
|
||||
required this.liveGreen,
|
||||
required this.offlineAccent,
|
||||
required this.radiusSm,
|
||||
required this.radiusMd,
|
||||
required this.radiusLg,
|
||||
@@ -27,6 +30,21 @@ class PluriWaveTokens extends ThemeExtension<PluriWaveTokens> {
|
||||
final Color glassBorder;
|
||||
final Color glowColor;
|
||||
|
||||
/// Redesign additions (Design ADR-1). Row/card surface for the flat lists
|
||||
/// introduced by the redesign (Favoritos, Buscar results). De-literalised
|
||||
/// from `pluriwave_theme.dart`'s previous `surfaceContainerLow` literal —
|
||||
/// same value, now named — so rendered output stays byte-identical.
|
||||
final Color listSurface;
|
||||
|
||||
/// "EN DIRECTO" indicator and the Escuchar hero's waveform colour.
|
||||
/// De-literalised from `pluriwave_theme.dart`'s previous `secondary`
|
||||
/// literal — same value, now named.
|
||||
final Color liveGreen;
|
||||
|
||||
/// Offline/reconnect banner accent (WU16). No prior literal to promote —
|
||||
/// a genuinely new colour.
|
||||
final Color offlineAccent;
|
||||
|
||||
final double radiusSm;
|
||||
final double radiusMd;
|
||||
final double radiusLg;
|
||||
@@ -53,6 +71,9 @@ class PluriWaveTokens extends ThemeExtension<PluriWaveTokens> {
|
||||
glassSurface: Color(0x1FFFFFFF),
|
||||
glassBorder: Color(0x33FFFFFF),
|
||||
glowColor: Color(0x6621D4D9),
|
||||
listSurface: Color(0xFF102532),
|
||||
liveGreen: Color(0xFF7EE4C2),
|
||||
offlineAccent: Color(0xFF94A3B8),
|
||||
radiusSm: 14,
|
||||
radiusMd: 22,
|
||||
radiusLg: 30,
|
||||
@@ -70,6 +91,9 @@ class PluriWaveTokens extends ThemeExtension<PluriWaveTokens> {
|
||||
Color? glassSurface,
|
||||
Color? glassBorder,
|
||||
Color? glowColor,
|
||||
Color? listSurface,
|
||||
Color? liveGreen,
|
||||
Color? offlineAccent,
|
||||
double? radiusSm,
|
||||
double? radiusMd,
|
||||
double? radiusLg,
|
||||
@@ -85,6 +109,9 @@ class PluriWaveTokens extends ThemeExtension<PluriWaveTokens> {
|
||||
glassSurface: glassSurface ?? this.glassSurface,
|
||||
glassBorder: glassBorder ?? this.glassBorder,
|
||||
glowColor: glowColor ?? this.glowColor,
|
||||
listSurface: listSurface ?? this.listSurface,
|
||||
liveGreen: liveGreen ?? this.liveGreen,
|
||||
offlineAccent: offlineAccent ?? this.offlineAccent,
|
||||
radiusSm: radiusSm ?? this.radiusSm,
|
||||
radiusMd: radiusMd ?? this.radiusMd,
|
||||
radiusLg: radiusLg ?? this.radiusLg,
|
||||
@@ -111,6 +138,10 @@ class PluriWaveTokens extends ThemeExtension<PluriWaveTokens> {
|
||||
Color.lerp(glassSurface, other.glassSurface, t) ?? glassSurface,
|
||||
glassBorder: Color.lerp(glassBorder, other.glassBorder, t) ?? glassBorder,
|
||||
glowColor: Color.lerp(glowColor, other.glowColor, t) ?? glowColor,
|
||||
listSurface: Color.lerp(listSurface, other.listSurface, t) ?? listSurface,
|
||||
liveGreen: Color.lerp(liveGreen, other.liveGreen, t) ?? liveGreen,
|
||||
offlineAccent:
|
||||
Color.lerp(offlineAccent, other.offlineAccent, t) ?? offlineAccent,
|
||||
radiusSm: lerpDouble(radiusSm, other.radiusSm, t) ?? radiusSm,
|
||||
radiusMd: lerpDouble(radiusMd, other.radiusMd, t) ?? radiusMd,
|
||||
radiusLg: lerpDouble(radiusLg, other.radiusLg, t) ?? radiusLg,
|
||||
|
||||
@@ -0,0 +1,111 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
/// Design ADR-1: the named type scale, split out of [PluriWaveTokens] into
|
||||
/// its own ThemeExtension so screens stop copy-pasting
|
||||
/// `.copyWith(fontWeight: w900, letterSpacing: …)`. The set is closed at six
|
||||
/// styles — a seventh requires amending ADR-1.
|
||||
///
|
||||
/// [eyebrowLabel] never applies `toUpperCase()` — casing is a
|
||||
/// locale-hostile transform (Turkish dotless i, and scripts with no case at
|
||||
/// all). The style carries weight/letter-spacing only; each ARB value is
|
||||
/// authored in its own display form.
|
||||
@immutable
|
||||
class PluriWaveTypography extends ThemeExtension<PluriWaveTypography> {
|
||||
const PluriWaveTypography({
|
||||
required this.heroTime,
|
||||
required this.sectionTitle,
|
||||
required this.screenTitle,
|
||||
required this.cardTitle,
|
||||
required this.bodyStrong,
|
||||
required this.eyebrowLabel,
|
||||
});
|
||||
|
||||
/// 88 / w800 / height 1.0 / ls -2.0. Alarm ringing, inline time editor.
|
||||
/// Every call site must wrap this in `FittedBox(fit: BoxFit.scaleDown)` —
|
||||
/// it renders at 176px under a 2.0 text scaler.
|
||||
final TextStyle heroTime;
|
||||
|
||||
/// 23 / w800 / ls -0.6. Root screen section headings.
|
||||
final TextStyle sectionTitle;
|
||||
|
||||
/// 19 / w800 / ls -0.4. PluriPushScaffold header title.
|
||||
final TextStyle screenTitle;
|
||||
|
||||
/// 14.5 / w700. Station cards, settings rows, alarm cards.
|
||||
final TextStyle cardTitle;
|
||||
|
||||
/// 13 / w600. Card subtitles, meta lines.
|
||||
final TextStyle bodyStrong;
|
||||
|
||||
/// 11 / w800 / ls 0.8. "EN DIRECTO", "PROGRAMADOS", settings group
|
||||
/// headers.
|
||||
final TextStyle eyebrowLabel;
|
||||
|
||||
/// Builds the scale from [family] — the SAME resolved Google Fonts
|
||||
/// TextTheme instance [PluriWaveTheme.dark] already computes. Building the
|
||||
/// styles anywhere else would re-merge the font family, which is the
|
||||
/// copy-paste this extension exists to eliminate.
|
||||
factory PluriWaveTypography.from(TextTheme family) {
|
||||
final fallback = family.bodyMedium ?? const TextStyle();
|
||||
TextStyle style(
|
||||
double fontSize,
|
||||
FontWeight fontWeight, {
|
||||
double? letterSpacing,
|
||||
double? height,
|
||||
}) {
|
||||
return fallback.copyWith(
|
||||
fontSize: fontSize,
|
||||
fontWeight: fontWeight,
|
||||
letterSpacing: letterSpacing,
|
||||
height: height,
|
||||
);
|
||||
}
|
||||
|
||||
return PluriWaveTypography(
|
||||
heroTime: style(88, FontWeight.w800, letterSpacing: -2.0, height: 1.0),
|
||||
sectionTitle: style(23, FontWeight.w800, letterSpacing: -0.6),
|
||||
screenTitle: style(19, FontWeight.w800, letterSpacing: -0.4),
|
||||
cardTitle: style(14.5, FontWeight.w700),
|
||||
bodyStrong: style(13, FontWeight.w600),
|
||||
eyebrowLabel: style(11, FontWeight.w800, letterSpacing: 0.8),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
PluriWaveTypography copyWith({
|
||||
TextStyle? heroTime,
|
||||
TextStyle? sectionTitle,
|
||||
TextStyle? screenTitle,
|
||||
TextStyle? cardTitle,
|
||||
TextStyle? bodyStrong,
|
||||
TextStyle? eyebrowLabel,
|
||||
}) {
|
||||
return PluriWaveTypography(
|
||||
heroTime: heroTime ?? this.heroTime,
|
||||
sectionTitle: sectionTitle ?? this.sectionTitle,
|
||||
screenTitle: screenTitle ?? this.screenTitle,
|
||||
cardTitle: cardTitle ?? this.cardTitle,
|
||||
bodyStrong: bodyStrong ?? this.bodyStrong,
|
||||
eyebrowLabel: eyebrowLabel ?? this.eyebrowLabel,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
PluriWaveTypography lerp(
|
||||
covariant ThemeExtension<PluriWaveTypography>? other,
|
||||
double t,
|
||||
) {
|
||||
if (other is! PluriWaveTypography) return this;
|
||||
return PluriWaveTypography(
|
||||
heroTime: TextStyle.lerp(heroTime, other.heroTime, t) ?? heroTime,
|
||||
sectionTitle:
|
||||
TextStyle.lerp(sectionTitle, other.sectionTitle, t) ?? sectionTitle,
|
||||
screenTitle:
|
||||
TextStyle.lerp(screenTitle, other.screenTitle, t) ?? screenTitle,
|
||||
cardTitle: TextStyle.lerp(cardTitle, other.cardTitle, t) ?? cardTitle,
|
||||
bodyStrong: TextStyle.lerp(bodyStrong, other.bodyStrong, t) ?? bodyStrong,
|
||||
eyebrowLabel:
|
||||
TextStyle.lerp(eyebrowLabel, other.eyebrowLabel, t) ?? eyebrowLabel,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import '../tema/pluriwave_theme.dart';
|
||||
import 'pluri_wave_scaffold.dart';
|
||||
|
||||
/// Design ADR-2: chrome is decided by route topology, not a parameter.
|
||||
/// [PluriPushScaffold] is the ONE shape a second-level (pushed) screen may
|
||||
/// take — root screens are plain body widgets living in `_PaginaPrincipal`
|
||||
/// and never construct a Scaffold themselves.
|
||||
///
|
||||
/// The load-bearing part of this API is what it does NOT have: there is no
|
||||
/// `bottomNavigationBar` parameter. That absence is what makes "second-level
|
||||
/// screens have no tab bar" unrepresentable rather than a convention someone
|
||||
/// can forget.
|
||||
class PluriPushScaffold extends StatelessWidget {
|
||||
const PluriPushScaffold({
|
||||
super.key,
|
||||
required this.title,
|
||||
required this.body,
|
||||
this.titleOverride,
|
||||
this.leadingIcon = Icons.arrow_back_rounded,
|
||||
this.onBack,
|
||||
this.actions = const <Widget>[],
|
||||
this.bottom,
|
||||
this.floatingActionButton,
|
||||
});
|
||||
|
||||
/// Styled once, here, with [PluriWaveTypography.screenTitle] — the reason
|
||||
/// this is a String and not a Widget, so screens stop copy-pasting the
|
||||
/// style. See [titleOverride] for the one documented exception.
|
||||
final String title;
|
||||
|
||||
final Widget body;
|
||||
|
||||
/// Single documented exception to [title]: the full player's centered
|
||||
/// "EN DIRECTO" pill (WU14). A second consumer means ADR-2 gets revisited,
|
||||
/// not that this parameter quietly becomes the general case.
|
||||
final Widget? titleOverride;
|
||||
|
||||
/// Defaults to a back arrow; `pantalla_reproductor.dart` dismisses with
|
||||
/// `keyboard_arrow_down` instead — still one pushed route, one back
|
||||
/// affordance, only the glyph differs.
|
||||
final IconData leadingIcon;
|
||||
|
||||
/// Defaults to [Navigator.maybePop].
|
||||
final VoidCallback? onBack;
|
||||
|
||||
final List<Widget> actions;
|
||||
|
||||
/// Optional persistent footer (e.g. a CTA).
|
||||
final PreferredSizeWidget? bottom;
|
||||
|
||||
final Widget? floatingActionButton;
|
||||
|
||||
static const double headerHeight = 56;
|
||||
|
||||
/// Centralises route construction so the transition is uniform and a test
|
||||
/// can assert "pushed, not index-switched". Precedent:
|
||||
/// `PantallaReproductor.abrir`.
|
||||
static Future<T?> push<T>(BuildContext context, WidgetBuilder builder) {
|
||||
return Navigator.of(
|
||||
context,
|
||||
).push<T>(MaterialPageRoute<T>(builder: builder));
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final type = context.pluriType;
|
||||
return PluriWaveScaffold(
|
||||
appBar: AppBar(
|
||||
toolbarHeight: headerHeight,
|
||||
automaticallyImplyLeading: false,
|
||||
leading: IconButton(
|
||||
icon: Icon(leadingIcon),
|
||||
onPressed: onBack ?? () => Navigator.maybePop(context),
|
||||
),
|
||||
title: titleOverride ?? Text(title, style: type.screenTitle),
|
||||
actions: actions,
|
||||
bottom: bottom,
|
||||
),
|
||||
body: body,
|
||||
floatingActionButton: floatingActionButton,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -101,31 +101,31 @@ Switching Without Push (provider only; consumption is WU5), Escuchar Tab Rename
|
||||
**New tests**: `test/tema/pluriwave_typography_test.dart`, `test/widgets/pluri_push_scaffold_test.dart`, `test/estado/estado_navegacion_test.dart`
|
||||
**Modified tests**: `test/tema/pluriwave_tokens_test.dart` (or equivalent), `test/widget_test.dart`
|
||||
|
||||
- [ ] 1.1 RED — write `pluriwave_typography_test.dart` asserting the 6 named styles (`heroTime`, `sectionTitle`,
|
||||
- [x] 1.1 RED — write `pluriwave_typography_test.dart` asserting the 6 named styles (`heroTime`, `sectionTitle`,
|
||||
`screenTitle`, `cardTitle`, `bodyStrong`, `eyebrowLabel`) exist with their documented size/weight/letter-spacing
|
||||
and that `PluriWaveTheme.dark()` registers the extension.
|
||||
- [ ] 1.2 RED — extend the tokens test asserting `PluriWaveTokens.listSurface`, `.liveGreen`, `.offlineAccent` exist
|
||||
- [x] 1.2 RED — extend the tokens test asserting `PluriWaveTokens.listSurface`, `.liveGreen`, `.offlineAccent` exist
|
||||
and participate in `lerp`.
|
||||
- [ ] 1.3 GREEN — add the 3 colour fields to `lib/tema/pluriwave_tokens.dart` (constructor, `copyWith`, `lerp`);
|
||||
- [x] 1.3 GREEN — add the 3 colour fields to `lib/tema/pluriwave_tokens.dart` (constructor, `copyWith`, `lerp`);
|
||||
de-literalise `pluriwave_theme.dart:12,15`; create `lib/tema/pluriwave_typography.dart` built inside
|
||||
`PluriWaveTheme.dark()`.
|
||||
- [ ] 1.4 RED — write `pluri_push_scaffold_test.dart`: each of the 5 root screens mounted bare builds zero
|
||||
- [x] 1.4 RED — write `pluri_push_scaffold_test.dart`: each of the 5 root screens mounted bare builds zero
|
||||
`Scaffold`; a `PluriPushScaffold` renders exactly one 56px `AppBar` + back affordance and exposes no
|
||||
`bottomNavigationBar` parameter on its constructor.
|
||||
- [ ] 1.5 GREEN — implement `lib/widgets/pluri_push_scaffold.dart` per the ADR-2 API (`title`, `body`,
|
||||
- [x] 1.5 GREEN — implement `lib/widgets/pluri_push_scaffold.dart` per the ADR-2 API (`title`, `body`,
|
||||
`titleOverride`, `leadingIcon`, `onBack`, `actions`, `bottom`, `floatingActionButton`, static `push`).
|
||||
- [ ] 1.6 RED — write `estado_navegacion_test.dart`: `irA()` transitions `RaizPluriWave`, no-ops and does not notify
|
||||
- [x] 1.6 RED — write `estado_navegacion_test.dart`: `irA()` transitions `RaizPluriWave`, no-ops and does not notify
|
||||
on same-root, `indice` matches enum declaration order.
|
||||
- [ ] 1.7 GREEN — implement `lib/estado/estado_navegacion.dart` (`RaizPluriWave` enum, `EstadoNavegacionRaiz`);
|
||||
- [x] 1.7 GREEN — implement `lib/estado/estado_navegacion.dart` (`RaizPluriWave` enum, `EstadoNavegacionRaiz`);
|
||||
register it in `app.dart`'s `MultiProvider`; convert the 3 `setState(() => _indice = ...)` alarm-deep-link
|
||||
sites (`app.dart:310,333,350`) to `irA(...)`.
|
||||
- [ ] 1.8 RED — update `test/widget_test.dart` to expect "Escuchar" (not "Inicio") as the first tab's label, with
|
||||
- [x] 1.8 RED — update `test/widget_test.dart` to expect "Escuchar" (not "Inicio") as the first tab's label, with
|
||||
`PluriIconGlyph.home` unchanged as its icon.
|
||||
- [ ] 1.9 GREEN — change `app_en.arb` line 4 `navHome` value to `"Listen"` and `app_es.arb` line 4 to `"Escuchar"`;
|
||||
- [x] 1.9 GREEN — change `app_en.arb` line 4 `navHome` value to `"Listen"` and `app_es.arb` line 4 to `"Escuchar"`;
|
||||
retitle the first tab in `lib/app.dart`. Leave the other 11 ARB files untouched (WU18's job).
|
||||
- [ ] 1.10 REFACTOR — sweep `lib/tema/`, `app.dart` for now-dead literals; confirm zero consumer screens were
|
||||
- [x] 1.10 REFACTOR — sweep `lib/tema/`, `app.dart` for now-dead literals; confirm zero consumer screens were
|
||||
touched (WU1 converts zero screens to `PluriPushScaffold` — that is what keeps this commit at its line budget).
|
||||
- [ ] 1.11 Verify — run the command above; confirm `git diff --stat` touches only tokens/theme/typography files, the
|
||||
- [x] 1.11 Verify — run the command above; confirm `git diff --stat` touches only tokens/theme/typography files, the
|
||||
new push-scaffold and nav-state files, `app.dart`, `app_en.arb`, `app_es.arb`, and their tests.
|
||||
|
||||
## WU2 — Android Auto verification (zero code)
|
||||
|
||||
@@ -0,0 +1,75 @@
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:pluriwave/estado/estado_navegacion.dart';
|
||||
|
||||
/// Design ADR-8: EstadoNavegacionRaiz is the single source of truth for
|
||||
/// which of the 5 root tabs is active. Switching roots never pushes a
|
||||
/// route — this is a plain ChangeNotifier update.
|
||||
void main() {
|
||||
group('RaizPluriWave', () {
|
||||
test('declaration order is the tab order (0-indexed)', () {
|
||||
expect(RaizPluriWave.escuchar.index, 0);
|
||||
expect(RaizPluriWave.buscar.index, 1);
|
||||
expect(RaizPluriWave.favoritos.index, 2);
|
||||
expect(RaizPluriWave.alarmas.index, 3);
|
||||
expect(RaizPluriWave.ajustes.index, 4);
|
||||
expect(RaizPluriWave.values.length, 5);
|
||||
});
|
||||
});
|
||||
|
||||
group('EstadoNavegacionRaiz', () {
|
||||
test('starts on escuchar', () {
|
||||
final estado = EstadoNavegacionRaiz();
|
||||
expect(estado.actual, RaizPluriWave.escuchar);
|
||||
expect(estado.indice, 0);
|
||||
});
|
||||
|
||||
test('irA transitions to the requested root', () {
|
||||
final estado = EstadoNavegacionRaiz();
|
||||
|
||||
estado.irA(RaizPluriWave.favoritos);
|
||||
|
||||
expect(estado.actual, RaizPluriWave.favoritos);
|
||||
});
|
||||
|
||||
test('indice mirrors the enum declaration order after a transition', () {
|
||||
final estado = EstadoNavegacionRaiz();
|
||||
|
||||
estado.irA(RaizPluriWave.alarmas);
|
||||
|
||||
expect(estado.indice, RaizPluriWave.alarmas.index);
|
||||
expect(estado.indice, 3);
|
||||
});
|
||||
|
||||
test('irA notifies listeners on a real transition', () {
|
||||
final estado = EstadoNavegacionRaiz();
|
||||
var notifyCount = 0;
|
||||
estado.addListener(() => notifyCount++);
|
||||
|
||||
estado.irA(RaizPluriWave.buscar);
|
||||
|
||||
expect(notifyCount, 1);
|
||||
});
|
||||
|
||||
test('irA no-ops and does not notify when already on that root', () {
|
||||
final estado = EstadoNavegacionRaiz();
|
||||
var notifyCount = 0;
|
||||
estado.addListener(() => notifyCount++);
|
||||
|
||||
estado.irA(RaizPluriWave.escuchar); // already the starting root
|
||||
|
||||
expect(notifyCount, 0);
|
||||
expect(estado.actual, RaizPluriWave.escuchar);
|
||||
});
|
||||
|
||||
test('a repeated irA to the same non-initial root only notifies once', () {
|
||||
final estado = EstadoNavegacionRaiz();
|
||||
estado.irA(RaizPluriWave.ajustes);
|
||||
var notifyCount = 0;
|
||||
estado.addListener(() => notifyCount++);
|
||||
|
||||
estado.irA(RaizPluriWave.ajustes);
|
||||
|
||||
expect(notifyCount, 0);
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:pluriwave/tema/pluriwave_tokens.dart';
|
||||
|
||||
/// Design ADR-1: 3 new colour fields join the existing PluriWaveTokens
|
||||
/// extension (colours belong here because they must participate in lerp).
|
||||
/// Dedicated file under test/tema/ so the WU1 verify command
|
||||
/// (`flutter test test/tema/ ...`) discovers it; the single pre-existing
|
||||
/// PluriWaveTokens assertion inside pluriwave_foundations_test.dart is left
|
||||
/// untouched.
|
||||
void main() {
|
||||
group('PluriWaveTokens — redesign colours', () {
|
||||
test('dark keeps the pre-existing base palette', () {
|
||||
expect(PluriWaveTokens.dark.deepViolet, const Color(0xFF07121A));
|
||||
expect(PluriWaveTokens.dark.radiusMd, 22);
|
||||
expect(PluriWaveTokens.dark.spacingMd, 16);
|
||||
});
|
||||
|
||||
test(
|
||||
'dark exposes listSurface and liveGreen de-literalised from theme.dart',
|
||||
() {
|
||||
// Design ADR-1: these match the theme's previous raw literals
|
||||
// (surfaceContainerLow / secondary) exactly, so promoting them to
|
||||
// named tokens keeps rendered output byte-identical.
|
||||
expect(PluriWaveTokens.dark.listSurface, const Color(0xFF102532));
|
||||
expect(PluriWaveTokens.dark.liveGreen, const Color(0xFF7EE4C2));
|
||||
},
|
||||
);
|
||||
|
||||
test('dark exposes offlineAccent as a genuinely new colour', () {
|
||||
expect(PluriWaveTokens.dark.offlineAccent, const Color(0xFF94A3B8));
|
||||
});
|
||||
|
||||
test('copyWith overrides only the new colour fields', () {
|
||||
final overridden = PluriWaveTokens.dark.copyWith(
|
||||
listSurface: Colors.black,
|
||||
liveGreen: Colors.black,
|
||||
offlineAccent: Colors.black,
|
||||
);
|
||||
expect(overridden.listSurface, Colors.black);
|
||||
expect(overridden.liveGreen, Colors.black);
|
||||
expect(overridden.offlineAccent, Colors.black);
|
||||
// Untouched fields keep their original value.
|
||||
expect(overridden.deepViolet, PluriWaveTokens.dark.deepViolet);
|
||||
});
|
||||
|
||||
test('lerp interpolates listSurface, liveGreen and offlineAccent', () {
|
||||
const a = PluriWaveTokens.dark;
|
||||
final b = a.copyWith(
|
||||
listSurface: Colors.white,
|
||||
liveGreen: Colors.white,
|
||||
offlineAccent: Colors.white,
|
||||
);
|
||||
|
||||
final mid = a.lerp(b, 0.5);
|
||||
|
||||
expect(mid.listSurface, Color.lerp(a.listSurface, Colors.white, 0.5));
|
||||
expect(mid.liveGreen, Color.lerp(a.liveGreen, Colors.white, 0.5));
|
||||
expect(mid.offlineAccent, Color.lerp(a.offlineAccent, Colors.white, 0.5));
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,87 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:pluriwave/tema/pluriwave_theme.dart';
|
||||
import 'package:pluriwave/tema/pluriwave_typography.dart';
|
||||
|
||||
/// Design ADR-1: the named type scale is a second ThemeExtension, closed at
|
||||
/// six styles. Pins the documented size/weight/letter-spacing per style and
|
||||
/// confirms PluriWaveTheme.dark() registers it, mirroring how
|
||||
/// PluriWaveTokens/PluriWaveMotion are already tested.
|
||||
///
|
||||
/// PluriWaveTheme.dark() is only ever resolved from inside a testWidgets
|
||||
/// callback (never from setUpAll or a plain test()). GoogleFonts.
|
||||
/// plusJakartaSansTextTheme fires a background network fetch per text role;
|
||||
/// TestWidgetsFlutterBinding only installs its HTTP-blocking override
|
||||
/// inside an active test-widgets zone, so calling PluriWaveTheme.dark()
|
||||
/// outside one lets a real (and here unreachable) network request escape
|
||||
/// and fail asynchronously. The numeric values asserted below (fontSize/
|
||||
/// weight/letterSpacing/height) are set synchronously by
|
||||
/// PluriWaveTypography.from regardless of that background fetch's outcome.
|
||||
void main() {
|
||||
late PluriWaveTypography type;
|
||||
|
||||
group('PluriWaveTypography', () {
|
||||
testWidgets('PluriWaveTheme.dark() registers the extension', (
|
||||
tester,
|
||||
) async {
|
||||
final theme = PluriWaveTheme.dark();
|
||||
final extension = theme.extension<PluriWaveTypography>();
|
||||
expect(extension, isNotNull);
|
||||
type = extension!;
|
||||
});
|
||||
|
||||
test('heroTime is 88 / w800 / height 1.0 / letter-spacing -2.0', () {
|
||||
expect(type.heroTime.fontSize, 88);
|
||||
expect(type.heroTime.fontWeight, FontWeight.w800);
|
||||
expect(type.heroTime.height, 1.0);
|
||||
expect(type.heroTime.letterSpacing, -2.0);
|
||||
});
|
||||
|
||||
test('sectionTitle is 23 / w800 / letter-spacing -0.6', () {
|
||||
expect(type.sectionTitle.fontSize, 23);
|
||||
expect(type.sectionTitle.fontWeight, FontWeight.w800);
|
||||
expect(type.sectionTitle.letterSpacing, -0.6);
|
||||
});
|
||||
|
||||
test('screenTitle is 19 / w800 / letter-spacing -0.4', () {
|
||||
expect(type.screenTitle.fontSize, 19);
|
||||
expect(type.screenTitle.fontWeight, FontWeight.w800);
|
||||
expect(type.screenTitle.letterSpacing, -0.4);
|
||||
});
|
||||
|
||||
test('cardTitle is 14.5 / w700', () {
|
||||
expect(type.cardTitle.fontSize, 14.5);
|
||||
expect(type.cardTitle.fontWeight, FontWeight.w700);
|
||||
});
|
||||
|
||||
test('bodyStrong is 13 / w600', () {
|
||||
expect(type.bodyStrong.fontSize, 13);
|
||||
expect(type.bodyStrong.fontWeight, FontWeight.w600);
|
||||
});
|
||||
|
||||
test('eyebrowLabel is 11 / w800 / letter-spacing 0.8', () {
|
||||
expect(type.eyebrowLabel.fontSize, 11);
|
||||
expect(type.eyebrowLabel.fontWeight, FontWeight.w800);
|
||||
expect(type.eyebrowLabel.letterSpacing, 0.8);
|
||||
});
|
||||
|
||||
testWidgets('context.pluriType exposes the registered extension', (
|
||||
tester,
|
||||
) async {
|
||||
late BuildContext capturedContext;
|
||||
await tester.pumpWidget(
|
||||
MaterialApp(
|
||||
theme: PluriWaveTheme.dark(),
|
||||
home: Builder(
|
||||
builder: (context) {
|
||||
capturedContext = context;
|
||||
return const SizedBox.shrink();
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
expect(capturedContext.pluriType.cardTitle.fontSize, 14.5);
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -3,7 +3,12 @@
|
||||
// existe en este proyecto — corregido para usar PluriWaveApp.
|
||||
// Los tests de integración completos (audio, streaming) requieren un dispositivo.
|
||||
|
||||
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_bottom_navigation.dart';
|
||||
import 'package:pluriwave/widgets/pluri_icon.dart';
|
||||
|
||||
void main() {
|
||||
testWidgets('Placeholder — tests de integración requieren dispositivo', (
|
||||
@@ -14,4 +19,54 @@ void main() {
|
||||
// por el test de smoke incorrecto del boilerplate original.
|
||||
expect(true, isTrue);
|
||||
});
|
||||
|
||||
// Spec app-navigation-shell — "Escuchar Tab Rename Preserves the ARB Key"
|
||||
// and "Escuchar uses the home glyph": the former "Inicio" tab is
|
||||
// relabeled by changing navHome's VALUE only; PluriIconGlyph.home stays
|
||||
// its icon. app.dart's real _navItems()/_paginas wiring is private to
|
||||
// that library, so this pins the same two contracts at the pieces it
|
||||
// composes: the ARB value, the PluriNavItem the tab is built from, and
|
||||
// that PluriIconGlyph.home still resolves to a renderable icon.
|
||||
group('Escuchar tab rename (WU1)', () {
|
||||
test('navHome value renamed in en/es without touching the key', () async {
|
||||
final l10nEs = await AppLocalizations.delegate.load(const Locale('es'));
|
||||
final l10nEn = await AppLocalizations.delegate.load(const Locale('en'));
|
||||
|
||||
expect(l10nEs.navHome, 'Escuchar');
|
||||
expect(l10nEn.navHome, 'Listen');
|
||||
});
|
||||
|
||||
test(
|
||||
'the home tab item still pairs navHome with PluriIconGlyph.home',
|
||||
() async {
|
||||
final l10n = await AppLocalizations.delegate.load(const Locale('es'));
|
||||
|
||||
final item = PluriNavItem(
|
||||
glyph: PluriIconGlyph.home,
|
||||
label: l10n.navHome,
|
||||
);
|
||||
|
||||
expect(item.label, 'Escuchar');
|
||||
expect(item.glyph, PluriIconGlyph.home);
|
||||
},
|
||||
);
|
||||
|
||||
testWidgets('PluriIconGlyph.home still renders as the home icon', (
|
||||
tester,
|
||||
) async {
|
||||
await tester.pumpWidget(
|
||||
MaterialApp(
|
||||
theme: PluriWaveTheme.dark(),
|
||||
home: const Scaffold(
|
||||
body: PluriIcon(
|
||||
glyph: PluriIconGlyph.home,
|
||||
semanticLabel: 'Escuchar',
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
expect(find.bySemanticsLabel('Escuchar'), findsOneWidget);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@@ -0,0 +1,280 @@
|
||||
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/pantallas/pantalla_inicio.dart';
|
||||
import 'package:pluriwave/servicios/servicio_alarmas.dart';
|
||||
import 'package:pluriwave/widgets/pluri_push_scaffold.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
|
||||
import '../helpers/fakes.dart';
|
||||
import '../helpers/fakes_alarmas.dart';
|
||||
|
||||
/// Design ADR-2 enforcement test: chrome is decided by route topology, not a
|
||||
/// parameter. Mounting each of the 5 root screens bare (no ancestor
|
||||
/// PluriWaveScaffold/Scaffold) must render zero Scaffolds — "no tab bar on
|
||||
/// second-level screens" becomes a structural fact, not a convention. A
|
||||
/// PluriPushScaffold, by contrast, is the ONE place a second-level screen
|
||||
/// gets a Scaffold, and it must render exactly one 56px AppBar with a back
|
||||
/// affordance.
|
||||
void main() {
|
||||
setUp(() {
|
||||
SharedPreferences.setMockInitialValues({});
|
||||
});
|
||||
|
||||
Widget testApp(Widget home) {
|
||||
return MaterialApp(
|
||||
locale: const Locale('es'),
|
||||
localizationsDelegates: AppLocalizations.localizationsDelegates,
|
||||
supportedLocales: AppLocalizations.supportedLocales,
|
||||
// No Scaffold ancestor here on purpose — a bare Material is enough
|
||||
// for InkWell/Card without secretly providing the very Scaffold this
|
||||
// test asserts is absent.
|
||||
home: Material(child: home),
|
||||
);
|
||||
}
|
||||
|
||||
Future<File> archivoCustomVacio() async => File(
|
||||
'${Directory.current.path}/test/fixtures/emisoras_custom_vacio.json',
|
||||
);
|
||||
|
||||
// Existing project convention (see pantalla_inicio_test.dart et al.): the
|
||||
// default 800x600 test surface clips these content-heavy root screens.
|
||||
void setLargeSurface(WidgetTester tester) {
|
||||
tester.view.physicalSize = const Size(1440, 3200);
|
||||
tester.view.devicePixelRatio = 1.0;
|
||||
addTearDown(tester.view.resetPhysicalSize);
|
||||
addTearDown(tester.view.resetDevicePixelRatio);
|
||||
}
|
||||
|
||||
// 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);
|
||||
}
|
||||
|
||||
group('The 5 root screens build zero Scaffold when mounted bare', () {
|
||||
testWidgets('PantallaInicio', (tester) async {
|
||||
setLargeSurface(tester);
|
||||
final estado = EstadoRadio(
|
||||
audio: FakeServicioAudio(),
|
||||
favoritos: FakeServicioFavoritos(),
|
||||
radio: FakeServicioRadio(),
|
||||
servicioEcualizador: FakeServicioEcualizador(),
|
||||
servicioGrabacion: FakeServicioGrabacionRadioInactiva(),
|
||||
resolverArchivoCustom: archivoCustomVacio,
|
||||
iniciarAutomaticamente: false,
|
||||
);
|
||||
addTearDown(estado.dispose);
|
||||
|
||||
await tester.pumpWidget(
|
||||
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),
|
||||
],
|
||||
child: testApp(const PantallaInicio()),
|
||||
),
|
||||
);
|
||||
await tester.pump();
|
||||
|
||||
expect(find.byType(Scaffold), findsNothing);
|
||||
});
|
||||
|
||||
testWidgets('PantallaBuscar', (tester) async {
|
||||
setLargeSurface(tester);
|
||||
final busqueda = EstadoBusqueda(radio: FakeServicioRadio());
|
||||
addTearDown(busqueda.dispose);
|
||||
|
||||
await tester.pumpWidget(
|
||||
ListenableProvider<EstadoBusqueda>.value(
|
||||
value: busqueda,
|
||||
child: testApp(const PantallaBuscar()),
|
||||
),
|
||||
);
|
||||
await tester.pump();
|
||||
|
||||
expect(find.byType(Scaffold), findsNothing);
|
||||
});
|
||||
|
||||
testWidgets('PantallaFavoritos', (tester) async {
|
||||
setLargeSurface(tester);
|
||||
final estado = EstadoRadio(
|
||||
audio: FakeServicioAudio(),
|
||||
favoritos: FakeServicioFavoritos(),
|
||||
radio: FakeServicioRadio(),
|
||||
servicioEcualizador: FakeServicioEcualizador(),
|
||||
servicioGrabacion: FakeServicioGrabacionRadioInactiva(),
|
||||
resolverArchivoCustom: archivoCustomVacio,
|
||||
iniciarAutomaticamente: false,
|
||||
);
|
||||
addTearDown(estado.dispose);
|
||||
|
||||
await tester.pumpWidget(
|
||||
ChangeNotifierProvider<EstadoRadio>.value(
|
||||
value: estado,
|
||||
child: testApp(const PantallaFavoritos()),
|
||||
),
|
||||
);
|
||||
await tester.pump();
|
||||
|
||||
expect(find.byType(Scaffold), findsNothing);
|
||||
});
|
||||
|
||||
testWidgets('PantallaAlarmas', (tester) async {
|
||||
setLargeSurface(tester);
|
||||
final android = FakePuertoAlarmasAndroid();
|
||||
final estadoAlarmas = EstadoAlarmas(
|
||||
servicio: ServicioAlarmas(reloj: DateTime.now),
|
||||
android: android,
|
||||
iniciarAutomaticamente: false,
|
||||
);
|
||||
addTearDown(estadoAlarmas.dispose);
|
||||
addTearDown(android.dispose);
|
||||
|
||||
await tester.pumpWidget(
|
||||
ChangeNotifierProvider<EstadoAlarmas>.value(
|
||||
value: estadoAlarmas,
|
||||
child: testApp(const PantallaAlarmas()),
|
||||
),
|
||||
);
|
||||
await tester.pump();
|
||||
|
||||
expect(find.byType(Scaffold), findsNothing);
|
||||
});
|
||||
|
||||
testWidgets('PantallaAjustes', (tester) async {
|
||||
setLargeSurface(tester);
|
||||
suppressListTileInkAssertion();
|
||||
final estado = EstadoRadio(
|
||||
audio: FakeServicioAudio(),
|
||||
favoritos: FakeServicioFavoritos(),
|
||||
radio: FakeServicioRadio(),
|
||||
servicioEcualizador: FakeServicioEcualizador(),
|
||||
servicioGrabacion: FakeServicioGrabacionRadioInactiva(),
|
||||
resolverArchivoCustom: archivoCustomVacio,
|
||||
iniciarAutomaticamente: false,
|
||||
);
|
||||
addTearDown(estado.dispose);
|
||||
final estadoIdioma = EstadoIdioma();
|
||||
addTearDown(estadoIdioma.dispose);
|
||||
|
||||
await tester.pumpWidget(
|
||||
MultiProvider(
|
||||
providers: [
|
||||
ChangeNotifierProvider<EstadoRadio>.value(value: estado),
|
||||
ListenableProvider<EstadoEcualizador>.value(
|
||||
value: estado.ecualizador,
|
||||
),
|
||||
ListenableProvider<EstadoGrabacion>.value(value: estado.grabacion),
|
||||
ChangeNotifierProvider<EstadoIdioma>.value(value: estadoIdioma),
|
||||
],
|
||||
child: testApp(const PantallaAjustes()),
|
||||
),
|
||||
);
|
||||
await tester.pump();
|
||||
|
||||
expect(find.byType(Scaffold), findsNothing);
|
||||
});
|
||||
});
|
||||
|
||||
group('PluriPushScaffold', () {
|
||||
testWidgets('renders exactly one 56px AppBar with a back affordance', (
|
||||
tester,
|
||||
) async {
|
||||
await tester.pumpWidget(
|
||||
MaterialApp(
|
||||
localizationsDelegates: AppLocalizations.localizationsDelegates,
|
||||
supportedLocales: AppLocalizations.supportedLocales,
|
||||
home: Navigator(
|
||||
onGenerateRoute:
|
||||
(settings) => MaterialPageRoute<void>(
|
||||
builder:
|
||||
(_) => const PluriPushScaffold(
|
||||
title: 'Detalle',
|
||||
body: SizedBox.shrink(),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(find.byType(Scaffold), findsOneWidget);
|
||||
expect(find.byType(AppBar), findsOneWidget);
|
||||
final appBar = tester.widget<AppBar>(find.byType(AppBar));
|
||||
expect(appBar.toolbarHeight, PluriPushScaffold.headerHeight);
|
||||
expect(PluriPushScaffold.headerHeight, 56);
|
||||
expect(find.byIcon(Icons.arrow_back_rounded), findsOneWidget);
|
||||
expect(find.text('Detalle'), findsOneWidget);
|
||||
});
|
||||
|
||||
// Design ADR-2's load-bearing contract: PluriPushScaffold has NO
|
||||
// bottomNavigationBar parameter. This is a type-system guarantee, not a
|
||||
// runtime one — passing `bottomNavigationBar: ...` below would simply
|
||||
// fail to compile. There is nothing to assert dynamically; the absence
|
||||
// of that parameter IS the enforcement.
|
||||
|
||||
testWidgets('push navigates via Navigator.push (route depth increases)', (
|
||||
tester,
|
||||
) async {
|
||||
await tester.pumpWidget(
|
||||
MaterialApp(
|
||||
localizationsDelegates: AppLocalizations.localizationsDelegates,
|
||||
supportedLocales: AppLocalizations.supportedLocales,
|
||||
home: Builder(
|
||||
builder:
|
||||
(context) => Material(
|
||||
child: Center(
|
||||
child: ElevatedButton(
|
||||
onPressed:
|
||||
() => PluriPushScaffold.push(
|
||||
context,
|
||||
(_) => const PluriPushScaffold(
|
||||
title: 'Empujada',
|
||||
body: SizedBox.shrink(),
|
||||
),
|
||||
),
|
||||
child: const Text('abrir'),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
expect(find.byType(PluriPushScaffold), findsNothing);
|
||||
await tester.tap(find.text('abrir'));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(find.byType(PluriPushScaffold), findsOneWidget);
|
||||
expect(find.text('Empujada'), findsOneWidget);
|
||||
});
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user