feat(alarma-sonando): restyle ringing screen, drop live countdown label
Replace the glass-card container with a full-bleed blurred-art background, giant heroTime display, 3 fixed snooze tiles (3/5/10 min, one highlighted), and a full-width stop pill. The status label now also renders a static "Subiendo volumen" line - no seconds counter - when the alarm has a configured fade-in, per resolution 4: the native-to-Flutter progress channel a live counter would need is deliberately absent from this architecture. The dismiss guard and force-stop retry banner are untouched: the banner is byte-identical to its pre-restyle form, only repositioned, and the guard test's own diff against main stays empty. size:exception: 530 changed lines (440+/90-) against the 200-300 forecast - lib/ alone is 302 lines, at the edge of the band; the two touched test files account for the rest. Not split further: this is one cohesive restyle to the single screen in this branch where an inconsistent intermediate state is least acceptable.
This commit is contained in:
@@ -1,4 +1,5 @@
|
|||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
|
import 'dart:ui';
|
||||||
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter/services.dart';
|
import 'package:flutter/services.dart';
|
||||||
@@ -11,7 +12,6 @@ import '../modelos/alarma_musical.dart';
|
|||||||
import '../tema/pluri_animate.dart';
|
import '../tema/pluri_animate.dart';
|
||||||
import '../tema/pluriwave_theme.dart';
|
import '../tema/pluriwave_theme.dart';
|
||||||
import '../tema/pluriwave_tokens.dart';
|
import '../tema/pluriwave_tokens.dart';
|
||||||
import '../widgets/pluri_glass_surface.dart';
|
|
||||||
import '../widgets/pluri_wave_scaffold.dart';
|
import '../widgets/pluri_wave_scaffold.dart';
|
||||||
|
|
||||||
class PantallaAlarmaSonando extends StatefulWidget {
|
class PantallaAlarmaSonando extends StatefulWidget {
|
||||||
@@ -153,13 +153,6 @@ class _PantallaAlarmaSonandoState extends State<PantallaAlarmaSonando> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
List<int> _opcionesSnooze() {
|
|
||||||
final opciones = <int>{3, 5, 10};
|
|
||||||
final propio = widget.alarma.snoozeMinutos;
|
|
||||||
if (propio > 0) opciones.add(propio);
|
|
||||||
return opciones.toList()..sort();
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void dispose() {
|
void dispose() {
|
||||||
_alarmas.removeListener(_alReconciliarFinExterno);
|
_alarmas.removeListener(_alReconciliarFinExterno);
|
||||||
@@ -195,39 +188,53 @@ class _PantallaAlarmaSonandoState extends State<PantallaAlarmaSonando> {
|
|||||||
AppLocalizations l10n,
|
AppLocalizations l10n,
|
||||||
PluriWaveTokens tokens,
|
PluriWaveTokens tokens,
|
||||||
) {
|
) {
|
||||||
|
final type = context.pluriType;
|
||||||
|
// WU11 (native-alarms delta — restyle, drop live countdown label):
|
||||||
|
// full-bleed blurred art replaces the PluriGlassSurface card. Cold-GPU
|
||||||
|
// note (Design 2.4) still applies to the entry animation below, which
|
||||||
|
// is why it stays on the foreground content only, not the background.
|
||||||
return PluriWaveScaffold(
|
return PluriWaveScaffold(
|
||||||
body: SafeArea(
|
body: Stack(
|
||||||
child: Padding(
|
fit: StackFit.expand,
|
||||||
padding: const EdgeInsets.all(20),
|
children: [
|
||||||
child: Center(
|
_FondoArteDifuminado(tokens: tokens),
|
||||||
child: PluriGlassSurface(
|
SafeArea(
|
||||||
borderRadius: BorderRadius.circular(32),
|
child: Padding(
|
||||||
padding: const EdgeInsets.all(24),
|
padding: const EdgeInsets.fromLTRB(24, 20, 24, 24),
|
||||||
blurSigma: 10,
|
|
||||||
glowColor: tokens.warmCoral.withValues(alpha: 0.35),
|
|
||||||
child: Column(
|
child: Column(
|
||||||
mainAxisSize: MainAxisSize.min,
|
|
||||||
children: [
|
children: [
|
||||||
Image.asset(
|
const Spacer(flex: 2),
|
||||||
'assets/icons/alarmas/alarm_music.png',
|
FittedBox(
|
||||||
width: 128,
|
fit: BoxFit.scaleDown,
|
||||||
height: 128,
|
child: Text(
|
||||||
),
|
_hora(alarma),
|
||||||
const SizedBox(height: 16),
|
key: const ValueKey('ringing-hero-time'),
|
||||||
Text(
|
style: type.heroTime,
|
||||||
_hora(alarma),
|
|
||||||
style: Theme.of(context).textTheme.displayMedium?.copyWith(
|
|
||||||
fontWeight: FontWeight.w900,
|
|
||||||
letterSpacing: -2,
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 8),
|
const SizedBox(height: 6),
|
||||||
Text(
|
Text(
|
||||||
localizedAlarmName(l10n, alarma.nombre),
|
localizedAlarmName(l10n, alarma.nombre),
|
||||||
textAlign: TextAlign.center,
|
textAlign: TextAlign.center,
|
||||||
style: Theme.of(context).textTheme.titleLarge,
|
style: type.bodyStrong,
|
||||||
),
|
),
|
||||||
const SizedBox(height: 8),
|
const SizedBox(height: 22),
|
||||||
|
ClipRRect(
|
||||||
|
borderRadius: BorderRadius.circular(tokens.radiusLg),
|
||||||
|
child: Image.asset(
|
||||||
|
'assets/icons/alarmas/alarm_music.png',
|
||||||
|
width: 168,
|
||||||
|
height: 168,
|
||||||
|
fit: BoxFit.cover,
|
||||||
|
errorBuilder:
|
||||||
|
(_, __, ___) => Icon(
|
||||||
|
Icons.music_note_rounded,
|
||||||
|
size: 96,
|
||||||
|
color: tokens.warmCoral,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 18),
|
||||||
// Static status line (Design D8): sourced only from
|
// Static status line (Design D8): sourced only from
|
||||||
// widget.alarma, never from a live audio/player state — the
|
// widget.alarma, never from a live audio/player state — the
|
||||||
// ring's own audio state is owned natively and this screen
|
// ring's own audio state is owned natively and this screen
|
||||||
@@ -237,31 +244,54 @@ class _PantallaAlarmaSonandoState extends State<PantallaAlarmaSonando> {
|
|||||||
? localizedStationName(l10n, alarma.emisora!.nombre)
|
? localizedStationName(l10n, alarma.emisora!.nombre)
|
||||||
: l10n.alarmRingingNotificationTitle,
|
: l10n.alarmRingingNotificationTitle,
|
||||||
textAlign: TextAlign.center,
|
textAlign: TextAlign.center,
|
||||||
|
style: type.cardTitle,
|
||||||
),
|
),
|
||||||
const SizedBox(height: 22),
|
// WU11 (native-alarms delta — Ringing Screen Shows a
|
||||||
Text(
|
// Static Status Label): only rendered while this alarm was
|
||||||
l10n.snoozeAction,
|
// actually configured with a fade-in; a STATIC label, no
|
||||||
style: Theme.of(context).textTheme.labelLarge,
|
// seconds suffix, no ticking value — the native→Flutter
|
||||||
|
// progress channel that a live countdown would need is
|
||||||
|
// deliberately absent from this architecture (resolution
|
||||||
|
// 4). Not spec-tested to also disappear once the fade-in
|
||||||
|
// period elapses: this screen has no clock signal to know
|
||||||
|
// when that is, and inventing one would be exactly the
|
||||||
|
// out-of-scope plumbing being avoided.
|
||||||
|
if (alarma.fadeInSegundos > 0) ...[
|
||||||
|
const SizedBox(height: 4),
|
||||||
|
_EstadoSubidaVolumen(l10n: l10n, tokens: tokens),
|
||||||
|
],
|
||||||
|
const Spacer(flex: 3),
|
||||||
|
Align(
|
||||||
|
alignment: Alignment.centerLeft,
|
||||||
|
child: Text(
|
||||||
|
l10n.snoozeAction,
|
||||||
|
style: type.eyebrowLabel.copyWith(
|
||||||
|
color: tokens.warmCoral,
|
||||||
|
),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 8),
|
const SizedBox(height: 10),
|
||||||
Wrap(
|
_FilaSnoozeFija(
|
||||||
spacing: 8,
|
alarma: alarma,
|
||||||
runSpacing: 8,
|
l10n: l10n,
|
||||||
alignment: WrapAlignment.center,
|
tokens: tokens,
|
||||||
children: [
|
onPosponer: _posponer,
|
||||||
for (final minutos in _opcionesSnooze())
|
|
||||||
OutlinedButton.icon(
|
|
||||||
onPressed: () => _posponer(minutos),
|
|
||||||
icon: const Icon(Icons.snooze_rounded),
|
|
||||||
label: Text(l10n.alarmSnoozeOptionLabel(minutos)),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
),
|
||||||
const SizedBox(height: 14),
|
const SizedBox(height: 14),
|
||||||
FilledButton.icon(
|
SizedBox(
|
||||||
onPressed: _detener,
|
width: double.infinity,
|
||||||
icon: const Icon(Icons.stop_rounded),
|
child: FilledButton.icon(
|
||||||
label: Text(l10n.stopAlarmAction),
|
key: const ValueKey('ringing-stop-button'),
|
||||||
|
style: FilledButton.styleFrom(
|
||||||
|
minimumSize: const Size.fromHeight(76),
|
||||||
|
shape: RoundedRectangleBorder(
|
||||||
|
borderRadius: BorderRadius.circular(tokens.radiusLg),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
onPressed: _detener,
|
||||||
|
icon: const Icon(Icons.stop_circle_rounded),
|
||||||
|
label: Text(l10n.stopAlarmAction),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
if (_falloDetencionVisible) ...[
|
if (_falloDetencionVisible) ...[
|
||||||
const SizedBox(height: 14),
|
const SizedBox(height: 14),
|
||||||
@@ -271,7 +301,7 @@ class _PantallaAlarmaSonandoState extends State<PantallaAlarmaSonando> {
|
|||||||
),
|
),
|
||||||
).pluriFadeIn(context),
|
).pluriFadeIn(context),
|
||||||
),
|
),
|
||||||
),
|
],
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -310,3 +340,167 @@ class _PantallaAlarmaSonandoState extends State<PantallaAlarmaSonando> {
|
|||||||
|
|
||||||
String _hora(AlarmaMusical alarma) =>
|
String _hora(AlarmaMusical alarma) =>
|
||||||
'${alarma.hora.toString().padLeft(2, '0')}:${alarma.minuto.toString().padLeft(2, '0')}';
|
'${alarma.hora.toString().padLeft(2, '0')}:${alarma.minuto.toString().padLeft(2, '0')}';
|
||||||
|
|
||||||
|
/// Full-bleed blurred backdrop (WU11, replaces the `PluriGlassSurface` card
|
||||||
|
/// container per task 11.3). This app has no per-station artwork/favicon
|
||||||
|
/// safe to render here: `Emisora.favicon` is a network URL, and rendering
|
||||||
|
/// one via `Image.network` inside a widget test hangs/throws without a
|
||||||
|
/// mocked `HttpClient` — a hazard no other screen in this codebase accepts
|
||||||
|
/// either. The existing bundled alarm asset is reused instead, heavily
|
||||||
|
/// blurred and stretched; purely decorative, not spec-tested.
|
||||||
|
class _FondoArteDifuminado extends StatelessWidget {
|
||||||
|
const _FondoArteDifuminado({required this.tokens});
|
||||||
|
|
||||||
|
final PluriWaveTokens tokens;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Positioned.fill(
|
||||||
|
key: const ValueKey('ringing-background-art'),
|
||||||
|
child: Stack(
|
||||||
|
fit: StackFit.expand,
|
||||||
|
children: [
|
||||||
|
ImageFiltered(
|
||||||
|
imageFilter: ImageFilter.blur(sigmaX: 44, sigmaY: 44),
|
||||||
|
child: Opacity(
|
||||||
|
opacity: 0.5,
|
||||||
|
child: Image.asset(
|
||||||
|
'assets/icons/alarmas/alarm_music.png',
|
||||||
|
fit: BoxFit.cover,
|
||||||
|
errorBuilder: (_, __, ___) => const SizedBox.shrink(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
DecoratedBox(
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
gradient: LinearGradient(
|
||||||
|
begin: Alignment.topCenter,
|
||||||
|
end: Alignment.bottomCenter,
|
||||||
|
colors: [
|
||||||
|
tokens.deepViolet.withValues(alpha: 0.55),
|
||||||
|
tokens.deepViolet.withValues(alpha: 0.9),
|
||||||
|
tokens.deepViolet,
|
||||||
|
],
|
||||||
|
stops: const [0, 0.55, 1],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Static "turning up the volume" status (native-alarms delta, WU11 —
|
||||||
|
/// Ringing Screen Shows a Static Status Label): a dot + label, no
|
||||||
|
/// `AnimationController`/`Animate` anywhere in this widget. A pulsing dot
|
||||||
|
/// would reintroduce the exact "`pumpAndSettle()` never terminates" hazard
|
||||||
|
/// WU5 documented for `VisualizadorAudio`'s own repeating controller — this
|
||||||
|
/// screen must stay safe for `pumpAndSettle()` in every other existing test.
|
||||||
|
class _EstadoSubidaVolumen extends StatelessWidget {
|
||||||
|
const _EstadoSubidaVolumen({required this.l10n, required this.tokens});
|
||||||
|
|
||||||
|
final AppLocalizations l10n;
|
||||||
|
final PluriWaveTokens tokens;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Row(
|
||||||
|
key: const ValueKey('estado-subida-volumen'),
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
Container(
|
||||||
|
width: 6,
|
||||||
|
height: 6,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: tokens.liveGreen,
|
||||||
|
shape: BoxShape.circle,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 6),
|
||||||
|
Text(
|
||||||
|
l10n.alarmVolumeRisingStatus,
|
||||||
|
style: context.pluriType.bodyStrong.copyWith(color: tokens.liveGreen),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The ringing screen's snooze row (native-alarms delta, WU11): exactly 3
|
||||||
|
/// FIXED tiles (3/5/10 min), replacing the previous variable-length `Wrap`
|
||||||
|
/// that grew a 4th tile for a custom `snoozeMinutos`. **Design decision, not
|
||||||
|
/// spec-tested** (WU11 has no ADR): the highlighted (filled) tile is
|
||||||
|
/// whichever of the 3 matches `alarma.snoozeMinutos`; the alarm's own
|
||||||
|
/// editor-configured value still decides WHICH tile is filled, tapping any
|
||||||
|
/// tile still snoozes for exactly that tile's duration (`_posponer` is
|
||||||
|
/// called with the tapped value, never the alarm's stored default). If the
|
||||||
|
/// alarm's own value isn't one of the three — only reachable via a fixture
|
||||||
|
/// or a pre-redesign save, since the editor's own snooze picker only ever
|
||||||
|
/// offers `{3, 5, 10, current}` — 10 is the default highlight, matching the
|
||||||
|
/// mockup's own "10 min · habitual" example.
|
||||||
|
class _FilaSnoozeFija extends StatelessWidget {
|
||||||
|
const _FilaSnoozeFija({
|
||||||
|
required this.alarma,
|
||||||
|
required this.l10n,
|
||||||
|
required this.tokens,
|
||||||
|
required this.onPosponer,
|
||||||
|
});
|
||||||
|
|
||||||
|
final AlarmaMusical alarma;
|
||||||
|
final AppLocalizations l10n;
|
||||||
|
final PluriWaveTokens tokens;
|
||||||
|
final ValueChanged<int> onPosponer;
|
||||||
|
|
||||||
|
static const _opciones = [3, 5, 10];
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final destacado =
|
||||||
|
_opciones.contains(alarma.snoozeMinutos) ? alarma.snoozeMinutos : 10;
|
||||||
|
return Row(
|
||||||
|
children: [
|
||||||
|
for (final minutos in _opciones) ...[
|
||||||
|
if (minutos != _opciones.first) const SizedBox(width: 10),
|
||||||
|
_tile(minutos, minutos == destacado),
|
||||||
|
],
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _tile(int minutos, bool esDestacado) {
|
||||||
|
final forma = RoundedRectangleBorder(
|
||||||
|
borderRadius: BorderRadius.circular(tokens.radiusMd),
|
||||||
|
);
|
||||||
|
final etiqueta = Text(l10n.alarmSnoozeOptionLabel(minutos));
|
||||||
|
return Expanded(
|
||||||
|
flex: esDestacado ? 3 : 2,
|
||||||
|
child: SizedBox(
|
||||||
|
height: 76,
|
||||||
|
child:
|
||||||
|
esDestacado
|
||||||
|
? FilledButton(
|
||||||
|
onPressed: () => onPosponer(minutos),
|
||||||
|
style: FilledButton.styleFrom(
|
||||||
|
backgroundColor: tokens.warmCoral,
|
||||||
|
foregroundColor: tokens.deepViolet,
|
||||||
|
shape: forma,
|
||||||
|
),
|
||||||
|
child: etiqueta,
|
||||||
|
)
|
||||||
|
: OutlinedButton(
|
||||||
|
onPressed: () => onPosponer(minutos),
|
||||||
|
style: OutlinedButton.styleFrom(
|
||||||
|
foregroundColor: tokens.warmCoral,
|
||||||
|
backgroundColor: tokens.warmCoral.withValues(alpha: 0.16),
|
||||||
|
side: BorderSide(
|
||||||
|
color: tokens.warmCoral.withValues(alpha: 0.4),
|
||||||
|
),
|
||||||
|
shape: forma,
|
||||||
|
),
|
||||||
|
child: etiqueta,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -680,19 +680,45 @@ Banner Survive the Restyle
|
|||||||
> **Hard rule, restated:** if the restyle appears to require changing the dismiss-guard test, the task is to STOP
|
> **Hard rule, restated:** if the restyle appears to require changing the dismiss-guard test, the task is to STOP
|
||||||
> and escalate — not to edit the test.
|
> and escalate — not to edit the test.
|
||||||
|
|
||||||
- [ ] 11.1 RED — update the two non-guard test files for the full-bleed blurred-art layout, giant 88px time
|
- [x] 11.1 RED — update the two non-guard test files for the full-bleed blurred-art layout, giant 88px time
|
||||||
(`heroTime`, wrapped in `FittedBox(fit: BoxFit.scaleDown)` per the text-scaling rule), 3 fixed snooze tiles
|
(`heroTime`, wrapped in `FittedBox(fit: BoxFit.scaleDown)` per the text-scaling rule), 3 fixed snooze tiles
|
||||||
(3/5/10 min, 10 highlighted), full-width stop pill.
|
(3/5/10 min, 10 highlighted), full-width stop pill. **Correction found at apply time**: the pre-existing
|
||||||
- [ ] 11.2 RED — the status label renders "Subiendo volumen" (or localized equivalent) with no accompanying
|
"3/5/10 mas el personalizado" test (snoozeMinutos=7 growing a 4th button) is exactly the behavior this WU
|
||||||
numeric value that changes over time.
|
replaces — rewrote it in place (not deleted) to assert the new "always exactly 3, no 4th tile" contract,
|
||||||
- [ ] 11.3 GREEN — restyle to a full-bleed art background; replace the `Wrap` of snooze chips with 3 fixed tiles;
|
plus 2 new tests pinning which tile is `FilledButton` (highlighted) vs `OutlinedButton`.
|
||||||
replace the glass-card container.
|
- [x] 11.2 RED — the status label renders "Subiendo volumen" (or localized equivalent) with no accompanying
|
||||||
- [ ] 11.4 GREEN — keep the status label static, sourced only from `widget.alarma` (unchanged from the existing
|
numeric value that changes over time. New ARB key `alarmVolumeRisingStatus` carries NO placeholder at all
|
||||||
documented constraint); preserve the force-stop retry banner in the new layout.
|
(unlike `alarmFadeInSummary`, which has `{seconds}`) — by construction this label can never grow a live
|
||||||
- [ ] 11.5 REFACTOR — run `pantalla_alarma_sonando_dismiss_guard_test.dart` **unchanged**; if it fails, STOP — do
|
counter without a deliberate key change. Gated on `alarma.fadeInSegundos > 0`: the existing `_montarPantalla`
|
||||||
not edit it, escalate instead.
|
test helper in `pantalla_alarma_sonando_test.dart` already carried an unused `fadeInSegundos` parameter
|
||||||
- [ ] 11.6 Verify — force-stop banner still appears on a simulated stop failure; dismiss-guard test file diff is
|
defaulting to 0, confirming this gate was anticipated ahead of this WU.
|
||||||
empty.
|
- [x] 11.3 GREEN — restyle to a full-bleed art background; replace the `Wrap` of snooze chips with 3 fixed tiles;
|
||||||
|
replace the glass-card container. No per-station artwork exists in this codebase (`Emisora.favicon` is a
|
||||||
|
network URL — rendering it via `Image.network` in a widget test hangs/throws without a mocked
|
||||||
|
`HttpClient`), so the existing bundled `alarm_music.png` asset is reused, blurred (`ImageFiltered`,
|
||||||
|
sigma 44) and stretched — decorative, not spec-tested.
|
||||||
|
- [x] 11.4 GREEN — keep the status label static, sourced only from `widget.alarma` (unchanged from the existing
|
||||||
|
documented constraint); preserve the force-stop retry banner in the new layout. `_bannerFalloDetencion` is
|
||||||
|
BYTE-IDENTICAL to its pre-WU11 form (same l10n keys, same widget structure) — only repositioned, never
|
||||||
|
rewritten, so every existing force-stop test (SS-3a/SS-3b/SS-3c/RES-2) keeps passing unmodified. **Design
|
||||||
|
decision, not specified by any ADR (WU11 has none)**: the "Subiendo volumen" status dot is intentionally
|
||||||
|
NOT animated/pulsing — an `AnimationController.repeat()` here would reintroduce the exact
|
||||||
|
"`pumpAndSettle()` never terminates" hazard WU5 documented for `VisualizadorAudio`.
|
||||||
|
- [x] 11.5 REFACTOR — ran `pantalla_alarma_sonando_dismiss_guard_test.dart` **unchanged**: all 8 cases green,
|
||||||
|
byte-identical, no edit needed or made.
|
||||||
|
- [x] 11.6 Verify — force-stop banner still appears on a simulated stop failure (all 3 force-stop scenarios green,
|
||||||
|
unmodified); dismiss-guard test file diff is empty against BOTH `HEAD` and `main`.
|
||||||
|
|
||||||
|
**`size:exception` recorded.** Realized: **530 changed lines** (440+/90-) across 3 files against the 200-300
|
||||||
|
forecast — `lib/` production code alone is 302 lines (248+/54-), essentially at the top edge of the forecast band
|
||||||
|
by itself; the 2 modified test files add 228 lines, the same "a strict-TDD commit carries its test files" pattern
|
||||||
|
as every prior WU (Engram `reference/estimating-strict-tdd-diffs`, id 2514) — this WU touches no ARB keys beyond
|
||||||
|
the 1 new `alarmVolumeRisingStatus` string, so no 13-file l10n/gen inflation this time; test growth alone accounts
|
||||||
|
for the overage. Not pre-flagged for exception in the forecast table (unlike WU10), but the same "one cohesive
|
||||||
|
visual+behavioral restyle to one safety-critical screen" reasoning applies: splitting the background/snooze-tiles/
|
||||||
|
stop-pill/status-label changes into separate commits would leave an inconsistent intermediate UI on the ringing
|
||||||
|
screen specifically — the one screen in this whole branch where an inconsistent intermediate state is least
|
||||||
|
acceptable.
|
||||||
|
|
||||||
## WU13 — Ecualizador settings screen (5-band restyle)
|
## WU13 — Ecualizador settings screen (5-band restyle)
|
||||||
|
|
||||||
|
|||||||
@@ -11,6 +11,8 @@ import 'package:pluriwave/modelos/emisora.dart';
|
|||||||
import 'package:pluriwave/pantallas/pantalla_alarma_sonando.dart';
|
import 'package:pluriwave/pantallas/pantalla_alarma_sonando.dart';
|
||||||
import 'package:pluriwave/servicios/servicio_alarmas.dart';
|
import 'package:pluriwave/servicios/servicio_alarmas.dart';
|
||||||
import 'package:pluriwave/servicios/servicio_audio.dart';
|
import 'package:pluriwave/servicios/servicio_audio.dart';
|
||||||
|
import 'package:pluriwave/tema/pluriwave_theme.dart';
|
||||||
|
import 'package:pluriwave/widgets/pluri_glass_surface.dart';
|
||||||
import 'package:pluriwave/widgets/pluri_wave_scaffold.dart';
|
import 'package:pluriwave/widgets/pluri_wave_scaffold.dart';
|
||||||
import 'package:provider/provider.dart';
|
import 'package:provider/provider.dart';
|
||||||
import 'package:shared_preferences/shared_preferences.dart';
|
import 'package:shared_preferences/shared_preferences.dart';
|
||||||
@@ -89,9 +91,7 @@ Future<void> _montarPantalla(
|
|||||||
navigator.push(
|
navigator.push(
|
||||||
MaterialPageRoute<void>(
|
MaterialPageRoute<void>(
|
||||||
builder:
|
builder:
|
||||||
(_) => PantallaAlarmaSonando(
|
(_) => PantallaAlarmaSonando(alarma: estadoAlarmas.alarmas.single),
|
||||||
alarma: estadoAlarmas.alarmas.single,
|
|
||||||
),
|
|
||||||
fullscreenDialog: true,
|
fullscreenDialog: true,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -143,4 +143,38 @@ void main() {
|
|||||||
expect(find.text(l10n.stopAlarmAction), findsOneWidget);
|
expect(find.text(l10n.stopAlarmAction), findsOneWidget);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
|
group('WU11 — restyle a pantalla completa', () {
|
||||||
|
testWidgets(
|
||||||
|
'el tiempo gigante usa PluriWaveTypography.heroTime envuelto en '
|
||||||
|
'FittedBox(scaleDown)',
|
||||||
|
(tester) async {
|
||||||
|
await _montarPantalla(tester);
|
||||||
|
|
||||||
|
final heroFinder = find.byKey(const ValueKey('ringing-hero-time'));
|
||||||
|
expect(heroFinder, findsOneWidget);
|
||||||
|
final texto = tester.widget<Text>(heroFinder);
|
||||||
|
final contexto = tester.element(heroFinder);
|
||||||
|
expect(texto.style, contexto.pluriType.heroTime);
|
||||||
|
expect(
|
||||||
|
find.ancestor(of: heroFinder, matching: find.byType(FittedBox)),
|
||||||
|
findsOneWidget,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
testWidgets(
|
||||||
|
'el fondo es arte difuminado a pantalla completa; el contenedor '
|
||||||
|
'glass-card anterior desaparece',
|
||||||
|
(tester) async {
|
||||||
|
await _montarPantalla(tester);
|
||||||
|
|
||||||
|
expect(
|
||||||
|
find.byKey(const ValueKey('ringing-background-art')),
|
||||||
|
findsOneWidget,
|
||||||
|
);
|
||||||
|
expect(find.byType(PluriGlassSurface), findsNothing);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -107,8 +107,7 @@ Future<_Entorno> _montarPantalla(
|
|||||||
navigator.push(
|
navigator.push(
|
||||||
MaterialPageRoute<void>(
|
MaterialPageRoute<void>(
|
||||||
builder:
|
builder:
|
||||||
(_) =>
|
(_) => PantallaAlarmaSonando(alarma: estadoAlarmas.alarmas.single),
|
||||||
PantallaAlarmaSonando(alarma: estadoAlarmas.alarmas.single),
|
|
||||||
fullscreenDialog: true,
|
fullscreenDialog: true,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -124,29 +123,153 @@ void main() {
|
|||||||
SharedPreferences.setMockInitialValues({});
|
SharedPreferences.setMockInitialValues({});
|
||||||
});
|
});
|
||||||
|
|
||||||
testWidgets(
|
group('WU11 — 3 tiles de posponer fijos (3/5/10 min)', () {
|
||||||
'muestra botones de posponer 3/5/10 mas el personalizado (S2-R1-A/C)',
|
testWidgets(
|
||||||
(tester) async {
|
'siempre son exactamente 3 tiles fijos, incluso con un snoozeMinutos '
|
||||||
await _montarPantalla(tester, snoozeMinutos: 7);
|
'personalizado que no es 3/5/10 (S2-R1-A/C, restilizado)',
|
||||||
|
(tester) async {
|
||||||
|
// WU11 correction: the ringing screen's snooze row is no longer a
|
||||||
|
// variable-length Wrap that grows for a custom value — the mockup's
|
||||||
|
// "3 fixed tiles" replaces it. A custom snoozeMinutos (7 here, same
|
||||||
|
// fixture as before WU11) still configures the ALARM's own default
|
||||||
|
// elsewhere (the editor), but no longer grows a 4th tile on this
|
||||||
|
// screen specifically.
|
||||||
|
await _montarPantalla(tester, snoozeMinutos: 7);
|
||||||
|
|
||||||
expect(find.text(l10n.alarmSnoozeOptionLabel(3)), findsOneWidget);
|
expect(find.text(l10n.alarmSnoozeOptionLabel(3)), findsOneWidget);
|
||||||
expect(find.text(l10n.alarmSnoozeOptionLabel(5)), findsOneWidget);
|
expect(find.text(l10n.alarmSnoozeOptionLabel(5)), findsOneWidget);
|
||||||
expect(find.text(l10n.alarmSnoozeOptionLabel(7)), findsOneWidget);
|
expect(find.text(l10n.alarmSnoozeOptionLabel(10)), findsOneWidget);
|
||||||
expect(find.text(l10n.alarmSnoozeOptionLabel(10)), findsOneWidget);
|
expect(find.text(l10n.alarmSnoozeOptionLabel(7)), findsNothing);
|
||||||
expect(find.text(l10n.stopAlarmAction), findsOneWidget);
|
expect(find.text(l10n.stopAlarmAction), findsOneWidget);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
testWidgets(
|
testWidgets(
|
||||||
'no duplica el boton cuando snoozeMinutos coincide con una opcion fija',
|
'sigue habiendo exactamente 3 tiles cuando snoozeMinutos coincide con '
|
||||||
(tester) async {
|
'una opcion fija',
|
||||||
await _montarPantalla(tester, snoozeMinutos: 5);
|
(tester) async {
|
||||||
|
await _montarPantalla(tester, snoozeMinutos: 5);
|
||||||
|
|
||||||
expect(find.text(l10n.alarmSnoozeOptionLabel(3)), findsOneWidget);
|
expect(find.text(l10n.alarmSnoozeOptionLabel(3)), findsOneWidget);
|
||||||
expect(find.text(l10n.alarmSnoozeOptionLabel(5)), findsOneWidget);
|
expect(find.text(l10n.alarmSnoozeOptionLabel(5)), findsOneWidget);
|
||||||
expect(find.text(l10n.alarmSnoozeOptionLabel(10)), findsOneWidget);
|
expect(find.text(l10n.alarmSnoozeOptionLabel(10)), findsOneWidget);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
|
testWidgets(
|
||||||
|
'el tile que coincide con snoozeMinutos es el destacado (FilledButton); '
|
||||||
|
'los otros dos son OutlinedButton',
|
||||||
|
(tester) async {
|
||||||
|
await _montarPantalla(tester, snoozeMinutos: 5);
|
||||||
|
|
||||||
|
expect(
|
||||||
|
find.ancestor(
|
||||||
|
of: find.text(l10n.alarmSnoozeOptionLabel(5)),
|
||||||
|
matching: find.byType(FilledButton),
|
||||||
|
),
|
||||||
|
findsOneWidget,
|
||||||
|
);
|
||||||
|
expect(
|
||||||
|
find.ancestor(
|
||||||
|
of: find.text(l10n.alarmSnoozeOptionLabel(3)),
|
||||||
|
matching: find.byType(OutlinedButton),
|
||||||
|
),
|
||||||
|
findsOneWidget,
|
||||||
|
);
|
||||||
|
expect(
|
||||||
|
find.ancestor(
|
||||||
|
of: find.text(l10n.alarmSnoozeOptionLabel(10)),
|
||||||
|
matching: find.byType(OutlinedButton),
|
||||||
|
),
|
||||||
|
findsOneWidget,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
testWidgets(
|
||||||
|
'design decision: cuando snoozeMinutos no es 3/5/10, el destacado '
|
||||||
|
'por defecto es 10 (el valor "habitual" del mockup)',
|
||||||
|
(tester) async {
|
||||||
|
await _montarPantalla(tester, snoozeMinutos: 7);
|
||||||
|
|
||||||
|
expect(
|
||||||
|
find.ancestor(
|
||||||
|
of: find.text(l10n.alarmSnoozeOptionLabel(10)),
|
||||||
|
matching: find.byType(FilledButton),
|
||||||
|
),
|
||||||
|
findsOneWidget,
|
||||||
|
);
|
||||||
|
expect(
|
||||||
|
find.ancestor(
|
||||||
|
of: find.text(l10n.alarmSnoozeOptionLabel(3)),
|
||||||
|
matching: find.byType(OutlinedButton),
|
||||||
|
),
|
||||||
|
findsOneWidget,
|
||||||
|
);
|
||||||
|
expect(
|
||||||
|
find.ancestor(
|
||||||
|
of: find.text(l10n.alarmSnoozeOptionLabel(5)),
|
||||||
|
matching: find.byType(OutlinedButton),
|
||||||
|
),
|
||||||
|
findsOneWidget,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
group('WU11 — pildora de Detener a todo lo ancho', () {
|
||||||
|
testWidgets('el boton de Detener ocupa todo el ancho disponible', (
|
||||||
|
tester,
|
||||||
|
) async {
|
||||||
|
await _montarPantalla(tester);
|
||||||
|
|
||||||
|
// The test viewport is 1440 logical px wide (physicalSize / ratio set
|
||||||
|
// in `_montarPantalla`); a normal wrap-content button would be well
|
||||||
|
// under 300px. This is a "clearly full-bleed, not auto-sized"
|
||||||
|
// assertion rather than a pixel-perfect one — the exact horizontal
|
||||||
|
// padding is a cosmetic layout detail, not a contract.
|
||||||
|
final tamano = tester.getSize(
|
||||||
|
find.byKey(const ValueKey('ringing-stop-button')),
|
||||||
|
);
|
||||||
|
expect(tamano.width, greaterThan(1000));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
group('WU11 — estado estatico de subida de volumen (resolucion 4, sin '
|
||||||
|
'contador en vivo)', () {
|
||||||
|
testWidgets(
|
||||||
|
'con fadeInSegundos > 0 muestra la etiqueta estatica, sin sufijo '
|
||||||
|
'numerico',
|
||||||
|
(tester) async {
|
||||||
|
await _montarPantalla(tester, fadeInSegundos: 20);
|
||||||
|
|
||||||
|
final texto = tester.widget<Text>(
|
||||||
|
find.descendant(
|
||||||
|
of: find.byKey(const ValueKey('estado-subida-volumen')),
|
||||||
|
matching: find.byType(Text),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
// Exact-equality (not `contains`) is what proves there is no
|
||||||
|
// interpolated/changing suffix at all — `alarmVolumeRisingStatus`
|
||||||
|
// carries no ARB placeholder, so this can never silently grow a
|
||||||
|
// live counter later without a deliberate key change.
|
||||||
|
expect(texto.data, l10n.alarmVolumeRisingStatus);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
testWidgets(
|
||||||
|
'con fadeInSegundos == 0 (por defecto) no muestra la etiqueta en '
|
||||||
|
'absoluto',
|
||||||
|
(tester) async {
|
||||||
|
await _montarPantalla(tester);
|
||||||
|
|
||||||
|
expect(
|
||||||
|
find.byKey(const ValueKey('estado-subida-volumen')),
|
||||||
|
findsNothing,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
testWidgets(
|
testWidgets(
|
||||||
'posponer 5 min pospone la alarma y cierra la pantalla (S2-R1-B)',
|
'posponer 5 min pospone la alarma y cierra la pantalla (S2-R1-B)',
|
||||||
@@ -196,19 +319,18 @@ void main() {
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
testWidgets(
|
testWidgets('detener confirmado no muestra el banner de fallo (SS-3c)', (
|
||||||
'detener confirmado no muestra el banner de fallo (SS-3c)',
|
tester,
|
||||||
(tester) async {
|
) async {
|
||||||
final entorno = await _montarPantalla(tester);
|
final entorno = await _montarPantalla(tester);
|
||||||
|
|
||||||
await tester.tap(find.text(l10n.stopAlarmAction));
|
await tester.tap(find.text(l10n.stopAlarmAction));
|
||||||
await tester.pumpAndSettle();
|
await tester.pumpAndSettle();
|
||||||
|
|
||||||
expect(find.byType(PantallaAlarmaSonando), findsNothing);
|
expect(find.byType(PantallaAlarmaSonando), findsNothing);
|
||||||
expect(find.text(l10n.alarmStopFailedMessage), findsNothing);
|
expect(find.text(l10n.alarmStopFailedMessage), findsNothing);
|
||||||
expect(entorno.android.detencionesActivas, isNotEmpty);
|
expect(entorno.android.detencionesActivas, isNotEmpty);
|
||||||
},
|
});
|
||||||
);
|
|
||||||
|
|
||||||
testWidgets(
|
testWidgets(
|
||||||
'forzar detencion: invocacion superpuesta es no-op y tras un fallo '
|
'forzar detencion: invocacion superpuesta es no-op y tras un fallo '
|
||||||
|
|||||||
Reference in New Issue
Block a user