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.
239 lines
8.7 KiB
Dart
239 lines
8.7 KiB
Dart
import 'dart:io';
|
|
|
|
import 'package:flutter/material.dart';
|
|
import 'package:flutter_test/flutter_test.dart';
|
|
import 'package:pluriwave/estado/estado_alarmas.dart';
|
|
import 'package:pluriwave/estado/estado_busqueda.dart';
|
|
import 'package:pluriwave/estado/estado_ecualizador.dart';
|
|
import 'package:pluriwave/estado/estado_entitlement.dart';
|
|
import 'package:pluriwave/estado/estado_grabacion.dart';
|
|
import 'package:pluriwave/estado/estado_idioma.dart';
|
|
import 'package:pluriwave/estado/estado_radio.dart';
|
|
import 'package:pluriwave/l10n/gen/app_localizations.dart';
|
|
import 'package:pluriwave/pantallas/pantalla_ajustes.dart';
|
|
import 'package:pluriwave/pantallas/pantalla_alarmas.dart';
|
|
import 'package:pluriwave/pantallas/pantalla_buscar.dart';
|
|
import 'package:pluriwave/pantallas/pantalla_favoritos.dart';
|
|
import 'package:pluriwave/pantallas/pantalla_inicio.dart';
|
|
import 'package:pluriwave/servicios/servicio_alarmas.dart';
|
|
import 'package:pluriwave/servicios/servicio_anuncios.dart';
|
|
import 'package:pluriwave/widgets/pluri_root_header.dart';
|
|
import 'package:provider/provider.dart';
|
|
import 'package:shared_preferences/shared_preferences.dart';
|
|
|
|
import '../helpers/fakes.dart';
|
|
import '../helpers/fakes_alarmas.dart';
|
|
|
|
/// S1 (Tier 1 visual fidelity): `app.dart`'s `PluriWaveScaffold(appBar:
|
|
/// AppBar(...))` is gone — every root now draws its own [PluriRootHeader]
|
|
/// instead, matching the prototype (no screen in `t4` shows a global
|
|
/// `AppBar`). [PluriRootHeader]'s own title text is asserted via
|
|
/// `find.descendant` throughout — see `pluri_screen_header_retired_test.dart`
|
|
/// for S2's separate assertion that the old glass-hero subtitle is gone.
|
|
///
|
|
/// Pre-existing project constraint (see `pantalla_ajustes_test.dart`):
|
|
/// PluriGlassSurface paints a background over ListTile's ink layer, which
|
|
/// Flutter flags as a warning-level assertion, not a correctness bug.
|
|
void _suppressListTileInkAssertion() {
|
|
final original = FlutterError.onError;
|
|
FlutterError.onError = (details) {
|
|
if (details.exceptionAsString().contains(
|
|
'ListTile background color or ink splashes may be invisible',
|
|
)) {
|
|
return;
|
|
}
|
|
original?.call(details);
|
|
};
|
|
addTearDown(() => FlutterError.onError = original);
|
|
}
|
|
|
|
void main() {
|
|
setUp(() {
|
|
SharedPreferences.setMockInitialValues({});
|
|
});
|
|
|
|
Future<File> archivoCustomVacio() async => File(
|
|
'${Directory.current.path}/test/fixtures/emisoras_custom_vacio.json',
|
|
);
|
|
|
|
EstadoRadio crearEstadoRadio() => EstadoRadio(
|
|
esPremium: () => true,
|
|
audio: FakeServicioAudio(),
|
|
favoritos: FakeServicioFavoritos(),
|
|
radio: FakeServicioRadio(),
|
|
servicioEcualizador: FakeServicioEcualizador(),
|
|
servicioGrabacion: FakeServicioGrabacionRadioInactiva(),
|
|
resolverArchivoCustom: archivoCustomVacio,
|
|
iniciarAutomaticamente: false,
|
|
);
|
|
|
|
Widget testApp(EstadoRadio estado, Widget body, {EstadoAlarmas? alarmas}) {
|
|
return MultiProvider(
|
|
providers: [
|
|
ChangeNotifierProvider<EstadoRadio>.value(value: estado),
|
|
ListenableProvider<EstadoEcualizador>.value(value: estado.ecualizador),
|
|
ListenableProvider<EstadoGrabacion>.value(value: estado.grabacion),
|
|
ListenableProvider<EstadoBusqueda>.value(value: estado.busqueda),
|
|
ChangeNotifierProvider<EstadoIdioma>.value(value: EstadoIdioma()),
|
|
ChangeNotifierProvider<EstadoEntitlement>(
|
|
create: (_) => EstadoEntitlement(prefs: null),
|
|
),
|
|
Provider<ServicioAnuncios>(
|
|
create: (_) => ServicioAnuncios(esPremium: () => true),
|
|
),
|
|
if (alarmas != null)
|
|
ChangeNotifierProvider<EstadoAlarmas>.value(value: alarmas),
|
|
],
|
|
child: MaterialApp(
|
|
locale: const Locale('en'),
|
|
localizationsDelegates: AppLocalizations.localizationsDelegates,
|
|
supportedLocales: AppLocalizations.supportedLocales,
|
|
home: Scaffold(body: body),
|
|
),
|
|
);
|
|
}
|
|
|
|
void setLargeSurface(WidgetTester tester) {
|
|
tester.view.physicalSize = const Size(1440, 3200);
|
|
tester.view.devicePixelRatio = 1.0;
|
|
addTearDown(tester.view.resetPhysicalSize);
|
|
addTearDown(tester.view.resetDevicePixelRatio);
|
|
}
|
|
|
|
Future<void> pumpStable(WidgetTester tester) async {
|
|
await tester.pump();
|
|
await tester.pump(const Duration(milliseconds: 100));
|
|
}
|
|
|
|
Finder titleInHeader(String title) => find.descendant(
|
|
of: find.byType(PluriRootHeader),
|
|
matching: find.text(title),
|
|
);
|
|
|
|
testWidgets(
|
|
'Escuchar is the documented header exception: a 52px "now listening" '
|
|
'eyebrow instead of a PluriRootHeader title row, and still no AppBar',
|
|
(tester) async {
|
|
// The prototype gives Escuchar a STRUCTURALLY different header from
|
|
// every other root (t4 line 53 vs line 325): 52px rather than 56, an
|
|
// eyebrow at 15px/w800/55% opacity rather than a 23px title, and
|
|
// trailing actions rather than a CTA. This test originally asserted
|
|
// `PluriRootHeader` on all five roots — an over-broad premise, since
|
|
// Escuchar's header was never a title row in the design.
|
|
//
|
|
// What still holds unconditionally, and is what this guards: no
|
|
// AppBar anywhere (systemic item S1), and the sleep-timer action that
|
|
// used to live in the removed global chrome is still reachable here.
|
|
_suppressListTileInkAssertion();
|
|
setLargeSurface(tester);
|
|
final estado = crearEstadoRadio();
|
|
addTearDown(estado.dispose);
|
|
|
|
await tester.pumpWidget(testApp(estado, const PantallaInicio()));
|
|
await pumpStable(tester);
|
|
|
|
expect(find.byType(AppBar), findsNothing);
|
|
expect(
|
|
find.byType(PluriRootHeader),
|
|
findsNothing,
|
|
reason: 'Escuchar uses its own eyebrow header, not the shared one',
|
|
);
|
|
expect(find.text('Now listening'), findsOneWidget);
|
|
expect(
|
|
find.byIcon(Icons.bedtime_outlined),
|
|
findsOneWidget,
|
|
reason: 'the sleep-timer action survived the global AppBar removal',
|
|
);
|
|
},
|
|
);
|
|
|
|
testWidgets(
|
|
'Buscar draws its own PluriRootHeader with the search title, no AppBar',
|
|
(tester) async {
|
|
_suppressListTileInkAssertion();
|
|
setLargeSurface(tester);
|
|
final estado = crearEstadoRadio();
|
|
addTearDown(estado.dispose);
|
|
|
|
await tester.pumpWidget(testApp(estado, const PantallaBuscar()));
|
|
await pumpStable(tester);
|
|
|
|
expect(find.byType(PluriRootHeader), findsOneWidget);
|
|
expect(titleInHeader('Search signal'), findsOneWidget);
|
|
expect(find.byType(AppBar), findsNothing);
|
|
},
|
|
);
|
|
|
|
testWidgets(
|
|
'Favoritos (empty state) draws its own PluriRootHeader, no AppBar',
|
|
(tester) async {
|
|
_suppressListTileInkAssertion();
|
|
setLargeSurface(tester);
|
|
final estado = crearEstadoRadio();
|
|
addTearDown(estado.dispose);
|
|
|
|
await tester.pumpWidget(testApp(estado, const PantallaFavoritos()));
|
|
await pumpStable(tester);
|
|
|
|
expect(find.byType(PluriRootHeader), findsOneWidget);
|
|
expect(titleInHeader('Favorites'), findsOneWidget);
|
|
expect(find.byType(AppBar), findsNothing);
|
|
},
|
|
);
|
|
|
|
testWidgets('Alarmas draws its own PluriRootHeader, no AppBar', (
|
|
tester,
|
|
) async {
|
|
_suppressListTileInkAssertion();
|
|
setLargeSurface(tester);
|
|
final estado = crearEstadoRadio();
|
|
final alarmas = EstadoAlarmas(
|
|
esPremium: () => true,
|
|
servicio: ServicioAlarmas(reloj: () => DateTime(2026, 6, 1, 6, 0)),
|
|
android: FakePuertoAlarmasAndroid(),
|
|
iniciarAutomaticamente: false,
|
|
);
|
|
addTearDown(estado.dispose);
|
|
addTearDown(alarmas.dispose);
|
|
|
|
await tester.pumpWidget(
|
|
testApp(estado, const PantallaAlarmas(), alarmas: alarmas),
|
|
);
|
|
await pumpStable(tester);
|
|
|
|
expect(find.byType(PluriRootHeader), findsOneWidget);
|
|
expect(titleInHeader('Music wake-up'), findsOneWidget);
|
|
expect(find.byType(AppBar), findsNothing);
|
|
});
|
|
|
|
testWidgets(
|
|
'Ajustes draws its own PluriRootHeader, no AppBar, and its bedtime '
|
|
'action opens the sleep-timer sheet',
|
|
(tester) async {
|
|
_suppressListTileInkAssertion();
|
|
setLargeSurface(tester);
|
|
final estado = crearEstadoRadio();
|
|
addTearDown(estado.dispose);
|
|
|
|
await tester.pumpWidget(testApp(estado, const PantallaAjustes()));
|
|
await pumpStable(tester);
|
|
|
|
expect(find.byType(PluriRootHeader), findsOneWidget);
|
|
expect(titleInHeader('Settings'), findsOneWidget);
|
|
expect(find.byType(AppBar), findsNothing);
|
|
|
|
await tester.tap(find.byIcon(Icons.bedtime_outlined));
|
|
await pumpStable(tester);
|
|
|
|
// Not `find.text('Sleep timer')` — Ajustes' own "Sleep timer"
|
|
// FilaAjuste row (`l10n.timerSectionTitle`) coincidentally shares the
|
|
// exact same string as `l10n.sleepTimer`. The description line is
|
|
// unique to the sheet this action opens.
|
|
expect(
|
|
find.text('Smooth radio shutdown with an exact countdown.'),
|
|
findsOneWidget,
|
|
);
|
|
},
|
|
);
|
|
}
|