fix(vacaciones): edit and delete vacation ranges

Vacaciones ranges could be created but never edited or removed --
EstadoAlarmas already had crearRangoVacaciones/eliminarRangoVacaciones
with no UI affordance reaching them, and no update path at all.

Add EstadoAlarmas.editarRangoVacaciones and wire tap-to-edit /
swipe-to-delete (with confirmation) onto every range card, mirroring
the alarm list's own Dismissible + confirm-dialog pattern exactly. This
covers the active-range hero too: a freshly created range is active
immediately and only ever renders there, never in the
scheduled/past lists, so it needed the same affordances or a user's
very first range could never be fixed.
This commit is contained in:
2026-07-30 19:05:28 +02:00
parent d1a911e587
commit c7e1a212ca
30 changed files with 639 additions and 111 deletions
@@ -445,4 +445,173 @@ void main() {
},
);
});
group('issue 1 (feedback-pruebas): editar y eliminar rangos', () {
testWidgets(
'tocar la tarjeta de un rango programado abre el editor precargado '
'con su nombre y fechas',
(tester) async {
final estado = await _crearEstado(
vacaciones: [
RangoVacaciones(
id: 'f2',
nombre: 'Verano',
inicio: _hoyDia.add(const Duration(days: 20)),
fin: _hoyDia.add(const Duration(days: 25)),
),
],
);
addTearDown(estado.dispose);
await tester.pumpWidget(_buildScreen(estado));
await _pumpEstable(tester);
final l10n = AppLocalizations.of(
tester.element(find.byType(PantallaVacaciones)),
);
await tester.tap(find.byKey(const ValueKey('vacaciones-tarjeta-f2')));
await _pumpEstable(tester);
expect(find.text(l10n.editVacationRangeTitle), findsOneWidget);
// Scoped to the TextField specifically -- the original card's OWN
// "Verano" label is still (offstage, behind the modal) in the tree,
// so a bare `find.text('Verano')` would ambiguously match both.
expect(find.widgetWithText(TextField, 'Verano'), findsOneWidget);
},
);
testWidgets(
'guardar el editor abierto por tap actualiza el rango existente (no '
'crea uno nuevo)',
(tester) async {
final estado = await _crearEstado(
vacaciones: [
RangoVacaciones(
id: 'f2',
nombre: 'Verano',
inicio: _hoyDia.add(const Duration(days: 20)),
fin: _hoyDia.add(const Duration(days: 25)),
),
],
);
addTearDown(estado.dispose);
await tester.pumpWidget(_buildScreen(estado));
await _pumpEstable(tester);
final l10n = AppLocalizations.of(
tester.element(find.byType(PantallaVacaciones)),
);
await tester.tap(find.byKey(const ValueKey('vacaciones-tarjeta-f2')));
await _pumpEstable(tester);
await tester.enterText(find.byType(TextField), 'Verano renombrado');
final boton = tester.widget<FilledButton>(
find.widgetWithText(FilledButton, l10n.saveRangeAction),
);
boton.onPressed!();
await _pumpEstable(tester);
expect(estado.vacaciones, hasLength(1));
expect(estado.vacaciones.single.id, 'f2');
expect(estado.vacaciones.single.nombre, 'Verano renombrado');
},
);
testWidgets(
'deslizar la tarjeta de un rango pide confirmacion; cancelar la '
'conserva y confirmar la elimina',
(tester) async {
final estado = await _crearEstado(
vacaciones: [
RangoVacaciones(
id: 'f2',
nombre: 'Verano',
inicio: _hoyDia.add(const Duration(days: 20)),
fin: _hoyDia.add(const Duration(days: 25)),
),
],
);
addTearDown(estado.dispose);
await tester.pumpWidget(_buildScreen(estado));
await _pumpEstable(tester);
final l10n = AppLocalizations.of(
tester.element(find.byType(PantallaVacaciones)),
);
// Cancelar: el rango se conserva.
await tester.drag(
find.byKey(const ValueKey('vacaciones-tarjeta-f2')),
const Offset(-600, 0),
);
await _pumpEstable(tester);
expect(find.text(l10n.vacationDeleteConfirmTitle), findsOneWidget);
await tester.tap(find.text(l10n.cancelAction));
await _pumpEstable(tester);
expect(estado.vacaciones, hasLength(1));
// Confirmar: el rango se elimina.
await tester.drag(
find.byKey(const ValueKey('vacaciones-tarjeta-f2')),
const Offset(-600, 0),
);
await _pumpEstable(tester);
expect(find.text(l10n.vacationDeleteConfirmTitle), findsOneWidget);
await tester.tap(find.text(l10n.deleteAction));
await _pumpEstable(tester);
expect(estado.vacaciones, isEmpty);
},
);
testWidgets(
'el rango ACTIVO (mostrado en el hero) tambien se puede editar (tap) '
'y eliminar (swipe) -- un rango recien creado siempre esta activo y '
'nunca aparece en las listas programado/pasado',
(tester) async {
final estado = await _crearEstado(
vacaciones: [
RangoVacaciones(
id: 'v1',
nombre: 'Julio activo',
inicio: _hoyDia.subtract(const Duration(days: 3)),
fin: _hoyDia.add(const Duration(days: 5)),
),
],
);
addTearDown(estado.dispose);
await tester.pumpWidget(_buildScreen(estado));
await _pumpEstable(tester);
final l10n = AppLocalizations.of(
tester.element(find.byType(PantallaVacaciones)),
);
await tester.tap(find.byKey(const ValueKey('vacaciones-tarjeta-v1')));
await _pumpEstable(tester);
expect(find.text(l10n.editVacationRangeTitle), findsOneWidget);
// Scoped to the TextField specifically -- the hero's OWN "Julio
// activo" label is still (offstage, behind the modal) in the tree.
expect(find.widgetWithText(TextField, 'Julio activo'), findsOneWidget);
// Dismiss the editor sheet (no explicit close button -- same as the
// pre-existing "Anadir rango" sheet, dismissible via the standard
// modal-bottom-sheet Navigator.pop) before interacting with the
// list underneath it.
Navigator.of(tester.element(find.byType(PantallaVacaciones))).pop();
await _pumpEstable(tester);
await tester.drag(
find.byKey(const ValueKey('vacaciones-tarjeta-v1')),
const Offset(-600, 0),
);
await _pumpEstable(tester);
await tester.tap(find.text(l10n.deleteAction));
await _pumpEstable(tester);
expect(estado.vacaciones, isEmpty);
expect(find.text(l10n.vacationNoActiveRangeHint), findsOneWidget);
},
);
});
}