Files
pluriwave/test/pantallas/pantalla_vacaciones_test.dart
FreeTLab d81fabbe27 refactor(iap): make esPremium a required constructor parameter
EstadoAlarmas, EstadoGrabacion and EstadoRadio defaulted `esPremium` to
`() => true`, so any construction site that forgot to wire entitlement
compiled fine and silently ran ungated — failing OPEN to premium and
disabling the paywall with no test able to catch it.

The parameter is now required with no default. Production wiring in
app.dart was already correct and is unchanged; the 184 pre-existing test
call sites now pass `() => true` explicitly, which is exactly the old
implicit default, so every assertion is untouched.

EstadoRadio has no gate of its own but constructs EstadoGrabacion, so it
inherits the same contract.

The one test that existed to pin the old default is renamed to describe
what it still covers (the premium path through iniciar() with no
duracion); its assertions are unchanged.
2026-08-10 22:06:36 +02:00

759 lines
25 KiB
Dart

import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:pluriwave/estado/estado_alarmas.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/pantallas/pantalla_vacaciones.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';
import '../helpers/fakes_alarmas.dart';
/// WU9, `alarm-vacation-ranges` spec — the real Vacaciones manager screen
/// that WU8's summary row pushes to (replacing WU8's own temporary
/// placeholder). Covers: active-range hero (name + days-remaining countdown
/// + per-alarm impact line), the "PROGRAMADOS" upcoming list, the "Añadir
/// rango" CTA, and the "Rangos pasados" history section.
///
/// Clock note: `PantallaVacaciones` calls `EstadoAlarmas.rangoVacacionesActivo()`
/// etc. with NO `ahora` argument (production code always uses real
/// `DateTime.now()` — design ADR-6 explicitly rejects a testable Clock
/// abstraction as out of scope for this redesign). So fixtures here are
/// built RELATIVE to `DateTime.now()`, never as hardcoded calendar dates —
/// the `ServicioAlarmas`'s own `reloj` (used only for alarm SCHEDULING
/// maths, irrelevant to the vacation-query assertions below) is left at its
/// real-clock default too, for consistency.
DateTime get _hoy => DateTime.now();
DateTime get _hoyDia => DateTime(_hoy.year, _hoy.month, _hoy.day);
Future<EstadoAlarmas> _crearEstado({
List<AlarmaMusical> alarmas = const [],
List<RangoVacaciones> vacaciones = const [],
}) async {
final android = FakePuertoAlarmasAndroid();
final estado = EstadoAlarmas(
esPremium: () => true,
servicio: ServicioAlarmas(),
android: android,
iniciarAutomaticamente: false,
);
for (final alarma in alarmas) {
await estado.guardarAlarma(alarma);
}
if (vacaciones.isNotEmpty) {
await estado.guardarVacaciones(vacaciones);
}
return estado;
}
Widget _buildScreen(EstadoAlarmas estado) {
return ChangeNotifierProvider<EstadoAlarmas>.value(
value: estado,
child: MaterialApp(
locale: const Locale('es'),
localizationsDelegates: AppLocalizations.localizationsDelegates,
supportedLocales: AppLocalizations.supportedLocales,
home: const PantallaVacaciones(),
),
);
}
Future<void> _pumpEstable(WidgetTester tester) async {
await tester.pump();
await tester.pumpAndSettle();
}
void main() {
setUp(() {
SharedPreferences.setMockInitialValues({});
});
testWidgets(
'renders inside a PluriPushScaffold; con un rango activo muestra su '
'nombre, los dias restantes, y la linea de impacto por alarma',
(tester) async {
final estado = await _crearEstado(
alarmas: [
const AlarmaMusical(
id: 'p1',
nombre: 'Pausada 1',
hora: 7,
minuto: 30,
tipoProgramacion: TipoProgramacionAlarma.diaria,
diasSemana: [],
sonarEnVacaciones: false,
),
const AlarmaMusical(
id: 'p2',
nombre: 'Pausada 2',
hora: 13,
minuto: 45,
tipoProgramacion: TipoProgramacionAlarma.diaria,
diasSemana: [],
sonarEnVacaciones: false,
),
const AlarmaMusical(
id: 'n1',
nombre: 'Sigue',
hora: 8,
minuto: 15,
tipoProgramacion: TipoProgramacionAlarma.diaria,
diasSemana: [],
),
],
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);
expect(find.byType(PluriPushScaffold), findsOneWidget);
final l10n = AppLocalizations.of(
tester.element(find.byType(PantallaVacaciones)),
);
expect(find.text('Julio activo'), findsOneWidget);
expect(find.text(l10n.vacationSummaryActiveCountdown(5)), findsOneWidget);
expect(
find.text(l10n.vacationImpactPausedLabel('07:30, 13:45')),
findsOneWidget,
);
expect(
find.text(l10n.vacationImpactContinuesLabel('08:15')),
findsOneWidget,
);
},
);
testWidgets('sin rango activo, muestra la pista en su lugar', (tester) async {
final estado = await _crearEstado();
addTearDown(estado.dispose);
await tester.pumpWidget(_buildScreen(estado));
await _pumpEstable(tester);
final l10n = AppLocalizations.of(
tester.element(find.byType(PantallaVacaciones)),
);
expect(find.text(l10n.vacationNoActiveRangeHint), findsOneWidget);
});
testWidgets('los rangos futuros aparecen bajo el encabezado PROGRAMADOS', (
tester,
) async {
final estado = await _crearEstado(
vacaciones: [
RangoVacaciones(
id: 'f1',
nombre: 'Rango lejano',
inicio: _hoyDia.add(const Duration(days: 120)),
fin: _hoyDia.add(const Duration(days: 125)),
),
RangoVacaciones(
id: 'f2',
nombre: 'Rango cercano',
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)),
);
expect(find.text(l10n.vacationUpcomingSectionTitle), findsOneWidget);
expect(find.text('Rango lejano'), findsOneWidget);
expect(find.text('Rango cercano'), findsOneWidget);
});
testWidgets(
'los rangos pasados aparecen bajo el encabezado "Rangos pasados" tras '
'expandir la fila (audit 9b.7: colapsada por defecto, t4:489)',
(tester) async {
final estado = await _crearEstado(
vacaciones: [
RangoVacaciones(
id: 'pa1',
nombre: 'Rango viejo',
inicio: _hoyDia.subtract(const Duration(days: 30)),
fin: _hoyDia.subtract(const Duration(days: 20)),
),
],
);
addTearDown(estado.dispose);
await tester.pumpWidget(_buildScreen(estado));
await _pumpEstable(tester);
final l10n = AppLocalizations.of(
tester.element(find.byType(PantallaVacaciones)),
);
expect(find.text(l10n.vacationPastSectionTitle), findsOneWidget);
await tester.tap(find.text(l10n.vacationPastSectionTitle));
await _pumpEstable(tester);
expect(find.text('Rango viejo'), findsOneWidget);
},
);
testWidgets('el CTA "Añadir rango" abre el formulario de alta existente', (
tester,
) async {
final estado = await _crearEstado();
addTearDown(estado.dispose);
await tester.pumpWidget(_buildScreen(estado));
await _pumpEstable(tester);
final l10n = AppLocalizations.of(
tester.element(find.byType(PantallaVacaciones)),
);
await tester.tap(find.text(l10n.addVacationRangeCta));
await _pumpEstable(tester);
expect(find.text(l10n.newVacationRangeTitle), findsOneWidget);
});
// Item 21 / audit 9b.4-9b.5 (t4:451-462, 469-479): the screen's signature
// element is a start/end date pair, never a determinate progress bar.
testWidgets(
'el rango activo muestra el par de fechas, no una barra de progreso',
(tester) async {
final inicio = _hoyDia.subtract(const Duration(days: 3));
final fin = _hoyDia.add(const Duration(days: 5));
final estado = await _crearEstado(
vacaciones: [
RangoVacaciones(
id: 'v1',
nombre: 'Julio activo',
inicio: inicio,
fin: fin,
),
],
);
addTearDown(estado.dispose);
await tester.pumpWidget(_buildScreen(estado));
await _pumpEstable(tester);
expect(
find.byType(LinearProgressIndicator),
findsNothing,
reason:
't4:451-462 replaces the progress bar with a start/end date '
'pair — the screen\'s signature element',
);
expect(find.text(diaMesLocalizado('es', inicio)), findsOneWidget);
expect(find.text(diaMesLocalizado('es', fin)), findsOneWidget);
expect(
find.text(nombreDiaSemanaLocalizado('es', inicio)),
findsOneWidget,
);
expect(find.text(nombreDiaSemanaLocalizado('es', fin)), findsOneWidget);
final fechaTexto = tester.widget<Text>(
find.text(diaMesLocalizado('es', inicio)),
);
expect(
fechaTexto.style?.fontSize,
26,
reason: 't4:459 the date digits are 26px/w800/ls-1',
);
expect(fechaTexto.style?.fontWeight, FontWeight.w800);
final diaSemanaTexto = tester.widget<Text>(
find.text(nombreDiaSemanaLocalizado('es', inicio)),
);
expect(
diaSemanaTexto.style?.fontSize,
11,
reason: 't4:459 the weekday caption is 11px/w700/50%',
);
expect(diaSemanaTexto.style?.fontWeight, FontWeight.w700);
final regla = tester.widget<DecoratedBox>(
find.byKey(const ValueKey('vacaciones-regla-activo')),
);
final decoration = regla.decoration as BoxDecoration;
expect(
decoration.gradient,
isNotNull,
reason:
't4:460 the active range connector is a brand-teal gradient, '
'not the flat rgba(255,255,255,.14) used by scheduled rows',
);
},
);
testWidgets(
'un rango programado usa el mismo par de fechas, con una regla plana y '
'una fila de etiqueta con el nombre del rango',
(tester) async {
final inicio = _hoyDia.add(const Duration(days: 20));
final fin = _hoyDia.add(const Duration(days: 25));
final estado = await _crearEstado(
vacaciones: [
RangoVacaciones(id: 'f2', nombre: 'Verano', inicio: inicio, fin: fin),
],
);
addTearDown(estado.dispose);
await tester.pumpWidget(_buildScreen(estado));
await _pumpEstable(tester);
expect(
find.descendant(
of: find.byType(PantallaVacaciones),
matching: find.byType(ListTile),
),
findsNothing,
reason: '9b.5 (t4:469-479) replaces the ListTile row with a card',
);
expect(find.text(diaMesLocalizado('es', inicio)), findsOneWidget);
expect(find.text(diaMesLocalizado('es', fin)), findsOneWidget);
expect(
find.text('Verano'),
findsOneWidget,
reason:
't4:478 the label row shows the range name next to a '
'"label" icon',
);
final regla = tester.widget<DecoratedBox>(
find.byKey(const ValueKey('vacaciones-regla-f2')),
);
final decoration = regla.decoration as BoxDecoration;
expect(decoration.gradient, isNull);
expect(
decoration.color,
Colors.white.withValues(alpha: 0.14),
reason:
't4:474 scheduled/past rows use a flat rgba(255,255,255,.14) '
'connector, not the active gradient',
);
},
);
group('visual fidelity (audit 9b.1/9b.2/9b.6/9b.7)', () {
testWidgets(
'9b.1: a header "Add" action is reachable and opens the same form '
'as the CTA (t4:446)',
(tester) async {
final estado = await _crearEstado();
addTearDown(estado.dispose);
await tester.pumpWidget(_buildScreen(estado));
await _pumpEstable(tester);
final l10n = AppLocalizations.of(
tester.element(find.byType(PantallaVacaciones)),
);
final appBar = tester.widget<AppBar>(find.byType(AppBar));
expect(appBar.actions, isNotNull);
expect(appBar.actions, isNotEmpty);
await tester.tap(find.byKey(const ValueKey('vacation-add-header')));
await _pumpEstable(tester);
expect(find.text(l10n.newVacationRangeTitle), findsOneWidget);
},
);
testWidgets(
'9b.2: the explanatory info banner is always visible (t4:448)',
(tester) async {
final estado = await _crearEstado();
addTearDown(estado.dispose);
await tester.pumpWidget(_buildScreen(estado));
await _pumpEstable(tester);
final l10n = AppLocalizations.of(
tester.element(find.byType(PantallaVacaciones)),
);
expect(find.text(l10n.vacationExplainerBanner), findsOneWidget);
},
);
testWidgets(
'9b.6: the bottom CTA has a dashed border and a date_range icon '
'(t4:487)',
(tester) async {
final estado = await _crearEstado();
addTearDown(estado.dispose);
await tester.pumpWidget(_buildScreen(estado));
await _pumpEstable(tester);
expect(find.byIcon(Icons.date_range_rounded), findsOneWidget);
// Exactly ONE add_rounded on the whole screen -- the header's OWN
// "Add" action (audit 9b.1). The bottom CTA no longer uses it.
expect(find.byIcon(Icons.add_rounded), findsOneWidget);
},
);
testWidgets(
'9b.7: "Rangos pasados" is collapsed by default, showing a count, '
'and expands on tap (t4:489)',
(tester) async {
final estado = await _crearEstado(
vacaciones: [
RangoVacaciones(
id: 'pa1',
nombre: 'Rango viejo',
inicio: _hoyDia.subtract(const Duration(days: 30)),
fin: _hoyDia.subtract(const Duration(days: 20)),
),
],
);
addTearDown(estado.dispose);
await tester.pumpWidget(_buildScreen(estado));
await _pumpEstable(tester);
final l10n = AppLocalizations.of(
tester.element(find.byType(PantallaVacaciones)),
);
expect(find.text(l10n.vacationPastSectionTitle), findsOneWidget);
expect(find.text('1'), findsOneWidget);
expect(
find.text('Rango viejo'),
findsNothing,
reason:
'collapsed by default -- matches the prototype count-only row',
);
await tester.tap(find.text(l10n.vacationPastSectionTitle));
await _pumpEstable(tester);
expect(find.text('Rango viejo'), findsOneWidget);
},
);
});
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);
},
);
});
group('fix vacaciones-delete: el editor ofrece una accion de eliminar solo '
'al editar un rango existente, reusando _confirmarEliminarRango y '
'eliminarRangoVacaciones exactamente como el swipe', () {
testWidgets(
'creando un rango NUEVO (CTA "Anadir rango"), el editor NO muestra '
'una accion de eliminar -- no hay nada que borrar todavia',
(tester) async {
final estado = await _crearEstado();
addTearDown(estado.dispose);
await tester.pumpWidget(_buildScreen(estado));
await _pumpEstable(tester);
final l10n = AppLocalizations.of(
tester.element(find.byType(PantallaVacaciones)),
);
await tester.tap(find.text(l10n.addVacationRangeCta));
await _pumpEstable(tester);
expect(find.text(l10n.newVacationRangeTitle), findsOneWidget);
expect(
find.byKey(const ValueKey('vacation-delete-button')),
findsNothing,
);
},
);
testWidgets('editando un rango EXISTENTE (tap en su tarjeta), el editor SI '
'muestra una accion de eliminar junto al boton de guardar', (
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);
await tester.tap(find.byKey(const ValueKey('vacaciones-tarjeta-f2')));
await _pumpEstable(tester);
final l10n = AppLocalizations.of(
tester.element(find.byType(PantallaVacaciones)),
);
expect(find.text(l10n.editVacationRangeTitle), findsOneWidget);
expect(
find.byKey(const ValueKey('vacation-delete-button')),
findsOneWidget,
);
});
testWidgets(
'tocar eliminar en el editor pide confirmacion (misma que el swipe); '
'cancelar conserva el rango y el editor sigue abierto',
(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.tap(find.byKey(const ValueKey('vacation-delete-button')));
await _pumpEstable(tester);
expect(find.text(l10n.vacationDeleteConfirmTitle), findsOneWidget);
await tester.tap(find.text(l10n.cancelAction));
await _pumpEstable(tester);
expect(estado.vacaciones, hasLength(1));
expect(find.text(l10n.editVacationRangeTitle), findsOneWidget);
},
);
testWidgets('tocar eliminar en el editor y confirmar llama a '
'eliminarRangoVacaciones y cierra el editor (mismo efecto que el '
'swipe, sin pasar por _guardar)', (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.tap(find.byKey(const ValueKey('vacation-delete-button')));
await _pumpEstable(tester);
expect(find.text(l10n.vacationDeleteConfirmTitle), findsOneWidget);
// `find.widgetWithText(FilledButton, ...)`, not a bare
// `find.text(...)`: the sheet's own OutlinedButton delete action
// (same "Eliminar" label) is still in the tree behind the dialog,
// so a bare text finder would ambiguously match both.
await tester.tap(find.widgetWithText(FilledButton, l10n.deleteAction));
await _pumpEstable(tester);
expect(estado.vacaciones, isEmpty);
expect(
find.text(l10n.editVacationRangeTitle),
findsNothing,
reason: 'the sheet must pop, exactly like a successful _guardar',
);
});
});
}