From 4168dc50192eca536ccb7e24c1ccea9356590f62 Mon Sep 17 00:00:00 2001 From: freetlab Date: Fri, 31 Jul 2026 01:05:46 +0200 Subject: [PATCH] fix(alarmas): show which days a weekday alarm actually fires on MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The alarms list showed a generic "Días" label for a diasSemana alarm instead of its actual configured days. Render the real recurrence (e.g. "Lun, Mié, Vie") by reusing the SAME per-day abbreviation the editor's own day-picker circles already use -- no new formatting scheme, no new ARB keys for the days themselves. Also surface fade/volume/vacation-pause state on the card, each only when it is a genuinely useful deviation from the common case: a fade badge when fadeInSegundos > 0 (reusing the existing alarmFadeInLabel key), a volume percentage when it differs from the 85% default, and a vacation-paused badge when the alarm is both configured to pause and a vacation range is currently active (mirrors the exact predicate ServicioProgramacionAlarmas already uses). One compact line, not a badge per field. Fixes a text-collision regression in pantalla_alarmas_editor_test.dart: opening the editor for an alarm whose own day now renders on its card (e.g. "Lun") made a bare find.text(weekday) ambiguous against the editor's day-picker circle with the same label -- scoped that finder to the BottomSheet subtree. --- lib/l10n/app_ar.arb | 3 +- lib/l10n/app_bn.arb | 3 +- lib/l10n/app_de.arb | 3 +- lib/l10n/app_en.arb | 3 +- lib/l10n/app_es.arb | 3 +- lib/l10n/app_fr.arb | 3 +- lib/l10n/app_hi.arb | 3 +- lib/l10n/app_id.arb | 3 +- lib/l10n/app_it.arb | 3 +- lib/l10n/app_ja.arb | 3 +- lib/l10n/app_pt.arb | 3 +- lib/l10n/app_ru.arb | 3 +- lib/l10n/app_zh.arb | 3 +- lib/l10n/gen/app_localizations.dart | 6 + lib/l10n/gen/app_localizations_ar.dart | 3 + lib/l10n/gen/app_localizations_bn.dart | 3 + lib/l10n/gen/app_localizations_de.dart | 3 + lib/l10n/gen/app_localizations_en.dart | 3 + lib/l10n/gen/app_localizations_es.dart | 3 + lib/l10n/gen/app_localizations_fr.dart | 3 + lib/l10n/gen/app_localizations_hi.dart | 3 + lib/l10n/gen/app_localizations_id.dart | 3 + lib/l10n/gen/app_localizations_it.dart | 3 + lib/l10n/gen/app_localizations_ja.dart | 3 + lib/l10n/gen/app_localizations_pt.dart | 3 + lib/l10n/gen/app_localizations_ru.dart | 3 + lib/l10n/gen/app_localizations_zh.dart | 3 + lib/pantallas/pantalla_alarmas.dart | 99 +++++- .../pantalla_alarmas_editor_test.dart | 21 +- .../pantalla_alarmas_recurrencia_test.dart | 330 ++++++++++++++++++ 30 files changed, 504 insertions(+), 30 deletions(-) create mode 100644 test/pantallas/pantalla_alarmas_recurrencia_test.dart diff --git a/lib/l10n/app_ar.arb b/lib/l10n/app_ar.arb index deb9d1b..489551b 100644 --- a/lib/l10n/app_ar.arb +++ b/lib/l10n/app_ar.arb @@ -840,5 +840,6 @@ "type": "String" } } - } + }, + "alarmCardVacationPausedBadge": "متوقفة مؤقتًا بسبب الإجازة" } diff --git a/lib/l10n/app_bn.arb b/lib/l10n/app_bn.arb index cf70e01..22fe5c8 100644 --- a/lib/l10n/app_bn.arb +++ b/lib/l10n/app_bn.arb @@ -840,5 +840,6 @@ "type": "String" } } - } + }, + "alarmCardVacationPausedBadge": "ছুটির কারণে বিরত" } diff --git a/lib/l10n/app_de.arb b/lib/l10n/app_de.arb index 41067df..5271aba 100644 --- a/lib/l10n/app_de.arb +++ b/lib/l10n/app_de.arb @@ -840,5 +840,6 @@ "type": "String" } } - } + }, + "alarmCardVacationPausedBadge": "Wegen Urlaub pausiert" } diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index 8b80932..7145eed 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -840,5 +840,6 @@ "type": "String" } } - } + }, + "alarmCardVacationPausedBadge": "Paused for vacation" } diff --git a/lib/l10n/app_es.arb b/lib/l10n/app_es.arb index 0ff53ee..797266f 100644 --- a/lib/l10n/app_es.arb +++ b/lib/l10n/app_es.arb @@ -799,5 +799,6 @@ "type": "String" } } - } + }, + "alarmCardVacationPausedBadge": "Pausada por vacaciones" } diff --git a/lib/l10n/app_fr.arb b/lib/l10n/app_fr.arb index 95e56dd..a2283ae 100644 --- a/lib/l10n/app_fr.arb +++ b/lib/l10n/app_fr.arb @@ -840,5 +840,6 @@ "type": "String" } } - } + }, + "alarmCardVacationPausedBadge": "En pause pour les vacances" } diff --git a/lib/l10n/app_hi.arb b/lib/l10n/app_hi.arb index f770523..d48538f 100644 --- a/lib/l10n/app_hi.arb +++ b/lib/l10n/app_hi.arb @@ -840,5 +840,6 @@ "type": "String" } } - } + }, + "alarmCardVacationPausedBadge": "छुट्टी के कारण रोका गया" } diff --git a/lib/l10n/app_id.arb b/lib/l10n/app_id.arb index 54de15f..fc50158 100644 --- a/lib/l10n/app_id.arb +++ b/lib/l10n/app_id.arb @@ -840,5 +840,6 @@ "type": "String" } } - } + }, + "alarmCardVacationPausedBadge": "Dijeda karena liburan" } diff --git a/lib/l10n/app_it.arb b/lib/l10n/app_it.arb index b9196bd..a909edd 100644 --- a/lib/l10n/app_it.arb +++ b/lib/l10n/app_it.arb @@ -840,5 +840,6 @@ "type": "String" } } - } + }, + "alarmCardVacationPausedBadge": "In pausa per le vacanze" } diff --git a/lib/l10n/app_ja.arb b/lib/l10n/app_ja.arb index 5bab00d..4ff04d4 100644 --- a/lib/l10n/app_ja.arb +++ b/lib/l10n/app_ja.arb @@ -840,5 +840,6 @@ "type": "String" } } - } + }, + "alarmCardVacationPausedBadge": "休暇のため一時停止中" } diff --git a/lib/l10n/app_pt.arb b/lib/l10n/app_pt.arb index 11d474a..30f46db 100644 --- a/lib/l10n/app_pt.arb +++ b/lib/l10n/app_pt.arb @@ -840,5 +840,6 @@ "type": "String" } } - } + }, + "alarmCardVacationPausedBadge": "Pausada por férias" } diff --git a/lib/l10n/app_ru.arb b/lib/l10n/app_ru.arb index 701f3cb..139126b 100644 --- a/lib/l10n/app_ru.arb +++ b/lib/l10n/app_ru.arb @@ -840,5 +840,6 @@ "type": "String" } } - } + }, + "alarmCardVacationPausedBadge": "Приостановлено на время отпуска" } diff --git a/lib/l10n/app_zh.arb b/lib/l10n/app_zh.arb index 5fe58a1..8df840b 100644 --- a/lib/l10n/app_zh.arb +++ b/lib/l10n/app_zh.arb @@ -840,5 +840,6 @@ "type": "String" } } - } + }, + "alarmCardVacationPausedBadge": "因假期已暂停" } diff --git a/lib/l10n/gen/app_localizations.dart b/lib/l10n/gen/app_localizations.dart index fca451f..7dd7eb2 100644 --- a/lib/l10n/gen/app_localizations.dart +++ b/lib/l10n/gen/app_localizations.dart @@ -3067,6 +3067,12 @@ abstract class AppLocalizations { /// In es, this message translates to: /// **'Preset: {preset}'** String eqCustomActionPresetLabel(String preset); + + /// No description provided for @alarmCardVacationPausedBadge. + /// + /// In es, this message translates to: + /// **'Pausada por vacaciones'** + String get alarmCardVacationPausedBadge; } class _AppLocalizationsDelegate diff --git a/lib/l10n/gen/app_localizations_ar.dart b/lib/l10n/gen/app_localizations_ar.dart index 651c617..0b634e7 100644 --- a/lib/l10n/gen/app_localizations_ar.dart +++ b/lib/l10n/gen/app_localizations_ar.dart @@ -1687,4 +1687,7 @@ class AppLocalizationsAr extends AppLocalizations { String eqCustomActionPresetLabel(String preset) { return 'الإعداد المسبق: $preset'; } + + @override + String get alarmCardVacationPausedBadge => 'متوقفة مؤقتًا بسبب الإجازة'; } diff --git a/lib/l10n/gen/app_localizations_bn.dart b/lib/l10n/gen/app_localizations_bn.dart index 7474095..6a78fc9 100644 --- a/lib/l10n/gen/app_localizations_bn.dart +++ b/lib/l10n/gen/app_localizations_bn.dart @@ -1696,4 +1696,7 @@ class AppLocalizationsBn extends AppLocalizations { String eqCustomActionPresetLabel(String preset) { return 'প্রিসেট: $preset'; } + + @override + String get alarmCardVacationPausedBadge => 'ছুটির কারণে বিরত'; } diff --git a/lib/l10n/gen/app_localizations_de.dart b/lib/l10n/gen/app_localizations_de.dart index 295812a..65b7f97 100644 --- a/lib/l10n/gen/app_localizations_de.dart +++ b/lib/l10n/gen/app_localizations_de.dart @@ -1709,4 +1709,7 @@ class AppLocalizationsDe extends AppLocalizations { String eqCustomActionPresetLabel(String preset) { return 'Voreinstellung: $preset'; } + + @override + String get alarmCardVacationPausedBadge => 'Wegen Urlaub pausiert'; } diff --git a/lib/l10n/gen/app_localizations_en.dart b/lib/l10n/gen/app_localizations_en.dart index f02de76..5715fcd 100644 --- a/lib/l10n/gen/app_localizations_en.dart +++ b/lib/l10n/gen/app_localizations_en.dart @@ -1689,4 +1689,7 @@ class AppLocalizationsEn extends AppLocalizations { String eqCustomActionPresetLabel(String preset) { return 'Preset: $preset'; } + + @override + String get alarmCardVacationPausedBadge => 'Paused for vacation'; } diff --git a/lib/l10n/gen/app_localizations_es.dart b/lib/l10n/gen/app_localizations_es.dart index fbb6307..48a506b 100644 --- a/lib/l10n/gen/app_localizations_es.dart +++ b/lib/l10n/gen/app_localizations_es.dart @@ -1703,4 +1703,7 @@ class AppLocalizationsEs extends AppLocalizations { String eqCustomActionPresetLabel(String preset) { return 'Preset: $preset'; } + + @override + String get alarmCardVacationPausedBadge => 'Pausada por vacaciones'; } diff --git a/lib/l10n/gen/app_localizations_fr.dart b/lib/l10n/gen/app_localizations_fr.dart index f071a13..3982bb0 100644 --- a/lib/l10n/gen/app_localizations_fr.dart +++ b/lib/l10n/gen/app_localizations_fr.dart @@ -1712,4 +1712,7 @@ class AppLocalizationsFr extends AppLocalizations { String eqCustomActionPresetLabel(String preset) { return 'Préréglage : $preset'; } + + @override + String get alarmCardVacationPausedBadge => 'En pause pour les vacances'; } diff --git a/lib/l10n/gen/app_localizations_hi.dart b/lib/l10n/gen/app_localizations_hi.dart index 6f62753..0f580ab 100644 --- a/lib/l10n/gen/app_localizations_hi.dart +++ b/lib/l10n/gen/app_localizations_hi.dart @@ -1691,4 +1691,7 @@ class AppLocalizationsHi extends AppLocalizations { String eqCustomActionPresetLabel(String preset) { return 'प्रीसेट: $preset'; } + + @override + String get alarmCardVacationPausedBadge => 'छुट्टी के कारण रोका गया'; } diff --git a/lib/l10n/gen/app_localizations_id.dart b/lib/l10n/gen/app_localizations_id.dart index 2b62c6a..c32e472 100644 --- a/lib/l10n/gen/app_localizations_id.dart +++ b/lib/l10n/gen/app_localizations_id.dart @@ -1699,4 +1699,7 @@ class AppLocalizationsId extends AppLocalizations { String eqCustomActionPresetLabel(String preset) { return 'Prasetel: $preset'; } + + @override + String get alarmCardVacationPausedBadge => 'Dijeda karena liburan'; } diff --git a/lib/l10n/gen/app_localizations_it.dart b/lib/l10n/gen/app_localizations_it.dart index 68bc853..72ff0ef 100644 --- a/lib/l10n/gen/app_localizations_it.dart +++ b/lib/l10n/gen/app_localizations_it.dart @@ -1711,4 +1711,7 @@ class AppLocalizationsIt extends AppLocalizations { String eqCustomActionPresetLabel(String preset) { return 'Preset attivo: $preset'; } + + @override + String get alarmCardVacationPausedBadge => 'In pausa per le vacanze'; } diff --git a/lib/l10n/gen/app_localizations_ja.dart b/lib/l10n/gen/app_localizations_ja.dart index bec733e..b5cad51 100644 --- a/lib/l10n/gen/app_localizations_ja.dart +++ b/lib/l10n/gen/app_localizations_ja.dart @@ -1642,4 +1642,7 @@ class AppLocalizationsJa extends AppLocalizations { String eqCustomActionPresetLabel(String preset) { return 'プリセット: $preset'; } + + @override + String get alarmCardVacationPausedBadge => '休暇のため一時停止中'; } diff --git a/lib/l10n/gen/app_localizations_pt.dart b/lib/l10n/gen/app_localizations_pt.dart index fa10bc0..e53fd7d 100644 --- a/lib/l10n/gen/app_localizations_pt.dart +++ b/lib/l10n/gen/app_localizations_pt.dart @@ -1699,4 +1699,7 @@ class AppLocalizationsPt extends AppLocalizations { String eqCustomActionPresetLabel(String preset) { return 'Predefinição: $preset'; } + + @override + String get alarmCardVacationPausedBadge => 'Pausada por férias'; } diff --git a/lib/l10n/gen/app_localizations_ru.dart b/lib/l10n/gen/app_localizations_ru.dart index 7b6b837..480f9f6 100644 --- a/lib/l10n/gen/app_localizations_ru.dart +++ b/lib/l10n/gen/app_localizations_ru.dart @@ -1703,4 +1703,7 @@ class AppLocalizationsRu extends AppLocalizations { String eqCustomActionPresetLabel(String preset) { return 'Пресет: $preset'; } + + @override + String get alarmCardVacationPausedBadge => 'Приостановлено на время отпуска'; } diff --git a/lib/l10n/gen/app_localizations_zh.dart b/lib/l10n/gen/app_localizations_zh.dart index 6c97bc2..a2a226b 100644 --- a/lib/l10n/gen/app_localizations_zh.dart +++ b/lib/l10n/gen/app_localizations_zh.dart @@ -1634,4 +1634,7 @@ class AppLocalizationsZh extends AppLocalizations { String eqCustomActionPresetLabel(String preset) { return '预设:$preset'; } + + @override + String get alarmCardVacationPausedBadge => '因假期已暂停'; } diff --git a/lib/pantallas/pantalla_alarmas.dart b/lib/pantallas/pantalla_alarmas.dart index 593b518..cb3b60c 100644 --- a/lib/pantallas/pantalla_alarmas.dart +++ b/lib/pantallas/pantalla_alarmas.dart @@ -265,6 +265,26 @@ class _TarjetaAlarma extends StatelessWidget { ? l10n.noStationUseInternalSound : localizedStationName(l10n, alarma.emisora!.nombre); + // Item 5: surfaces the genuinely useful fields that already exist on + // the model, WITHOUT turning the row into clutter -- each is shown + // only when it is a meaningful deviation from the common case. + // Mirrors EXACTLY the pause predicate `impactoDeRango`/ + // `ServicioProgramacionAlarmas` already use + // (`!sonarEnVacaciones` while `activa`), gated by whether a vacation + // range is CURRENTLY active -- an alarm configured to pause but with + // no active range right now is not actually paused by anything yet. + final pausadaPorVacaciones = + alarma.activa && + !alarma.sonarEnVacaciones && + estado.rangoVacacionesActivo() != null; + final detalles = [ + if (alarma.fadeInSegundos > 0) + l10n.alarmFadeInLabel(alarma.fadeInSegundos), + if ((alarma.volumen * 100).round() != 85) + '${(alarma.volumen * 100).round()}%', + if (pausadaPorVacaciones) l10n.alarmCardVacationPausedBadge, + ]; + return Dismissible( key: ValueKey('tarjeta-alarma-${alarma.id}'), direction: DismissDirection.horizontal, @@ -309,14 +329,21 @@ class _TarjetaAlarma extends StatelessWidget { ), ), 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), + // Item 5: real day list can run longer than the + // old generic "Días" label -- Flexible+ellipsis + // keeps a long selection from overflowing the + // Row instead of clipping visibly. + Flexible( + child: Text( + _recurrenciaCorta(l10n, alarma), + overflow: TextOverflow.ellipsis, + style: TextStyle( + fontSize: 12, + fontWeight: FontWeight.w700, + color: Theme.of(context).colorScheme + .onSurface + .withValues(alpha: 0.5), + ), ), ), ], @@ -369,6 +396,25 @@ class _TarjetaAlarma extends StatelessWidget { ), ], ), + // Item 5: fade/volume/vacation-pause state, only + // when each is a genuinely useful deviation from + // the common case (see `detalles` above) -- a + // single compact line, not a badge per field. + if (detalles.isNotEmpty) ...[ + const SizedBox(height: 3), + Text( + detalles.join(' · '), + key: ValueKey('tarjeta-alarma-detalles-${alarma.id}'), + overflow: TextOverflow.ellipsis, + style: TextStyle( + fontSize: 11, + fontWeight: FontWeight.w600, + color: Theme.of( + context, + ).colorScheme.onSurface.withValues(alpha: 0.55), + ), + ), + ], ], ), ), @@ -1668,16 +1714,39 @@ String _weekdayShort(AppLocalizations l10n, int day) => switch (day) { 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. +/// Audit 7.4 (t4:339) / item 5: a compact recurrence label next to the +/// alarm card's giant time. `diaria`/`unica` still show the SAME generic +/// labels the editor's own `TipoProgramacionAlarma` `SegmentedButton` +/// already uses (`dailyOption`/`oneTimeOption`) -- both are already fully +/// specific (there is nothing more concrete to say than "every day"/"just +/// once"). `diasSemana` now renders the alarm's ACTUAL configured days +/// (e.g. "Lun, Mié, Vie") instead of the generic `weekdaysOption` ("Días"), +/// reusing [_weekdayShort] (the SAME per-day abbreviation the editor's own +/// day-picker circles already use) -- no new ARB keys, no second +/// formatting scheme, and the resulting Text is wrapped in a +/// `Flexible`+ellipsis at the call site so a long selection never +/// overflows the row. String _recurrenciaCorta(AppLocalizations l10n, AlarmaMusical alarma) { return switch (alarma.tipoProgramacion) { TipoProgramacionAlarma.diaria => l10n.dailyOption, - TipoProgramacionAlarma.diasSemana => l10n.weekdaysOption, + TipoProgramacionAlarma.diasSemana => _diasSemanaCorto( + l10n, + alarma.diasSemana, + ), TipoProgramacionAlarma.unica => l10n.oneTimeOption, }; } + +/// The real, ordered day abbreviations for a `diasSemana` alarm (item 5), +/// e.g. "Lun, Mié, Vie". [diasSemana] is re-sorted defensively (the editor +/// always persists it sorted, but this does not rely on that). Falls back +/// to the generic [AppLocalizations.weekdaysOption] label when +/// [diasSemana] is empty -- the editor already blocks saving an empty +/// selection in this mode, but a corrupt/legacy persisted record could +/// still reach here, and showing nothing would be worse than the old +/// generic label. +String _diasSemanaCorto(AppLocalizations l10n, List diasSemana) { + if (diasSemana.isEmpty) return l10n.weekdaysOption; + final ordenados = [...diasSemana]..sort(); + return ordenados.map((dia) => _weekdayShort(l10n, dia)).join(', '); +} diff --git a/test/pantallas/pantalla_alarmas_editor_test.dart b/test/pantallas/pantalla_alarmas_editor_test.dart index fbfefd7..1879afa 100644 --- a/test/pantallas/pantalla_alarmas_editor_test.dart +++ b/test/pantallas/pantalla_alarmas_editor_test.dart @@ -190,9 +190,26 @@ void main() { expect(antes, isNot(l10n.alarmNoNextExecution)); // Lunes -> Martes: la fecha calculada SIEMPRE cambia, sea cual sea hoy. - await tester.tap(find.text(l10n.weekdayShortTuesday)); + // + // Item 5: the alarm CARD underneath now also renders the real day + // abbreviation ("Lun") for a diasSemana alarm, so a bare + // `find.text(...)` for a weekday letter is ambiguous while the + // editor sheet is open on top of the list — scope to the sheet's own + // BottomSheet subtree to target the day-picker circle specifically. + final hojaEditor = find.byType(BottomSheet); + await tester.tap( + find.descendant( + of: hojaEditor, + matching: find.text(l10n.weekdayShortTuesday), + ), + ); await tester.pumpAndSettle(); - await tester.tap(find.text(l10n.weekdayShortMonday)); + await tester.tap( + find.descendant( + of: hojaEditor, + matching: find.text(l10n.weekdayShortMonday), + ), + ); await tester.pumpAndSettle(); final despues = _textoPreview(tester); diff --git a/test/pantallas/pantalla_alarmas_recurrencia_test.dart b/test/pantallas/pantalla_alarmas_recurrencia_test.dart new file mode 100644 index 0000000..171072d --- /dev/null +++ b/test/pantallas/pantalla_alarmas_recurrencia_test.dart @@ -0,0 +1,330 @@ +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/pantallas/pantalla_alarmas.dart'; +import 'package:pluriwave/servicios/servicio_alarmas.dart'; +import 'package:provider/provider.dart'; +import 'package:shared_preferences/shared_preferences.dart'; + +import '../helpers/fakes.dart'; +import '../helpers/fakes_alarmas.dart'; + +/// Item 5: the alarm list must show which days a `diasSemana` alarm +/// actually fires on (e.g. "Lun, Mié, Vie"), not the generic "Días" label, +/// plus surface fade/volume/vacation-pause state when they are genuinely +/// informative -- without cluttering the row. +void main() { + setUp(() { + SharedPreferences.setMockInitialValues({}); + }); + + Future<(EstadoRadio, EstadoAlarmas)> montar( + WidgetTester tester, { + required AlarmaMusical alarma, + List vacaciones = const [], + }) async { + tester.view.physicalSize = const Size(1440, 3200); + tester.view.devicePixelRatio = 1.0; + addTearDown(tester.view.resetPhysicalSize); + addTearDown(tester.view.resetDevicePixelRatio); + + final radio = EstadoRadio( + audio: FakeServicioAudio(), + 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, 6, 0)), + android: android, + iniciarAutomaticamente: false, + ); + addTearDown(estadoAlarmas.dispose); + addTearDown(android.dispose); + + await estadoAlarmas.guardarAlarma(alarma); + if (vacaciones.isNotEmpty) { + await estadoAlarmas.guardarVacaciones(vacaciones); + } + + await tester.pumpWidget( + MultiProvider( + providers: [ + ChangeNotifierProvider.value(value: radio), + ChangeNotifierProvider.value(value: estadoAlarmas), + ], + child: MaterialApp( + locale: const Locale('es'), + localizationsDelegates: AppLocalizations.localizationsDelegates, + supportedLocales: AppLocalizations.supportedLocales, + home: const Scaffold(body: PantallaAlarmas()), + ), + ), + ); + await tester.pump(); + await tester.pump(const Duration(milliseconds: 100)); + + return (radio, estadoAlarmas); + } + + testWidgets( + 'diasSemana alarm shows the ACTUAL configured days (Lun, Mié, Vie), ' + 'not the generic "Días" label', + (tester) async { + await montar( + tester, + alarma: const AlarmaMusical( + id: 'a-dias', + nombre: 'Entre semana', + hora: 7, + minuto: 0, + tipoProgramacion: TipoProgramacionAlarma.diasSemana, + diasSemana: [DateTime.monday, DateTime.wednesday, DateTime.friday], + ), + ); + + expect(find.text('Lun, Mié, Vie'), findsOneWidget); + expect(find.text('Días'), findsNothing); + }, + ); + + testWidgets('daily alarm still shows "Diaria" (unaffected)', ( + tester, + ) async { + await montar( + tester, + alarma: const AlarmaMusical( + id: 'a-diaria', + nombre: 'Todos los días', + hora: 7, + minuto: 0, + tipoProgramacion: TipoProgramacionAlarma.diaria, + diasSemana: [], + ), + ); + + expect(find.text('Diaria'), findsOneWidget); + }); + + testWidgets('one-time alarm still shows "Una vez" (unaffected)', ( + tester, + ) async { + await montar( + tester, + alarma: const AlarmaMusical( + id: 'a-unica', + nombre: 'Una sola vez', + hora: 7, + minuto: 0, + tipoProgramacion: TipoProgramacionAlarma.unica, + diasSemana: [], + fechaUnica: null, + ), + ); + + expect(find.text('Una vez'), findsOneWidget); + }); + + testWidgets( + 'a diasSemana alarm with an (invalid/legacy) empty diasSemana falls ' + 'back to the generic label instead of showing nothing', + (tester) async { + await montar( + tester, + alarma: const AlarmaMusical( + id: 'a-dias-vacio', + nombre: 'Corrupta', + hora: 7, + minuto: 0, + tipoProgramacion: TipoProgramacionAlarma.diasSemana, + diasSemana: [], + ), + ); + + expect(find.text('Días'), findsOneWidget); + }, + ); + + testWidgets('a configured fade-in shows a compact "Fade-in Ns" detail', ( + tester, + ) async { + await montar( + tester, + alarma: const AlarmaMusical( + id: 'a-fade', + nombre: 'Con fade', + hora: 7, + minuto: 0, + tipoProgramacion: TipoProgramacionAlarma.diaria, + diasSemana: [], + fadeInSegundos: 8, + ), + ); + + expect(find.textContaining('Fade-in 8s'), findsOneWidget); + }); + + testWidgets('no fade-in (0s, the default) shows no fade detail', ( + tester, + ) async { + await montar( + tester, + alarma: const AlarmaMusical( + id: 'a-sin-fade', + nombre: 'Sin fade', + hora: 7, + minuto: 0, + tipoProgramacion: TipoProgramacionAlarma.diaria, + diasSemana: [], + fadeInSegundos: 0, + ), + ); + + expect(find.textContaining('Fade-in'), findsNothing); + }); + + testWidgets( + 'a non-default volume shows a compact percentage detail', + (tester) async { + await montar( + tester, + alarma: const AlarmaMusical( + id: 'a-vol', + nombre: 'Volumen bajo', + hora: 7, + minuto: 0, + tipoProgramacion: TipoProgramacionAlarma.diaria, + diasSemana: [], + volumen: 0.5, + ), + ); + + expect(find.textContaining('50%'), findsOneWidget); + }, + ); + + testWidgets('the default volume (85%) shows no volume detail', ( + tester, + ) async { + await montar( + tester, + alarma: const AlarmaMusical( + id: 'a-vol-default', + nombre: 'Volumen default', + hora: 7, + minuto: 0, + tipoProgramacion: TipoProgramacionAlarma.diaria, + diasSemana: [], + ), + ); + + expect(find.textContaining('85%'), findsNothing); + }); + + testWidgets( + 'an alarm paused by a CURRENTLY active vacation range shows a ' + 'vacation-paused detail', + (tester) async { + final l10n = lookupAppLocalizations(const Locale('es')); + await montar( + tester, + alarma: const AlarmaMusical( + id: 'a-vacaciones', + nombre: 'Pausada', + hora: 7, + minuto: 0, + tipoProgramacion: TipoProgramacionAlarma.diaria, + diasSemana: [], + sonarEnVacaciones: false, + ), + vacaciones: [ + // Wide, real-wall-clock-safe range (rangoVacacionesActivo() + // defaults to the REAL DateTime.now(), not this file's injected + // `reloj`) -- deliberately spans many years so the test stays + // valid regardless of exactly when it runs. + RangoVacaciones( + id: 'v1', + nombre: 'Verano', + inicio: DateTime(2020, 1, 1), + fin: DateTime(2030, 12, 31), + ), + ], + ); + + expect( + find.textContaining(l10n.alarmCardVacationPausedBadge), + findsOneWidget, + ); + }, + ); + + testWidgets( + 'an alarm that DOES sound during vacations shows NO vacation-paused ' + 'detail even with an active range', + (tester) async { + final l10n = lookupAppLocalizations(const Locale('es')); + await montar( + tester, + alarma: const AlarmaMusical( + id: 'a-suena-vacaciones', + nombre: 'Suena igual', + hora: 7, + minuto: 0, + tipoProgramacion: TipoProgramacionAlarma.diaria, + diasSemana: [], + sonarEnVacaciones: true, + ), + vacaciones: [ + // Wide, real-wall-clock-safe range (rangoVacacionesActivo() + // defaults to the REAL DateTime.now(), not this file's injected + // `reloj`) -- deliberately spans many years so the test stays + // valid regardless of exactly when it runs. + RangoVacaciones( + id: 'v1', + nombre: 'Verano', + inicio: DateTime(2020, 1, 1), + fin: DateTime(2030, 12, 31), + ), + ], + ); + + expect( + find.textContaining(l10n.alarmCardVacationPausedBadge), + findsNothing, + ); + }, + ); + + testWidgets( + 'sonarEnVacaciones:false with NO currently-active vacation range shows ' + 'no vacation-paused detail (nothing to be paused BY right now)', + (tester) async { + final l10n = lookupAppLocalizations(const Locale('es')); + await montar( + tester, + alarma: const AlarmaMusical( + id: 'a-sin-rango-activo', + nombre: 'Sin vacaciones activas', + hora: 7, + minuto: 0, + tipoProgramacion: TipoProgramacionAlarma.diaria, + diasSemana: [], + sonarEnVacaciones: false, + ), + ); + + expect( + find.textContaining(l10n.alarmCardVacationPausedBadge), + findsNothing, + ); + }, + ); +}