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:
2026-07-30 16:45:29 +02:00
parent dcb415b3f8
commit 36d7d5f692
2 changed files with 210 additions and 1 deletions
@@ -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();
},
);
}