fix(alarma-sonando): date line, additive snooze qualifier
Audit 9.4 (t4:419): "Lunes, 3 de agosto" now renders between the schedule pill and the hero time -- purely additive, a new sibling Text touching neither element. New formato_fechas.dart helper fechaLargaConDiaSemana (locale-aware via DateFormat.MMMMEEEEd). Audit 9.10 (t4:433): the highlighted snooze tile gains a "usual" qualifier (new ARB key alarmSnoozeUsualLabel) alongside the original flat label -- resolved DIFFERENTLY than its sibling 9.9 (permanently rejected, id 2525): instead of splitting the flat alarmSnoozeOptionLabel string into two differently-sized Text nodes (which would make it vanish from the render tree the protected dismiss-guard test locates via find.text), a SEPARATE small Text is added alongside it. The original label stays a single, untouched Text node, still inside the same FilledButton the guard taps. Verified against the full ringing-screen test surface (34 tests across 5 files, including the protected dismiss-guard file) -- all green, dismiss-guard file reconfirmed byte-identical to main.
This commit is contained in:
@@ -225,6 +225,25 @@ class _PantallaAlarmaSonandoState extends State<PantallaAlarmaSonando> {
|
||||
tokens: tokens,
|
||||
),
|
||||
const SizedBox(height: 22),
|
||||
// Audit 9.4 (t4 line 419): "Lunes, 3 de agosto" between
|
||||
// the pill and the hero time -- never rendered before.
|
||||
// Purely additive: a new sibling Text, touching neither
|
||||
// the pill above nor the hero time below.
|
||||
Text(
|
||||
fechaLargaConDiaSemana(
|
||||
Localizations.localeOf(context).toString(),
|
||||
DateTime.now(),
|
||||
),
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: Theme.of(
|
||||
context,
|
||||
).colorScheme.onSurface.withValues(alpha: 0.6),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 6),
|
||||
FittedBox(
|
||||
fit: BoxFit.scaleDown,
|
||||
child: Text(
|
||||
@@ -689,6 +708,20 @@ class _FilaSnoozeFija extends StatelessWidget {
|
||||
final forma = RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(tokens.radiusMd),
|
||||
);
|
||||
// Audit 9.10 (t4 line 433): the prototype stacks a big NUMBER over a
|
||||
// small "min · habitual" unit — the SAME text-splitting conflict as
|
||||
// 9.9 (permanently rejected, Engram id 2525): this flat string is
|
||||
// exactly what the protected dismiss-guard test locates via
|
||||
// `find.text(l10n.alarmSnoozeOptionLabel(N))` in four places, and
|
||||
// splitting it into two differently-styled Text nodes would make
|
||||
// that flat value vanish from the render tree.
|
||||
//
|
||||
// Resolved differently here than 9.9: rather than splitting THIS
|
||||
// string, an entirely SEPARATE small qualifier Text is added
|
||||
// alongside it (only on the destacado tile) — the original flat
|
||||
// label stays a single, untouched, unstyled-differently Text node,
|
||||
// still the exact widget the guard finds and taps. This delivers
|
||||
// the "habitual" qualifier without the conflict 9.9 hit.
|
||||
final etiqueta = Text(l10n.alarmSnoozeOptionLabel(minutos));
|
||||
return Expanded(
|
||||
flex: esDestacado ? 3 : 2,
|
||||
@@ -703,7 +736,21 @@ class _FilaSnoozeFija extends StatelessWidget {
|
||||
foregroundColor: tokens.deepViolet,
|
||||
shape: forma,
|
||||
),
|
||||
child: etiqueta,
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
etiqueta,
|
||||
Text(
|
||||
l10n.alarmSnoozeUsualLabel,
|
||||
style: TextStyle(
|
||||
fontSize: 11,
|
||||
fontWeight: FontWeight.w800,
|
||||
color: tokens.deepViolet.withValues(alpha: 0.75),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
)
|
||||
: OutlinedButton(
|
||||
onPressed: () => onPosponer(minutos),
|
||||
|
||||
@@ -0,0 +1,162 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:pluriwave/estado/estado_alarmas.dart';
|
||||
import 'package:pluriwave/estado/estado_radio.dart';
|
||||
import 'package:pluriwave/l10n/formato_fechas.dart';
|
||||
import 'package:pluriwave/l10n/gen/app_localizations.dart';
|
||||
import 'package:pluriwave/modelos/alarma_musical.dart';
|
||||
import 'package:pluriwave/modelos/emisora.dart';
|
||||
import 'package:pluriwave/pantallas/pantalla_alarma_sonando.dart';
|
||||
import 'package:pluriwave/servicios/servicio_alarmas.dart';
|
||||
import 'package:pluriwave/servicios/servicio_audio.dart';
|
||||
import 'package:pluriwave/tema/pluriwave_theme.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
|
||||
import '../helpers/fakes.dart';
|
||||
import '../helpers/fakes_alarmas.dart';
|
||||
|
||||
/// Closing batch (items 9.4, 9.10, audit id 2521) -- the ringing screen.
|
||||
/// Mirrors `pantalla_alarma_sonando_tier4_test.dart`'s own harness (kept as
|
||||
/// a local copy, not a shared import, matching this file's established
|
||||
/// precedent of small helper duplication over cross-test-file coupling).
|
||||
Future<void> _montarPantalla(WidgetTester tester) async {
|
||||
tester.view.physicalSize = const Size(1440, 3200);
|
||||
tester.view.devicePixelRatio = 1.0;
|
||||
addTearDown(tester.view.resetPhysicalSize);
|
||||
addTearDown(tester.view.resetDevicePixelRatio);
|
||||
|
||||
final audio = FakeServicioAudio();
|
||||
audio.emitirEstado(EstadoReproduccion.reproduciendo);
|
||||
final radio = EstadoRadio(
|
||||
audio: audio,
|
||||
favoritos: FakeServicioFavoritos(),
|
||||
radio: FakeServicioRadio(),
|
||||
servicioEcualizador: FakeServicioEcualizador(),
|
||||
servicioGrabacion: FakeServicioGrabacionRadioInactiva(),
|
||||
iniciarAutomaticamente: false,
|
||||
);
|
||||
addTearDown(radio.dispose);
|
||||
|
||||
final android = FakePuertoAlarmasAndroid();
|
||||
final estadoAlarmas = EstadoAlarmas(
|
||||
servicio: ServicioAlarmas(reloj: () => DateTime(2026, 6, 11, 7, 0)),
|
||||
android: android,
|
||||
iniciarAutomaticamente: false,
|
||||
);
|
||||
addTearDown(estadoAlarmas.dispose);
|
||||
addTearDown(android.dispose);
|
||||
await estadoAlarmas.guardarAlarma(
|
||||
const AlarmaMusical(
|
||||
id: 'tier5-1',
|
||||
nombre: 'Despertar',
|
||||
hora: 7,
|
||||
minuto: 30,
|
||||
tipoProgramacion: TipoProgramacionAlarma.diaria,
|
||||
diasSemana: [],
|
||||
snoozeMinutos: 10,
|
||||
emisora: Emisora(
|
||||
uuid: 'e1',
|
||||
nombre: 'Radio Uno',
|
||||
url: 'https://radio.example/stream',
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
await tester.pumpWidget(
|
||||
MultiProvider(
|
||||
providers: [
|
||||
ChangeNotifierProvider<EstadoRadio>.value(value: radio),
|
||||
ChangeNotifierProvider<EstadoAlarmas>.value(value: estadoAlarmas),
|
||||
],
|
||||
child: MaterialApp(
|
||||
locale: const Locale('es'),
|
||||
localizationsDelegates: AppLocalizations.localizationsDelegates,
|
||||
supportedLocales: AppLocalizations.supportedLocales,
|
||||
theme: PluriWaveTheme.dark(),
|
||||
home: const SizedBox.shrink(),
|
||||
),
|
||||
),
|
||||
);
|
||||
final navigator = tester.state<NavigatorState>(find.byType(Navigator));
|
||||
unawaited(
|
||||
navigator.push(
|
||||
MaterialPageRoute<void>(
|
||||
builder:
|
||||
(_) => PantallaAlarmaSonando(alarma: estadoAlarmas.alarmas.single),
|
||||
fullscreenDialog: true,
|
||||
),
|
||||
),
|
||||
);
|
||||
await tester.pump();
|
||||
await tester.pump(const Duration(milliseconds: 50));
|
||||
}
|
||||
|
||||
void main() {
|
||||
setUp(() {
|
||||
SharedPreferences.setMockInitialValues({});
|
||||
});
|
||||
|
||||
testWidgets('visual fidelity (audit 9.4): the date line renders between the '
|
||||
'schedule pill and the hero time (t4:419)', (tester) async {
|
||||
await _montarPantalla(tester);
|
||||
|
||||
final localeTag =
|
||||
Localizations.localeOf(
|
||||
tester.element(find.byType(PantallaAlarmaSonando)),
|
||||
).toString();
|
||||
final esperado = fechaLargaConDiaSemana(localeTag, DateTime.now());
|
||||
|
||||
expect(find.text(esperado), findsOneWidget);
|
||||
|
||||
// Order: pill above the date line, date line above the hero time.
|
||||
final pillY =
|
||||
tester
|
||||
.getBottomLeft(find.byKey(const ValueKey('ringing-schedule-pill')))
|
||||
.dy;
|
||||
final dateY = tester.getTopLeft(find.text(esperado)).dy;
|
||||
final timeY =
|
||||
tester.getTopLeft(find.byKey(const ValueKey('ringing-hero-time'))).dy;
|
||||
expect(pillY <= dateY, isTrue);
|
||||
expect(dateY <= timeY, isTrue);
|
||||
|
||||
// Regression guard: pumpAndSettle must still complete (purely
|
||||
// additive static text, no new animation).
|
||||
await tester.pumpAndSettle();
|
||||
});
|
||||
|
||||
testWidgets(
|
||||
'visual fidelity (audit 9.10): the highlighted snooze tile carries a '
|
||||
'"usual" qualifier ADDITIVELY -- the original flat label the dismiss '
|
||||
'guard finds by text stays fully intact',
|
||||
(tester) async {
|
||||
await _montarPantalla(tester);
|
||||
|
||||
final l10n = AppLocalizations.of(
|
||||
tester.element(find.byType(PantallaAlarmaSonando)),
|
||||
);
|
||||
|
||||
// The ORIGINAL flat label must still resolve to exactly one Text --
|
||||
// this IS the dismiss-guard's own finder contract.
|
||||
expect(find.text(l10n.alarmSnoozeOptionLabel(10)), findsOneWidget);
|
||||
// The additive qualifier renders too, but only on the destacado tile.
|
||||
expect(find.text(l10n.alarmSnoozeUsualLabel), findsOneWidget);
|
||||
expect(find.text(l10n.alarmSnoozeOptionLabel(3)), findsOneWidget);
|
||||
expect(find.text(l10n.alarmSnoozeOptionLabel(5)), findsOneWidget);
|
||||
|
||||
// Regression guard: the exact hazard 9.9/9.10 share -- pumpAndSettle
|
||||
// must still complete, and the flat label must remain the SAME
|
||||
// widget the dismiss guard taps (still inside a FilledButton).
|
||||
expect(
|
||||
find.ancestor(
|
||||
of: find.text(l10n.alarmSnoozeOptionLabel(10)),
|
||||
matching: find.byType(FilledButton),
|
||||
),
|
||||
findsOneWidget,
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
},
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user