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) {
|
void showPluriSleepTimerSheet(BuildContext context) {
|
||||||
showModalBottomSheet(
|
showModalBottomSheet(
|
||||||
context: context,
|
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,
|
showDragHandle: true,
|
||||||
builder:
|
builder:
|
||||||
(ctx) => Consumer<EstadoRadio>(
|
(ctx) => Consumer<EstadoRadio>(
|
||||||
@@ -79,12 +86,17 @@ void showPluriSleepTimerSheet(BuildContext context) {
|
|||||||
Duration(seconds: segundos),
|
Duration(seconds: segundos),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
onPressed: () {
|
// Issue 2 (feedback-pruebas): no longer pops
|
||||||
estado.iniciarTimerDuracion(
|
// the sheet -- `estado.iniciarTimerDuracion`
|
||||||
Duration(seconds: segundos),
|
// notifies this `Consumer<EstadoRadio>`,
|
||||||
);
|
// which swaps straight to the countdown
|
||||||
Navigator.pop(ctx);
|
// 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(
|
ActionChip(
|
||||||
avatar: const Icon(Icons.tune_rounded, size: 18),
|
avatar: const Icon(Icons.tune_rounded, size: 18),
|
||||||
@@ -93,8 +105,9 @@ void showPluriSleepTimerSheet(BuildContext context) {
|
|||||||
final duracion =
|
final duracion =
|
||||||
await _pedirDuracionPersonalizada(ctx);
|
await _pedirDuracionPersonalizada(ctx);
|
||||||
if (duracion == null || !ctx.mounted) return;
|
if (duracion == null || !ctx.mounted) return;
|
||||||
|
// Issue 2: same as above -- stays open on
|
||||||
|
// the countdown view rather than closing.
|
||||||
estado.iniciarTimerDuracion(duracion);
|
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