fix(alarmas): New pill, banner layout, vacation pill, card recurrence
Audit 7.1 (t4:325): the "New" action is a solid brand-teal pill with a plain add glyph -- was a tonal button with auto_awesome. Audit 7.2 (t4:326-330): the next-alarm banner is warmCoral-tinted with the Skip chip BESIDE the text on the same row -- was an opaque default card with the skip action stacked below as an OutlinedButton. Audit 7.3 (t4:332): the vacation row gains a trailing "d-d MON" date-range pill for the active-or-next range, built from the existing EstadoAlarmas.rangoVacacionesActivo/vacacionesProximas() accessors -- new formato_fechas.dart helper, no new state. Audit 7.4 (t4:337-345): the alarm card now shows a recurrence label next to the giant time (reusing the existing oneTimeOption/ dailyOption/weekdaysOption strings) and a themed station-icon slot next to the station name. The real per-station favicon is NOT rendered here -- same network-image hazard already documented for the ringing screen's audit 9.2 (Emisora.favicon is a network URL; Image.network hangs widget tests without a mocked HttpClient). The custom switch shape (52x32/26px thumb) is also left as Switch.adaptive -- a disclosed sub-gap, not a silent drop. Item 7.6 (_AccesoDiagnostico, an Android-reliability debug row) is a pre-approved addition not in the prototype -- informational only, no action needed.
This commit is contained in:
@@ -22,3 +22,21 @@ String diaMesLocalizado(String localeTag, DateTime fecha) =>
|
||||
/// audit 9b.4 (t4:459), the small caption under the day-month.
|
||||
String nombreDiaSemanaLocalizado(String localeTag, DateTime fecha) =>
|
||||
DateFormat.EEEE(localeTag).format(fecha);
|
||||
|
||||
/// Full weekday + month + day, locale-aware (e.g. "lunes, 3 de agosto" for
|
||||
/// `es`, "Monday, August 3" for `en`) — audit 9.4 (t4:419), the ringing
|
||||
/// screen's date line between the schedule pill and the hero time.
|
||||
String fechaLargaConDiaSemana(String localeTag, DateTime fecha) =>
|
||||
DateFormat.MMMMEEEEd(localeTag).format(fecha);
|
||||
|
||||
/// Short "d–d MON" range pill, locale-aware month abbreviation, uppercased
|
||||
/// (e.g. "4–18 AGO" for `es`, "4–18 AUG" for `en`) — audit 7.3 (t4:332),
|
||||
/// the vacation-row date-range pill on the Alarmas root. Always labels the
|
||||
/// range with the END date's month: vacation ranges are short (days to a
|
||||
/// couple of weeks), so a cross-month span is the rare case, and the
|
||||
/// prototype itself only ever shows a single abbreviation.
|
||||
String rangoFechasCorto(String localeTag, DateTime inicio, DateTime fin) {
|
||||
final dia = DateFormat.d(localeTag);
|
||||
final mes = DateFormat.MMM(localeTag).format(fin).toUpperCase();
|
||||
return '${dia.format(inicio)}–${dia.format(fin)} $mes';
|
||||
}
|
||||
|
||||
@@ -45,9 +45,26 @@ class PantallaAlarmas extends StatelessWidget {
|
||||
title: l10n.alarmScreenTitle,
|
||||
onSleepTimer: () => showPluriSleepTimerSheet(context),
|
||||
actions: [
|
||||
FilledButton.tonalIcon(
|
||||
// Audit 7.1 (t4:325): a solid brand-teal pill with a plain
|
||||
// `add` glyph -- was a tonal button with `auto_awesome`.
|
||||
FilledButton.icon(
|
||||
style: FilledButton.styleFrom(
|
||||
backgroundColor: PluriWaveTokens.brand,
|
||||
foregroundColor: const Color(0xFF062126),
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 14,
|
||||
vertical: 9,
|
||||
),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
textStyle: const TextStyle(
|
||||
fontSize: 13,
|
||||
fontWeight: FontWeight.w800,
|
||||
),
|
||||
),
|
||||
onPressed: () => _abrirEditor(context),
|
||||
icon: const Icon(Icons.auto_awesome_rounded, size: 18),
|
||||
icon: const Icon(Icons.add_rounded, size: 18),
|
||||
label: Text(l10n.createAlarmAction),
|
||||
),
|
||||
],
|
||||
@@ -105,6 +122,7 @@ class _PanelProximaAlarma extends StatelessWidget {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final l10n = AppLocalizations.of(context);
|
||||
final tokens = context.pluriTokens;
|
||||
final proxima = estado.proximaAlarma;
|
||||
final activasSinProxima =
|
||||
estado.alarmas
|
||||
@@ -112,56 +130,87 @@ class _PanelProximaAlarma extends StatelessWidget {
|
||||
.length;
|
||||
final proximaProgramable = proxima?.proximaProgramable;
|
||||
|
||||
return PluriGlassSurface(
|
||||
glowColor: context.pluriTokens.warmCoral.withValues(alpha: 0.28),
|
||||
child: Row(
|
||||
children: [
|
||||
_AssetIcon(
|
||||
'assets/icons/alarmas/alarm_music.png',
|
||||
size: 72,
|
||||
semanticLabel: l10n.alarmIconLabel,
|
||||
),
|
||||
const SizedBox(width: 14),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
proxima == null
|
||||
? activasSinProxima > 0
|
||||
? l10n.activeAlarmsWithoutNextTitle
|
||||
: l10n.noActiveAlarms
|
||||
: l10n.nextAlarmTitle,
|
||||
style: Theme.of(context).textTheme.titleMedium?.copyWith(
|
||||
fontWeight: FontWeight.w900,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
proxima == null
|
||||
? activasSinProxima > 0
|
||||
? l10n.activeAlarmsWithoutNextSubtitle(
|
||||
activasSinProxima,
|
||||
)
|
||||
: l10n.createAlarmHint
|
||||
: '${_nombreVisibleAlarma(l10n, proxima)} · ${_fechaHora(l10n, proximaProgramable!)}',
|
||||
),
|
||||
if (proxima != null) ...[
|
||||
const SizedBox(height: 10),
|
||||
Align(
|
||||
alignment: Alignment.centerLeft,
|
||||
child: OutlinedButton.icon(
|
||||
key: const ValueKey('hero-skip-next'),
|
||||
onPressed: () => _saltarDesdeHero(context, proxima),
|
||||
icon: const Icon(Icons.skip_next_rounded, size: 18),
|
||||
label: Text(l10n.alarmHeroSkipAction),
|
||||
// Audit 7.2 (t4:326-330): warmCoral-tinted card, `alarm_on` icon at
|
||||
// 26px, and the "Saltar" chip BESIDE the text on the same row -- was
|
||||
// an opaque default card with a 72px PNG and the skip action stacked
|
||||
// BELOW the text as an OutlinedButton.
|
||||
return DecoratedBox(
|
||||
key: const ValueKey('next-alarm-banner'),
|
||||
decoration: BoxDecoration(
|
||||
color: tokens.warmCoral.withValues(alpha: 0.13),
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
border: Border.all(color: tokens.warmCoral.withValues(alpha: 0.34)),
|
||||
),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 14),
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
Icon(Icons.alarm_on, size: 26, color: tokens.warmCoral),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Text(
|
||||
proxima == null
|
||||
? activasSinProxima > 0
|
||||
? l10n.activeAlarmsWithoutNextTitle
|
||||
: l10n.noActiveAlarms
|
||||
: l10n.nextAlarmTitle,
|
||||
style: Theme.of(context).textTheme.titleMedium?.copyWith(
|
||||
fontWeight: FontWeight.w900,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
proxima == null
|
||||
? activasSinProxima > 0
|
||||
? l10n.activeAlarmsWithoutNextSubtitle(
|
||||
activasSinProxima,
|
||||
)
|
||||
: l10n.createAlarmHint
|
||||
: '${_nombreVisibleAlarma(l10n, proxima)} · ${_fechaHora(l10n, proximaProgramable!)}',
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
if (proxima != null) ...[
|
||||
const SizedBox(width: 8),
|
||||
_ChipSaltar(onTap: () => _saltarDesdeHero(context, proxima)),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Audit 7.2 (t4:329): `padding:8px 12px;radius:10;rgba(255,255,255,.08)` --
|
||||
/// plain text, no icon, unlike the previous `OutlinedButton.icon`.
|
||||
class _ChipSaltar extends StatelessWidget {
|
||||
const _ChipSaltar({required this.onTap});
|
||||
|
||||
final VoidCallback onTap;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final l10n = AppLocalizations.of(context);
|
||||
return Material(
|
||||
color: Colors.white.withValues(alpha: 0.08),
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
child: InkWell(
|
||||
key: const ValueKey('hero-skip-next'),
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
onTap: onTap,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
|
||||
child: Text(
|
||||
l10n.alarmHeroSkipAction,
|
||||
style: const TextStyle(fontSize: 12, fontWeight: FontWeight.w800),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -243,17 +292,83 @@ class _TarjetaAlarma extends StatelessWidget {
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
_hora(alarma),
|
||||
style: Theme.of(
|
||||
context,
|
||||
).textTheme.displaySmall?.copyWith(
|
||||
fontWeight: FontWeight.w900,
|
||||
letterSpacing: -1.5,
|
||||
),
|
||||
// Audit 7.4 (t4:339): the recurrence label sits on
|
||||
// the SAME baseline as the giant time -- it used to
|
||||
// be missing entirely.
|
||||
Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.baseline,
|
||||
textBaseline: TextBaseline.alphabetic,
|
||||
children: [
|
||||
Text(
|
||||
_hora(alarma),
|
||||
style: Theme.of(
|
||||
context,
|
||||
).textTheme.displaySmall?.copyWith(
|
||||
fontWeight: FontWeight.w900,
|
||||
letterSpacing: -1.5,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Text(
|
||||
_recurrenciaCorta(l10n, alarma),
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.w700,
|
||||
color: Theme.of(
|
||||
context,
|
||||
).colorScheme.onSurface.withValues(alpha: 0.5),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 7),
|
||||
// Audit 7.4 (t4:341): a small station-art slot
|
||||
// inline with the name -- was the name alone.
|
||||
// `Emisora.favicon` is a network URL (the same
|
||||
// hazard documented for the ringing screen's audit
|
||||
// 9.2 -- `Image.network` here would hang widget
|
||||
// tests without a mocked HttpClient), so this is a
|
||||
// themed fallback icon, not the real per-station
|
||||
// artwork, mirroring the recordings-row precedent
|
||||
// (`pantalla_grabaciones.dart`'s `_FilaGrabacion`).
|
||||
Row(
|
||||
children: [
|
||||
if (alarma.emisora != null) ...[
|
||||
DecoratedBox(
|
||||
key: const ValueKey('tarjeta-alarma-arte'),
|
||||
decoration: BoxDecoration(
|
||||
color: tokens.listSurface,
|
||||
borderRadius: BorderRadius.circular(6),
|
||||
),
|
||||
child: SizedBox(
|
||||
width: 22,
|
||||
height: 22,
|
||||
child: Icon(
|
||||
Icons.radio_rounded,
|
||||
size: 14,
|
||||
color: Theme.of(context)
|
||||
.colorScheme
|
||||
.onSurface
|
||||
.withValues(alpha: 0.75),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 7),
|
||||
],
|
||||
Flexible(
|
||||
child: Text(
|
||||
estacion,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: TextStyle(
|
||||
fontSize: 12.5,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: Theme.of(context).colorScheme.onSurface
|
||||
.withValues(alpha: 0.75),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 2),
|
||||
Text(estacion, overflow: TextOverflow.ellipsis),
|
||||
],
|
||||
),
|
||||
),
|
||||
@@ -1089,6 +1204,12 @@ class _PanelVacaciones extends StatelessWidget {
|
||||
final l10n = AppLocalizations.of(context);
|
||||
final tokens = context.pluriTokens;
|
||||
final resumen = _resumenVacaciones(l10n, estado.vacaciones);
|
||||
// Audit 7.3 (t4:332): a trailing date-range pill ("4-18 AGO") for the
|
||||
// active-or-next range -- never rendered anywhere before.
|
||||
final proximas = estado.vacacionesProximas();
|
||||
final rangoRelevante =
|
||||
estado.rangoVacacionesActivo() ??
|
||||
(proximas.isEmpty ? null : proximas.first);
|
||||
return PluriGlassSurface(
|
||||
glowColor: PluriWaveTokens.skyBlue.withValues(alpha: 0.22),
|
||||
padding: EdgeInsets.zero,
|
||||
@@ -1122,6 +1243,10 @@ class _PanelVacaciones extends StatelessWidget {
|
||||
],
|
||||
),
|
||||
),
|
||||
if (rangoRelevante != null) ...[
|
||||
const SizedBox(width: 8),
|
||||
_PildoraFechasVacaciones(rango: rangoRelevante),
|
||||
],
|
||||
const Icon(Icons.chevron_right_rounded),
|
||||
],
|
||||
),
|
||||
@@ -1168,6 +1293,40 @@ class _PanelVacaciones extends StatelessWidget {
|
||||
}
|
||||
}
|
||||
|
||||
/// Audit 7.3 (t4:332): "4-18 AGO" -- `radius:999`, `liveGreen@.16` fill,
|
||||
/// `liveGreen@.38` border, 10.5px/w800 in `liveGreen` (matches the
|
||||
/// prototype's own `rgba(126,228,194,...)` teal, not the alarm banner's
|
||||
/// warmCoral).
|
||||
class _PildoraFechasVacaciones extends StatelessWidget {
|
||||
const _PildoraFechasVacaciones({required this.rango});
|
||||
|
||||
final RangoVacaciones rango;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final tokens = context.pluriTokens;
|
||||
final locale = AppLocalizations.of(context).localeName;
|
||||
return DecoratedBox(
|
||||
decoration: BoxDecoration(
|
||||
color: tokens.liveGreen.withValues(alpha: 0.16),
|
||||
borderRadius: BorderRadius.circular(999),
|
||||
border: Border.all(color: tokens.liveGreen.withValues(alpha: 0.38)),
|
||||
),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 9, vertical: 4),
|
||||
child: Text(
|
||||
rangoFechasCorto(locale, rango.inicioDia, rango.finDia),
|
||||
style: TextStyle(
|
||||
fontSize: 10.5,
|
||||
fontWeight: FontWeight.w800,
|
||||
color: tokens.liveGreen,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Audit 8.4 (t4 lines 383-390): one circular weekday button in the alarm
|
||||
/// editor's REPETIR row — was a `FilterChip`. `aspect-ratio:1` in the
|
||||
/// prototype is achieved here by the caller wrapping each instance in an
|
||||
@@ -1375,3 +1534,17 @@ String _weekdayShort(AppLocalizations l10n, int day) => switch (day) {
|
||||
// S5-R4: short dates follow the active locale (en-US = M/D/Y, ja = Y/M/D).
|
||||
String _fechaCorta(AppLocalizations l10n, DateTime fecha) =>
|
||||
fechaCortaLocalizada(l10n.localeName, fecha);
|
||||
|
||||
/// Audit 7.4 (t4:339): a compact recurrence label next to the alarm card's
|
||||
/// giant time. Reuses the SAME generic labels the editor's own
|
||||
/// `TipoProgramacionAlarma` `SegmentedButton` already shows (`oneTimeOption`
|
||||
/// / `dailyOption` / `weekdaysOption`) rather than inventing a new, more
|
||||
/// specific ARB string -- honest given the space (12px, next to a 34px
|
||||
/// time) genuinely only fits a short word, not a full weekday list.
|
||||
String _recurrenciaCorta(AppLocalizations l10n, AlarmaMusical alarma) {
|
||||
return switch (alarma.tipoProgramacion) {
|
||||
TipoProgramacionAlarma.diaria => l10n.dailyOption,
|
||||
TipoProgramacionAlarma.diasSemana => l10n.weekdaysOption,
|
||||
TipoProgramacionAlarma.unica => l10n.oneTimeOption,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -0,0 +1,193 @@
|
||||
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/gen/app_localizations.dart';
|
||||
import 'package:pluriwave/modelos/alarma_musical.dart';
|
||||
import 'package:pluriwave/modelos/emisora.dart';
|
||||
import 'package:pluriwave/pantallas/pantalla_alarmas.dart';
|
||||
import 'package:pluriwave/servicios/servicio_alarmas.dart';
|
||||
import 'package:pluriwave/tema/pluriwave_tokens.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
|
||||
import '../helpers/fakes.dart';
|
||||
import '../helpers/fakes_alarmas.dart';
|
||||
|
||||
/// Tier 5 visual-fidelity closeout (items 7.1-7.4, audit id 2521) --
|
||||
/// screens 4-14 batch.
|
||||
void main() {
|
||||
setUp(() {
|
||||
SharedPreferences.setMockInitialValues({});
|
||||
});
|
||||
|
||||
Future<void> montar(
|
||||
WidgetTester tester, {
|
||||
required EstadoAlarmas estadoAlarmas,
|
||||
EstadoRadio? radio,
|
||||
}) async {
|
||||
tester.view.physicalSize = const Size(1440, 3200);
|
||||
tester.view.devicePixelRatio = 1.0;
|
||||
addTearDown(tester.view.resetPhysicalSize);
|
||||
addTearDown(tester.view.resetDevicePixelRatio);
|
||||
|
||||
final estadoRadio =
|
||||
radio ??
|
||||
EstadoRadio(
|
||||
audio: FakeServicioAudio(),
|
||||
favoritos: FakeServicioFavoritos(),
|
||||
radio: FakeServicioRadio(),
|
||||
servicioEcualizador: FakeServicioEcualizador(),
|
||||
servicioGrabacion: FakeServicioGrabacionRadioInactiva(),
|
||||
iniciarAutomaticamente: false,
|
||||
);
|
||||
if (radio == null) addTearDown(estadoRadio.dispose);
|
||||
|
||||
await tester.pumpWidget(
|
||||
MultiProvider(
|
||||
providers: [
|
||||
ChangeNotifierProvider<EstadoRadio>.value(value: estadoRadio),
|
||||
ChangeNotifierProvider<EstadoAlarmas>.value(value: estadoAlarmas),
|
||||
],
|
||||
child: MaterialApp(
|
||||
locale: const Locale('en'),
|
||||
localizationsDelegates: AppLocalizations.localizationsDelegates,
|
||||
supportedLocales: AppLocalizations.supportedLocales,
|
||||
home: const Scaffold(body: PantallaAlarmas()),
|
||||
),
|
||||
),
|
||||
);
|
||||
await tester.pump();
|
||||
await tester.pump(const Duration(milliseconds: 100));
|
||||
}
|
||||
|
||||
EstadoAlarmas crearEstado() {
|
||||
final android = FakePuertoAlarmasAndroid();
|
||||
final estado = EstadoAlarmas(
|
||||
servicio: ServicioAlarmas(reloj: () => DateTime(2026, 6, 11, 6, 0)),
|
||||
android: android,
|
||||
iniciarAutomaticamente: false,
|
||||
);
|
||||
return estado;
|
||||
}
|
||||
|
||||
group('visual fidelity (audit 7.1): "New" action', () {
|
||||
testWidgets('is a solid brand-teal pill with an add glyph, not the tonal '
|
||||
'auto_awesome button (t4:325)', (tester) async {
|
||||
final estado = crearEstado();
|
||||
addTearDown(estado.dispose);
|
||||
await montar(tester, estadoAlarmas: estado);
|
||||
|
||||
expect(find.byIcon(Icons.auto_awesome_rounded), findsNothing);
|
||||
expect(find.byIcon(Icons.add_rounded), findsOneWidget);
|
||||
|
||||
final l10n = lookupAppLocalizations(const Locale('en'));
|
||||
final boton = tester.widget<FilledButton>(find.byType(FilledButton));
|
||||
final resuelto = boton.style?.backgroundColor?.resolve(<WidgetState>{});
|
||||
expect(resuelto, PluriWaveTokens.brand);
|
||||
expect(find.text(l10n.createAlarmAction), findsOneWidget);
|
||||
});
|
||||
});
|
||||
|
||||
group('visual fidelity (audit 7.2): next-alarm banner', () {
|
||||
testWidgets(
|
||||
'is warmCoral-tinted and the Skip chip sits BESIDE the text, not '
|
||||
'below it (t4:327-331)',
|
||||
(tester) async {
|
||||
final estado = crearEstado();
|
||||
addTearDown(estado.dispose);
|
||||
await estado.guardarAlarma(
|
||||
const AlarmaMusical(
|
||||
id: 'proxima',
|
||||
nombre: 'Despertar',
|
||||
hora: 7,
|
||||
minuto: 30,
|
||||
tipoProgramacion: TipoProgramacionAlarma.diaria,
|
||||
diasSemana: [],
|
||||
),
|
||||
);
|
||||
await montar(tester, estadoAlarmas: estado);
|
||||
|
||||
final l10n = lookupAppLocalizations(const Locale('en'));
|
||||
final banner = tester.widget<DecoratedBox>(
|
||||
find.byKey(const ValueKey('next-alarm-banner')),
|
||||
);
|
||||
final decoration = banner.decoration as BoxDecoration;
|
||||
expect(
|
||||
decoration.color,
|
||||
const Color(0xFFF4B860).withValues(alpha: 0.13),
|
||||
);
|
||||
|
||||
final skipY = tester.getCenter(find.text(l10n.alarmHeroSkipAction)).dy;
|
||||
final titleY = tester.getCenter(find.byIcon(Icons.alarm_on)).dy;
|
||||
expect(
|
||||
(skipY - titleY).abs() < 4,
|
||||
isTrue,
|
||||
reason: 'the Skip chip sits on the SAME row as the icon/text',
|
||||
);
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
group('visual fidelity (audit 7.3): vacation row date pill', () {
|
||||
testWidgets('shows the upcoming range as a "d-d MON" pill (t4:332)', (
|
||||
tester,
|
||||
) async {
|
||||
final estado = crearEstado();
|
||||
addTearDown(estado.dispose);
|
||||
await estado.crearRangoVacaciones(
|
||||
estado.servicio.crearRangoVacaciones(
|
||||
inicio: DateTime(2026, 8, 4),
|
||||
fin: DateTime(2026, 8, 18),
|
||||
nombre: 'Summer',
|
||||
),
|
||||
);
|
||||
await montar(tester, estadoAlarmas: estado);
|
||||
|
||||
expect(find.text('4–18 AUG'), findsOneWidget);
|
||||
});
|
||||
});
|
||||
|
||||
group('visual fidelity (audit 7.4): alarm card', () {
|
||||
testWidgets(
|
||||
'shows a recurrence label next to the time and a station icon next '
|
||||
'to the station name (t4:337-345)',
|
||||
(tester) async {
|
||||
final estado = crearEstado();
|
||||
addTearDown(estado.dispose);
|
||||
await estado.guardarAlarma(
|
||||
const AlarmaMusical(
|
||||
id: 'tarjeta',
|
||||
nombre: 'Despertar',
|
||||
hora: 7,
|
||||
minuto: 30,
|
||||
tipoProgramacion: TipoProgramacionAlarma.diaria,
|
||||
diasSemana: [],
|
||||
emisora: Emisora(
|
||||
uuid: 'e1',
|
||||
nombre: 'Radio Uno',
|
||||
url: 'https://radio.example/stream',
|
||||
),
|
||||
),
|
||||
);
|
||||
await montar(tester, estadoAlarmas: estado);
|
||||
|
||||
final l10n = lookupAppLocalizations(const Locale('en'));
|
||||
expect(
|
||||
find.descendant(
|
||||
of: find.byKey(const ValueKey('tarjeta-alarma-tarjeta')),
|
||||
matching: find.text(l10n.dailyOption),
|
||||
),
|
||||
findsOneWidget,
|
||||
);
|
||||
expect(
|
||||
find.descendant(
|
||||
of: find.byKey(const ValueKey('tarjeta-alarma-tarjeta')),
|
||||
matching: find.byKey(const ValueKey('tarjeta-alarma-arte')),
|
||||
),
|
||||
findsOneWidget,
|
||||
);
|
||||
},
|
||||
);
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user