fix(alarm): localize pre-notice countdown and fix snooze dismiss
Replace hardcoded Spanish pre-notice text with computed remaining minutes using l10n template passed via MethodChannel. Fix snooze dismiss in dead-app state with canPop guard and SystemNavigator.pop fallback.
This commit is contained in:
@@ -0,0 +1,279 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:pluriwave/estado/estado_alarmas.dart';
|
||||
import 'package:pluriwave/estado/estado_radio.dart';
|
||||
import 'package:pluriwave/l10n/gen/app_localizations.dart';
|
||||
import 'package:pluriwave/modelos/alarma_musical.dart';
|
||||
import 'package:pluriwave/modelos/emisora.dart';
|
||||
import 'package:pluriwave/pantallas/pantalla_alarma_sonando.dart';
|
||||
import 'package:pluriwave/servicios/servicio_alarmas.dart';
|
||||
import 'package:pluriwave/servicios/servicio_audio.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
|
||||
import '../helpers/fakes.dart';
|
||||
import '../helpers/fakes_alarmas.dart';
|
||||
|
||||
// Tracks SystemNavigator.pop() calls via the platform channel mock.
|
||||
class _SystemNavigatorSpy {
|
||||
int popCalls = 0;
|
||||
|
||||
void install() {
|
||||
TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger
|
||||
.setMockMethodCallHandler(
|
||||
SystemChannels.platform,
|
||||
(call) async {
|
||||
if (call.method == 'SystemNavigator.pop') {
|
||||
popCalls++;
|
||||
}
|
||||
return null;
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
void uninstall() {
|
||||
TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger
|
||||
.setMockMethodCallHandler(SystemChannels.platform, null);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _montarComoRaiz(
|
||||
WidgetTester tester, {
|
||||
required FakePuertoAlarmasAndroid android,
|
||||
required EstadoAlarmas estadoAlarmas,
|
||||
required EstadoRadio radio,
|
||||
}) async {
|
||||
tester.view.physicalSize = const Size(1440, 3200);
|
||||
tester.view.devicePixelRatio = 1.0;
|
||||
addTearDown(tester.view.resetPhysicalSize);
|
||||
addTearDown(tester.view.resetDevicePixelRatio);
|
||||
|
||||
// Mount alarm screen as ROOT route — simulates dead-app FSI launch.
|
||||
await tester.pumpWidget(
|
||||
MultiProvider(
|
||||
providers: [
|
||||
ChangeNotifierProvider<EstadoRadio>.value(value: radio),
|
||||
ChangeNotifierProvider<EstadoAlarmas>.value(value: estadoAlarmas),
|
||||
],
|
||||
child: MaterialApp(
|
||||
locale: const Locale('es'),
|
||||
localizationsDelegates: AppLocalizations.localizationsDelegates,
|
||||
supportedLocales: AppLocalizations.supportedLocales,
|
||||
home: PantallaAlarmaSonando(
|
||||
alarma: estadoAlarmas.alarmas.single,
|
||||
audioPrearrancado: true,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
}
|
||||
|
||||
Future<void> _montarConHistorial(
|
||||
WidgetTester tester, {
|
||||
required FakePuertoAlarmasAndroid android,
|
||||
required EstadoAlarmas estadoAlarmas,
|
||||
required EstadoRadio radio,
|
||||
}) async {
|
||||
tester.view.physicalSize = const Size(1440, 3200);
|
||||
tester.view.devicePixelRatio = 1.0;
|
||||
addTearDown(tester.view.resetPhysicalSize);
|
||||
addTearDown(tester.view.resetDevicePixelRatio);
|
||||
|
||||
// Mount with a previous route so canPop() returns true.
|
||||
await tester.pumpWidget(
|
||||
MultiProvider(
|
||||
providers: [
|
||||
ChangeNotifierProvider<EstadoRadio>.value(value: radio),
|
||||
ChangeNotifierProvider<EstadoAlarmas>.value(value: estadoAlarmas),
|
||||
],
|
||||
child: MaterialApp(
|
||||
locale: const Locale('es'),
|
||||
localizationsDelegates: AppLocalizations.localizationsDelegates,
|
||||
supportedLocales: AppLocalizations.supportedLocales,
|
||||
home: const SizedBox.shrink(),
|
||||
),
|
||||
),
|
||||
);
|
||||
final navigator = tester.state<NavigatorState>(find.byType(Navigator));
|
||||
unawaited(
|
||||
navigator.push(
|
||||
MaterialPageRoute<void>(
|
||||
builder: (_) => PantallaAlarmaSonando(
|
||||
alarma: estadoAlarmas.alarmas.single,
|
||||
audioPrearrancado: true,
|
||||
),
|
||||
fullscreenDialog: true,
|
||||
),
|
||||
),
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
}
|
||||
|
||||
Future<_Env> _buildEnv() async {
|
||||
final audio = FakeServicioAudio();
|
||||
audio.emitirEstado(EstadoReproduccion.reproduciendo);
|
||||
final radio = EstadoRadio(
|
||||
audio: audio,
|
||||
favoritos: FakeServicioFavoritos(),
|
||||
radio: FakeServicioRadio(),
|
||||
servicioEcualizador: FakeServicioEcualizador(),
|
||||
servicioGrabacion: FakeServicioGrabacionRadioInactiva(),
|
||||
iniciarAutomaticamente: false,
|
||||
);
|
||||
final android = FakePuertoAlarmasAndroid();
|
||||
final estadoAlarmas = EstadoAlarmas(
|
||||
servicio: ServicioAlarmas(reloj: () => DateTime(2026, 6, 11, 7, 0)),
|
||||
android: android,
|
||||
iniciarAutomaticamente: false,
|
||||
);
|
||||
await estadoAlarmas.guardarAlarma(
|
||||
AlarmaMusical(
|
||||
id: 'dismiss-test',
|
||||
nombre: 'Despertar',
|
||||
hora: 7,
|
||||
minuto: 30,
|
||||
tipoProgramacion: TipoProgramacionAlarma.diaria,
|
||||
diasSemana: const [],
|
||||
snoozeMinutos: 5,
|
||||
emisora: const Emisora(
|
||||
uuid: 'e1',
|
||||
nombre: 'Radio Uno',
|
||||
url: 'https://radio.example/stream',
|
||||
),
|
||||
),
|
||||
);
|
||||
return _Env(radio: radio, android: android, estadoAlarmas: estadoAlarmas);
|
||||
}
|
||||
|
||||
class _Env {
|
||||
_Env({
|
||||
required this.radio,
|
||||
required this.android,
|
||||
required this.estadoAlarmas,
|
||||
});
|
||||
final EstadoRadio radio;
|
||||
final FakePuertoAlarmasAndroid android;
|
||||
final EstadoAlarmas estadoAlarmas;
|
||||
|
||||
void dispose() {
|
||||
estadoAlarmas.dispose();
|
||||
android.dispose();
|
||||
radio.dispose();
|
||||
}
|
||||
}
|
||||
|
||||
void main() {
|
||||
final l10n = lookupAppLocalizations(const Locale('es'));
|
||||
|
||||
setUp(() {
|
||||
SharedPreferences.setMockInitialValues({});
|
||||
});
|
||||
|
||||
group('PantallaAlarmaSonando dismiss guard (Phase 5)', () {
|
||||
testWidgets(
|
||||
'posponer: cuando canPop es true, Navigator.pop es llamado y SystemNavigator.pop NO (S5-R1-A)',
|
||||
(tester) async {
|
||||
final env = await _buildEnv();
|
||||
addTearDown(env.dispose);
|
||||
final spy = _SystemNavigatorSpy()..install();
|
||||
addTearDown(spy.uninstall);
|
||||
|
||||
await _montarConHistorial(
|
||||
tester,
|
||||
android: env.android,
|
||||
estadoAlarmas: env.estadoAlarmas,
|
||||
radio: env.radio,
|
||||
);
|
||||
|
||||
// Verify the alarm screen is on top of a stack (canPop == true)
|
||||
expect(find.byType(PantallaAlarmaSonando), findsOneWidget);
|
||||
|
||||
await tester.tap(find.text(l10n.alarmSnoozeOptionLabel(5)));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
// Screen should be dismissed via Navigator.pop (stack pop)
|
||||
expect(find.byType(PantallaAlarmaSonando), findsNothing);
|
||||
// SystemNavigator.pop must NOT have been called
|
||||
expect(spy.popCalls, 0,
|
||||
reason: 'SystemNavigator.pop must not be called when canPop is true');
|
||||
},
|
||||
);
|
||||
|
||||
testWidgets(
|
||||
'posponer: cuando canPop es false (root), SystemNavigator.pop es llamado (S5-R1-B)',
|
||||
(tester) async {
|
||||
final env = await _buildEnv();
|
||||
addTearDown(env.dispose);
|
||||
final spy = _SystemNavigatorSpy()..install();
|
||||
addTearDown(spy.uninstall);
|
||||
|
||||
await _montarComoRaiz(
|
||||
tester,
|
||||
android: env.android,
|
||||
estadoAlarmas: env.estadoAlarmas,
|
||||
radio: env.radio,
|
||||
);
|
||||
|
||||
expect(find.byType(PantallaAlarmaSonando), findsOneWidget);
|
||||
|
||||
await tester.tap(find.text(l10n.alarmSnoozeOptionLabel(5)));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
// SystemNavigator.pop must be called exactly once
|
||||
expect(spy.popCalls, 1,
|
||||
reason: 'SystemNavigator.pop must be called when canPop is false');
|
||||
},
|
||||
);
|
||||
|
||||
testWidgets(
|
||||
'detener: cuando canPop es true, Navigator.pop es llamado y SystemNavigator.pop NO (S5-R1-A)',
|
||||
(tester) async {
|
||||
final env = await _buildEnv();
|
||||
addTearDown(env.dispose);
|
||||
final spy = _SystemNavigatorSpy()..install();
|
||||
addTearDown(spy.uninstall);
|
||||
|
||||
await _montarConHistorial(
|
||||
tester,
|
||||
android: env.android,
|
||||
estadoAlarmas: env.estadoAlarmas,
|
||||
radio: env.radio,
|
||||
);
|
||||
|
||||
await tester.tap(find.text(l10n.stopAlarmAction));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(find.byType(PantallaAlarmaSonando), findsNothing);
|
||||
expect(spy.popCalls, 0,
|
||||
reason: 'SystemNavigator.pop must not be called when canPop is true');
|
||||
},
|
||||
);
|
||||
|
||||
testWidgets(
|
||||
'detener: cuando canPop es false (root), SystemNavigator.pop es llamado (S5-R1-B)',
|
||||
(tester) async {
|
||||
final env = await _buildEnv();
|
||||
addTearDown(env.dispose);
|
||||
final spy = _SystemNavigatorSpy()..install();
|
||||
addTearDown(spy.uninstall);
|
||||
|
||||
await _montarComoRaiz(
|
||||
tester,
|
||||
android: env.android,
|
||||
estadoAlarmas: env.estadoAlarmas,
|
||||
radio: env.radio,
|
||||
);
|
||||
|
||||
await tester.tap(find.text(l10n.stopAlarmAction));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(spy.popCalls, 1,
|
||||
reason: 'SystemNavigator.pop must be called when canPop is false');
|
||||
},
|
||||
);
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user