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:
@@ -14,6 +14,13 @@ import 'pluri_layout.dart';
|
||||
void showPluriSleepTimerSheet(BuildContext context) {
|
||||
showModalBottomSheet(
|
||||
context: context,
|
||||
// Issue 2 (feedback-pruebas): without this, the sheet is capped to a
|
||||
// FRACTION of the screen height. That never mattered while the sheet
|
||||
// always closed immediately after picking a duration (see the removed
|
||||
// `Navigator.pop` calls below) -- now that the countdown view actually
|
||||
// stays open, its title + description + headline-sized remaining-time
|
||||
// text can overflow that capped height on a real phone width.
|
||||
isScrollControlled: true,
|
||||
showDragHandle: true,
|
||||
builder:
|
||||
(ctx) => Consumer<EstadoRadio>(
|
||||
@@ -79,12 +86,17 @@ void showPluriSleepTimerSheet(BuildContext context) {
|
||||
Duration(seconds: segundos),
|
||||
),
|
||||
),
|
||||
onPressed: () {
|
||||
estado.iniciarTimerDuracion(
|
||||
Duration(seconds: segundos),
|
||||
);
|
||||
Navigator.pop(ctx);
|
||||
},
|
||||
// Issue 2 (feedback-pruebas): no longer pops
|
||||
// the sheet -- `estado.iniciarTimerDuracion`
|
||||
// notifies this `Consumer<EstadoRadio>`,
|
||||
// which swaps straight to the countdown
|
||||
// view above so the user actually SEES the
|
||||
// remaining time instead of the sheet just
|
||||
// closing with no feedback.
|
||||
onPressed:
|
||||
() => estado.iniciarTimerDuracion(
|
||||
Duration(seconds: segundos),
|
||||
),
|
||||
),
|
||||
ActionChip(
|
||||
avatar: const Icon(Icons.tune_rounded, size: 18),
|
||||
@@ -93,8 +105,9 @@ void showPluriSleepTimerSheet(BuildContext context) {
|
||||
final duracion =
|
||||
await _pedirDuracionPersonalizada(ctx);
|
||||
if (duracion == null || !ctx.mounted) return;
|
||||
// Issue 2: same as above -- stays open on
|
||||
// the countdown view rather than closing.
|
||||
estado.iniciarTimerDuracion(duracion);
|
||||
Navigator.pop(ctx);
|
||||
},
|
||||
),
|
||||
],
|
||||
|
||||
@@ -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();
|
||||
},
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user