feat(alarmas): simplify alarm cards and add vacation summary row
Restyle the Alarmas root per the functional redesign: alarm cards drop
the always-visible edit/skip/delete button row for a minimal giant
time + station + switch layout. Tap opens the editor, swipe deletes
(with an AlertDialog confirmation), and the hero banner gains an
inline "Saltar" pill for the featured (soonest-firing) alarm's skip
action. No capability from the old button row is lost, only the
trigger location moved; estado_alarmas.dart and its scheduling/
snooze/dismiss-guard tests are untouched.
The vacation inline panel becomes a tappable summary row (range count
+ next-range countdown, computed over the existing estado.vacaciones)
that pushes a Vacaciones manager screen. That destination is a
placeholder for now (_PantallaVacacionesTemporal, holding the old
panel's body verbatim so add/delete-range capability is preserved) --
WU9 replaces it with the real PantallaVacaciones per design ADR-6.
New ARB keys (en/es only; other 11 locales are WU18's job):
alarmHeroSkipAction, alarmDeleteConfirmTitle/Message,
vacationRangesCount, vacationSummary{Active,Upcoming}Countdown.
This commit is contained in:
@@ -6,6 +6,7 @@ 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:pluriwave/widgets/pluri_push_scaffold.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
|
||||
@@ -18,6 +19,80 @@ class _Entorno {
|
||||
final EstadoAlarmas estadoAlarmas;
|
||||
}
|
||||
|
||||
class _EntornoDosAlarmas {
|
||||
_EntornoDosAlarmas({required this.estadoAlarmas, required this.android});
|
||||
|
||||
final EstadoAlarmas estadoAlarmas;
|
||||
final FakePuertoAlarmasAndroid android;
|
||||
}
|
||||
|
||||
/// WU8 fixture: two DAILY alarms ('r1' 07:00, 'r2' 08:00) under a FIXED clock
|
||||
/// (06:00, before both) so 'r1' is deterministically the featured/soonest
|
||||
/// alarm regardless of the wall-clock time the suite happens to run at.
|
||||
Future<_EntornoDosAlarmas> _montarDosAlarmas(WidgetTester tester) 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, 1, 6, 0)),
|
||||
android: android,
|
||||
iniciarAutomaticamente: false,
|
||||
);
|
||||
addTearDown(estadoAlarmas.dispose);
|
||||
addTearDown(android.dispose);
|
||||
await estadoAlarmas.guardarAlarma(
|
||||
const AlarmaMusical(
|
||||
id: 'r1',
|
||||
nombre: 'Primera',
|
||||
hora: 7,
|
||||
minuto: 0,
|
||||
tipoProgramacion: TipoProgramacionAlarma.diaria,
|
||||
diasSemana: [],
|
||||
),
|
||||
);
|
||||
await estadoAlarmas.guardarAlarma(
|
||||
const AlarmaMusical(
|
||||
id: 'r2',
|
||||
nombre: 'Segunda',
|
||||
hora: 8,
|
||||
minuto: 0,
|
||||
tipoProgramacion: TipoProgramacionAlarma.diaria,
|
||||
diasSemana: [],
|
||||
),
|
||||
);
|
||||
|
||||
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,
|
||||
home: const Scaffold(body: PantallaAlarmas()),
|
||||
),
|
||||
),
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
return _EntornoDosAlarmas(estadoAlarmas: estadoAlarmas, android: android);
|
||||
}
|
||||
|
||||
Future<_Entorno> _abrirEditor(WidgetTester tester) async {
|
||||
tester.view.physicalSize = const Size(1440, 3200);
|
||||
tester.view.devicePixelRatio = 1.0;
|
||||
@@ -73,10 +148,12 @@ Future<_Entorno> _abrirEditor(WidgetTester tester) async {
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
final l10n = lookupAppLocalizations(const Locale('es'));
|
||||
await tester.ensureVisible(find.text(l10n.editAction).first);
|
||||
// WU8: the always-visible "Edit" button is gone — tapping the simplified
|
||||
// card itself now opens the editor.
|
||||
final tarjeta = find.byKey(const ValueKey('tarjeta-alarma-ed1'));
|
||||
await tester.ensureVisible(tarjeta);
|
||||
await tester.pumpAndSettle();
|
||||
await tester.tap(find.text(l10n.editAction).first);
|
||||
await tester.tap(tarjeta);
|
||||
await tester.pumpAndSettle();
|
||||
return _Entorno(estadoAlarmas: estadoAlarmas);
|
||||
}
|
||||
@@ -207,4 +284,110 @@ void main() {
|
||||
final volumen = sliders.firstWhere((slider) => slider.max == 1.0);
|
||||
expect(volumen.min, 0.0);
|
||||
});
|
||||
|
||||
group('WU8 — tarjeta de alarma simplificada', () {
|
||||
testWidgets(
|
||||
'tocar la tarjeta abre el editor precargado con los datos de esa '
|
||||
'alarma',
|
||||
(tester) async {
|
||||
await _montarDosAlarmas(tester);
|
||||
|
||||
await tester.tap(find.byKey(const ValueKey('tarjeta-alarma-r2')));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(find.text(l10n.editAlarmTitle), findsOneWidget);
|
||||
expect(find.text('Segunda'), findsOneWidget);
|
||||
},
|
||||
);
|
||||
|
||||
testWidgets('deslizar la tarjeta pide confirmacion; cancelar la conserva y '
|
||||
'confirmar la elimina', (tester) async {
|
||||
final entorno = await _montarDosAlarmas(tester);
|
||||
|
||||
// Cancelar: la tarjeta se conserva.
|
||||
await tester.drag(
|
||||
find.byKey(const ValueKey('tarjeta-alarma-r2')),
|
||||
const Offset(-600, 0),
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
expect(find.text(l10n.alarmDeleteConfirmTitle), findsOneWidget);
|
||||
await tester.tap(find.text(l10n.cancelAction));
|
||||
await tester.pumpAndSettle();
|
||||
expect(entorno.estadoAlarmas.alarmas.length, 2);
|
||||
expect(find.byKey(const ValueKey('tarjeta-alarma-r2')), findsOneWidget);
|
||||
|
||||
// Confirmar: la tarjeta se elimina.
|
||||
await tester.drag(
|
||||
find.byKey(const ValueKey('tarjeta-alarma-r2')),
|
||||
const Offset(-600, 0),
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
expect(find.text(l10n.alarmDeleteConfirmTitle), findsOneWidget);
|
||||
await tester.tap(find.text(l10n.deleteAction));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(entorno.estadoAlarmas.alarmas.length, 1);
|
||||
expect(find.byKey(const ValueKey('tarjeta-alarma-r2')), findsNothing);
|
||||
});
|
||||
|
||||
testWidgets(
|
||||
'el banner destacado permite saltar la alarma mas proxima (accion '
|
||||
'"Saltar")',
|
||||
(tester) async {
|
||||
final entorno = await _montarDosAlarmas(tester);
|
||||
final proximaAntes = entorno.estadoAlarmas.proximaAlarma;
|
||||
expect(proximaAntes?.id, 'r1');
|
||||
|
||||
await tester.tap(find.byKey(const ValueKey('hero-skip-next')));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(find.byType(SnackBar), findsOneWidget);
|
||||
final r1 = entorno.estadoAlarmas.alarmas.firstWhere(
|
||||
(a) => a.id == 'r1',
|
||||
);
|
||||
expect(r1.proximaEjecucion, isNot(proximaAntes!.proximaEjecucion));
|
||||
},
|
||||
);
|
||||
|
||||
testWidgets('la fila resumen de vacaciones empuja una pantalla al tocarla '
|
||||
'(destino provisorio; WU9 lo reemplaza)', (tester) async {
|
||||
await _montarDosAlarmas(tester);
|
||||
|
||||
await tester.tap(find.byKey(const ValueKey('vacaciones-resumen')));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(find.byType(PluriPushScaffold), findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets(
|
||||
'guardia de capacidad: editar (tap), saltar (hero) y eliminar (swipe) '
|
||||
'siguen siendo alcanzables tras la simplificacion de la tarjeta',
|
||||
(tester) async {
|
||||
final entorno = await _montarDosAlarmas(tester);
|
||||
|
||||
// 1) Saltar, desde el banner destacado.
|
||||
await tester.tap(find.byKey(const ValueKey('hero-skip-next')));
|
||||
await tester.pumpAndSettle();
|
||||
expect(find.byType(SnackBar), findsOneWidget);
|
||||
|
||||
// 2) Editar, tocando la tarjeta.
|
||||
await tester.tap(find.byKey(const ValueKey('tarjeta-alarma-r2')));
|
||||
await tester.pumpAndSettle();
|
||||
expect(find.text(l10n.editAlarmTitle), findsOneWidget);
|
||||
await tester.tap(find.byIcon(Icons.close_rounded));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
// 3) Eliminar, deslizando y confirmando.
|
||||
await tester.drag(
|
||||
find.byKey(const ValueKey('tarjeta-alarma-r2')),
|
||||
const Offset(-600, 0),
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
await tester.tap(find.text(l10n.deleteAction));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(entorno.estadoAlarmas.alarmas.length, 1);
|
||||
},
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user