fix(timer): show the live countdown in the sleep timer sheet

showPluriSleepTimerSheet already had a working countdown branch
(ServicioTimer.tiempoRestanteStream), but every preset and the custom
duration flow popped the sheet immediately after starting the timer --
so the countdown never rendered in the primary flow, only if the user
happened to reopen the sheet afterwards.

Stop popping the sheet on start; the existing Consumer<EstadoRadio>
already reacts to iniciarTimerDuracion's notifyListeners and swaps to
the countdown view live. Also make the sheet scroll-controlled: at a
realistic phone width the countdown's title + description + headline-
sized remaining-time text overflowed the default half-screen cap that
never mattered while the sheet always closed before that view could
render.
This commit is contained in:
2026-07-30 19:14:15 +02:00
parent e75f010b98
commit c6ab295c54
2 changed files with 204 additions and 7 deletions
@@ -0,0 +1,184 @@
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:pluriwave/estado/estado_radio.dart';
import 'package:pluriwave/l10n/gen/app_localizations.dart';
import 'package:pluriwave/widgets/pluri_sleep_timer_sheet.dart';
import 'package:provider/provider.dart';
import 'package:shared_preferences/shared_preferences.dart';
import '../helpers/fakes.dart';
import '../helpers/fakes_alarmas.dart';
/// Issue 2 (feedback-pruebas): "Timer de sueño" opened from Escuchar (and
/// every other root header) showed a bottom sheet with no visible
/// countdown. `ServicioTimer.tiempoRestanteStream`/`tiempoRestante` already
/// existed and this sheet already had a `StreamBuilder` countdown branch —
/// but every preset/custom-duration action popped the sheet immediately
/// after starting the timer, so the countdown never had a chance to render
/// in the primary flow. Zero test coverage existed for this file before.
EstadoRadio _estado() => EstadoRadio(
audio: FakeServicioAudio(),
favoritos: FakeServicioFavoritos(),
radio: FakeServicioRadio(),
servicioEcualizador: FakeServicioEcualizador(),
servicioGrabacion: FakeServicioGrabacionRadioInactiva(),
iniciarAutomaticamente: false,
);
Widget _host(EstadoRadio estado) {
return ChangeNotifierProvider<EstadoRadio>.value(
value: estado,
child: MaterialApp(
locale: const Locale('es'),
localizationsDelegates: AppLocalizations.localizationsDelegates,
supportedLocales: AppLocalizations.supportedLocales,
home: Scaffold(
body: Builder(
builder:
(context) => TextButton(
onPressed: () => showPluriSleepTimerSheet(context),
child: const Text('abrir'),
),
),
),
),
);
}
void main() {
setUp(() {
SharedPreferences.setMockInitialValues({});
});
// The default 800x600 test viewport is shorter than a typical phone —
// same fix `pantalla_reproductor_test.dart`/`pantalla_inicio_test.dart`
// already established for other bottom-sheet/full-bleed content.
void ajustarSuperficieRealista(WidgetTester tester) {
tester.view.physicalSize = const Size(390, 844);
tester.view.devicePixelRatio = 1.0;
addTearDown(tester.view.resetPhysicalSize);
addTearDown(tester.view.resetDevicePixelRatio);
}
testWidgets(
'selecting a preset keeps the sheet open and switches it to the live '
'remaining-time countdown, instead of closing with no feedback',
(tester) async {
ajustarSuperficieRealista(tester);
final estado = _estado();
addTearDown(estado.dispose);
await tester.pumpWidget(_host(estado));
await tester.tap(find.text('abrir'));
await tester.pumpAndSettle();
final l10n = AppLocalizations.of(tester.element(find.text('abrir')));
expect(estado.timer.activo, isFalse);
expect(find.byType(ActionChip), findsWidgets);
final chip = tester.widget<ActionChip>(
find.widgetWithText(ActionChip, l10n.durationMinutesOnly(5)),
);
chip.onPressed!();
await tester.pump();
expect(estado.timer.activo, isTrue);
expect(
find.byType(ActionChip),
findsNothing,
reason: 'issue 2: the picker is replaced by the countdown view',
);
expect(find.text(l10n.cancelTimer), findsOneWidget);
expect(
find.text(l10n.durationMinutesOnly(5)),
findsOneWidget,
reason: 'issue 2: the remaining time is now surfaced live',
);
// `ServicioTimer` starts a REAL periodic Timer -- `addTearDown`
// callbacks run too late to satisfy the "no pending timers" check
// (a well-known flutter_test ordering quirk), so it must be
// cancelled here, inside the test body, before it ends.
await estado.timer.cancelar();
},
);
testWidgets('reopening the sheet while a timer is already active shows the '
'countdown immediately, not the duration picker', (tester) async {
ajustarSuperficieRealista(tester);
final estado = _estado();
addTearDown(estado.dispose);
estado.iniciarTimerDuracion(const Duration(minutes: 10));
await tester.pumpWidget(_host(estado));
await tester.tap(find.text('abrir'));
await tester.pumpAndSettle();
final l10n = AppLocalizations.of(tester.element(find.text('abrir')));
expect(find.byType(ActionChip), findsNothing);
expect(find.text(l10n.cancelTimer), findsOneWidget);
expect(find.text(l10n.durationMinutesOnly(10)), findsOneWidget);
// See the comment in the previous test — cancel before the body ends.
await estado.timer.cancelar();
});
testWidgets(
'cancelling from the countdown view stops the timer and closes the '
'sheet',
(tester) async {
ajustarSuperficieRealista(tester);
final estado = _estado();
addTearDown(estado.dispose);
estado.iniciarTimerDuracion(const Duration(minutes: 10));
await tester.pumpWidget(_host(estado));
await tester.tap(find.text('abrir'));
await tester.pumpAndSettle();
final l10n = AppLocalizations.of(tester.element(find.text('abrir')));
final boton = tester.widget<FilledButton>(
find.widgetWithText(FilledButton, l10n.cancelTimer),
);
boton.onPressed!();
await tester.pumpAndSettle();
expect(estado.timer.activo, isFalse);
expect(find.text(l10n.cancelTimer), findsNothing);
},
);
testWidgets(
'starting a custom duration ALSO keeps the sheet open on the countdown '
'view, not just the presets',
(tester) async {
ajustarSuperficieRealista(tester);
final estado = _estado();
addTearDown(estado.dispose);
await tester.pumpWidget(_host(estado));
await tester.tap(find.text('abrir'));
await tester.pumpAndSettle();
final l10n = AppLocalizations.of(tester.element(find.text('abrir')));
await tester.tap(find.text(l10n.optionOther));
await tester.pumpAndSettle();
// Confirm the custom-duration sub-sheet directly (default prefilled
// value is already "15" minutes -- see _TimerPersonalizadoSheetState).
final confirmar = tester.widget<FilledButton>(
find.widgetWithText(FilledButton, l10n.startTimer),
);
confirmar.onPressed!();
await tester.pumpAndSettle();
expect(estado.timer.activo, isTrue);
expect(find.byType(ActionChip), findsNothing);
expect(find.text(l10n.cancelTimer), findsOneWidget);
// See the comment in the first test — cancel before the body ends.
await estado.timer.cancelar();
},
);
}