Files
pluriwave/test/pantallas/pantalla_alarma_sonando_tier5_test.dart
T
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

216 lines
7.5 KiB
Dart

import 'dart:async';
import 'package:flutter/material.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/formato_fechas.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:pluriwave/tema/pluriwave_theme.dart';
import 'package:provider/provider.dart';
import 'package:shared_preferences/shared_preferences.dart';
import '../helpers/fakes.dart';
import '../helpers/fakes_alarmas.dart';
/// Closing batch (items 9.4, 9.10, audit id 2521) -- the ringing screen.
/// Mirrors `pantalla_alarma_sonando_tier4_test.dart`'s own harness (kept as
/// a local copy, not a shared import, matching this file's established
/// precedent of small helper duplication over cross-test-file coupling).
Future<void> _montarPantalla(WidgetTester tester) async {
tester.view.physicalSize = const Size(1440, 3200);
tester.view.devicePixelRatio = 1.0;
addTearDown(tester.view.resetPhysicalSize);
addTearDown(tester.view.resetDevicePixelRatio);
final audio = FakeServicioAudio();
audio.emitirEstado(EstadoReproduccion.reproduciendo);
final radio = EstadoRadio(
esPremium: () => true,
audio: audio,
favoritos: FakeServicioFavoritos(),
radio: FakeServicioRadio(),
servicioEcualizador: FakeServicioEcualizador(),
servicioGrabacion: FakeServicioGrabacionRadioInactiva(),
iniciarAutomaticamente: false,
);
addTearDown(radio.dispose);
final android = FakePuertoAlarmasAndroid();
final estadoAlarmas = EstadoAlarmas(
esPremium: () => true,
servicio: ServicioAlarmas(reloj: () => DateTime(2026, 6, 11, 7, 0)),
android: android,
iniciarAutomaticamente: false,
);
addTearDown(estadoAlarmas.dispose);
addTearDown(android.dispose);
await estadoAlarmas.guardarAlarma(
const AlarmaMusical(
id: 'tier5-1',
nombre: 'Despertar',
hora: 7,
minuto: 30,
tipoProgramacion: TipoProgramacionAlarma.diaria,
diasSemana: [],
snoozeMinutos: 10,
emisora: Emisora(
uuid: 'e1',
nombre: 'Radio Uno',
url: 'https://radio.example/stream',
),
),
);
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,
theme: PluriWaveTheme.dark(),
home: const SizedBox.shrink(),
),
),
);
final navigator = tester.state<NavigatorState>(find.byType(Navigator));
unawaited(
navigator.push(
MaterialPageRoute<void>(
builder:
(_) => PantallaAlarmaSonando(alarma: estadoAlarmas.alarmas.single),
fullscreenDialog: true,
),
),
);
await tester.pump();
await tester.pump(const Duration(milliseconds: 50));
}
void main() {
setUp(() {
SharedPreferences.setMockInitialValues({});
});
testWidgets('visual fidelity (audit 9.4): the date line renders BELOW the '
'hero time (t4:415-419: pill, then 7:30, then the date)', (tester) async {
await _montarPantalla(tester);
final localeTag =
Localizations.localeOf(
tester.element(find.byType(PantallaAlarmaSonando)),
).toString();
final esperado = fechaLargaConDiaSemana(localeTag, DateTime.now());
expect(find.text(esperado), findsOneWidget);
// Order: pill, then the hero time, then the date line. This test used
// to assert date-before-time and cited "t4:419" for it — but 419 is
// simply the source line the date occupies, and in the prototype it
// comes AFTER the 88px time on line 417. The citation refuted the
// assertion it was supporting.
final pillY =
tester
.getBottomLeft(find.byKey(const ValueKey('ringing-schedule-pill')))
.dy;
final timeY =
tester
.getBottomLeft(find.byKey(const ValueKey('ringing-hero-time')))
.dy;
final dateY = tester.getTopLeft(find.text(esperado)).dy;
expect(pillY <= timeY, isTrue, reason: 'pill sits above the time');
expect(timeY <= dateY, isTrue, reason: 'the date sits below the time');
// Regression guard: pumpAndSettle must still complete (purely
// additive static text, no new animation).
await tester.pumpAndSettle();
});
testWidgets(
'visual fidelity (audit 9.10): the highlighted snooze tile carries a '
'"usual" qualifier ADDITIVELY -- the original flat label the dismiss '
'guard finds by text stays fully intact',
(tester) async {
await _montarPantalla(tester);
final l10n = AppLocalizations.of(
tester.element(find.byType(PantallaAlarmaSonando)),
);
// The ORIGINAL flat label must still resolve to exactly one Text --
// this IS the dismiss-guard's own finder contract.
expect(find.text(l10n.alarmSnoozeOptionLabel(10)), findsOneWidget);
// The additive qualifier renders too, but only on the destacado tile.
expect(find.text(l10n.alarmSnoozeUsualLabel), findsOneWidget);
expect(find.text(l10n.alarmSnoozeOptionLabel(3)), findsOneWidget);
expect(find.text(l10n.alarmSnoozeOptionLabel(5)), findsOneWidget);
// Regression guard: the exact hazard 9.9/9.10 share -- pumpAndSettle
// must still complete, and the flat label must remain the SAME
// widget the dismiss guard taps (still inside a FilledButton).
expect(
find.ancestor(
of: find.text(l10n.alarmSnoozeOptionLabel(10)),
matching: find.byType(FilledButton),
),
findsOneWidget,
);
await tester.pumpAndSettle();
},
);
testWidgets(
'Issue 3 (feedback-pruebas): the gap above the snooze tiles matches the '
'gap below them (t4:427 draws a uniform gap:12 flex column) -- the '
'previous 10/14 pair matched neither the prototype nor each other',
(tester) async {
await _montarPantalla(tester);
final l10n = AppLocalizations.of(
tester.element(find.byType(PantallaAlarmaSonando)),
);
final eyebrowBottom =
tester
.getBottomLeft(
find
.ancestor(
of: find.byIcon(Icons.snooze_rounded),
matching: find.byType(Row),
)
.first,
)
.dy;
final tileDestacado = find.ancestor(
of: find.text(l10n.alarmSnoozeOptionLabel(10)),
matching: find.byType(FilledButton),
);
final tilesTop = tester.getTopLeft(tileDestacado).dy;
final tilesBottom = tester.getBottomLeft(tileDestacado).dy;
final stopButtonTop =
tester
.getTopLeft(find.byKey(const ValueKey('ringing-stop-button')))
.dy;
expect(
tilesTop - eyebrowBottom,
12,
reason: 't4:427: gap:12 above the snooze tiles',
);
expect(
stopButtonTop - tilesBottom,
12,
reason: 't4:427: gap:12 below the snooze tiles, same as above',
);
},
);
}