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.
309 lines
11 KiB
Dart
309 lines
11 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/widgets/pluri_push_scaffold.dart';
|
|
import 'package:provider/provider.dart';
|
|
import 'package:shared_preferences/shared_preferences.dart';
|
|
|
|
import '../helpers/fakes.dart';
|
|
import '../helpers/fakes_alarmas.dart';
|
|
|
|
/// Design ADR-2 enforcement test: chrome is decided by route topology, not a
|
|
/// parameter. Mounting each of the 5 root screens bare (no ancestor
|
|
/// PluriWaveScaffold/Scaffold) must render zero Scaffolds — "no tab bar on
|
|
/// second-level screens" becomes a structural fact, not a convention. A
|
|
/// PluriPushScaffold, by contrast, is the ONE place a second-level screen
|
|
/// gets a Scaffold, and it must render exactly one 56px AppBar with a back
|
|
/// affordance.
|
|
void main() {
|
|
setUp(() {
|
|
SharedPreferences.setMockInitialValues({});
|
|
});
|
|
|
|
Widget testApp(Widget home) {
|
|
return MaterialApp(
|
|
locale: const Locale('es'),
|
|
localizationsDelegates: AppLocalizations.localizationsDelegates,
|
|
supportedLocales: AppLocalizations.supportedLocales,
|
|
// No Scaffold ancestor here on purpose — a bare Material is enough
|
|
// for InkWell/Card without secretly providing the very Scaffold this
|
|
// test asserts is absent.
|
|
home: Material(child: home),
|
|
);
|
|
}
|
|
|
|
Future<File> archivoCustomVacio() async => File(
|
|
'${Directory.current.path}/test/fixtures/emisoras_custom_vacio.json',
|
|
);
|
|
|
|
// Existing project convention (see pantalla_inicio_test.dart et al.): the
|
|
// default 800x600 test surface clips these content-heavy root screens.
|
|
void setLargeSurface(WidgetTester tester) {
|
|
tester.view.physicalSize = const Size(1440, 3200);
|
|
tester.view.devicePixelRatio = 1.0;
|
|
addTearDown(tester.view.resetPhysicalSize);
|
|
addTearDown(tester.view.resetDevicePixelRatio);
|
|
}
|
|
|
|
// 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);
|
|
}
|
|
|
|
group('The 5 root screens build zero Scaffold when mounted bare', () {
|
|
testWidgets('PantallaInicio', (tester) async {
|
|
setLargeSurface(tester);
|
|
final estado = EstadoRadio(
|
|
esPremium: () => true,
|
|
audio: FakeServicioAudio(),
|
|
favoritos: FakeServicioFavoritos(),
|
|
radio: FakeServicioRadio(),
|
|
servicioEcualizador: FakeServicioEcualizador(),
|
|
servicioGrabacion: FakeServicioGrabacionRadioInactiva(),
|
|
resolverArchivoCustom: archivoCustomVacio,
|
|
iniciarAutomaticamente: false,
|
|
);
|
|
addTearDown(estado.dispose);
|
|
|
|
await tester.pumpWidget(
|
|
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),
|
|
],
|
|
child: testApp(const PantallaInicio()),
|
|
),
|
|
);
|
|
await tester.pump();
|
|
|
|
expect(find.byType(Scaffold), findsNothing);
|
|
});
|
|
|
|
testWidgets('PantallaBuscar', (tester) async {
|
|
setLargeSurface(tester);
|
|
// WU6 correction: PantallaBuscar now also owns the discovery landing
|
|
// state relocated from PantallaInicio (task 6.5), which reads
|
|
// EstadoRadio directly (near-you/trending/browse-grid selectors) —
|
|
// a bare EstadoBusqueda provider is no longer sufficient.
|
|
final estado = EstadoRadio(
|
|
esPremium: () => true,
|
|
audio: FakeServicioAudio(),
|
|
favoritos: FakeServicioFavoritos(),
|
|
radio: FakeServicioRadio(),
|
|
servicioEcualizador: FakeServicioEcualizador(),
|
|
servicioGrabacion: FakeServicioGrabacionRadioInactiva(),
|
|
resolverArchivoCustom: archivoCustomVacio,
|
|
iniciarAutomaticamente: false,
|
|
);
|
|
addTearDown(estado.dispose);
|
|
|
|
await tester.pumpWidget(
|
|
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),
|
|
],
|
|
child: testApp(const PantallaBuscar()),
|
|
),
|
|
);
|
|
await tester.pump();
|
|
|
|
expect(find.byType(Scaffold), findsNothing);
|
|
});
|
|
|
|
testWidgets('PantallaFavoritos', (tester) async {
|
|
setLargeSurface(tester);
|
|
final estado = EstadoRadio(
|
|
esPremium: () => true,
|
|
audio: FakeServicioAudio(),
|
|
favoritos: FakeServicioFavoritos(),
|
|
radio: FakeServicioRadio(),
|
|
servicioEcualizador: FakeServicioEcualizador(),
|
|
servicioGrabacion: FakeServicioGrabacionRadioInactiva(),
|
|
resolverArchivoCustom: archivoCustomVacio,
|
|
iniciarAutomaticamente: false,
|
|
);
|
|
addTearDown(estado.dispose);
|
|
|
|
await tester.pumpWidget(
|
|
ChangeNotifierProvider<EstadoRadio>.value(
|
|
value: estado,
|
|
child: testApp(const PantallaFavoritos()),
|
|
),
|
|
);
|
|
await tester.pump();
|
|
|
|
expect(find.byType(Scaffold), findsNothing);
|
|
});
|
|
|
|
testWidgets('PantallaAlarmas', (tester) async {
|
|
setLargeSurface(tester);
|
|
final android = FakePuertoAlarmasAndroid();
|
|
final estadoAlarmas = EstadoAlarmas(
|
|
esPremium: () => true,
|
|
servicio: ServicioAlarmas(reloj: DateTime.now),
|
|
android: android,
|
|
iniciarAutomaticamente: false,
|
|
);
|
|
addTearDown(estadoAlarmas.dispose);
|
|
addTearDown(android.dispose);
|
|
|
|
await tester.pumpWidget(
|
|
ChangeNotifierProvider<EstadoAlarmas>.value(
|
|
value: estadoAlarmas,
|
|
child: testApp(const PantallaAlarmas()),
|
|
),
|
|
);
|
|
await tester.pump();
|
|
|
|
expect(find.byType(Scaffold), findsNothing);
|
|
});
|
|
|
|
testWidgets('PantallaAjustes', (tester) async {
|
|
setLargeSurface(tester);
|
|
suppressListTileInkAssertion();
|
|
final estado = EstadoRadio(
|
|
esPremium: () => true,
|
|
audio: FakeServicioAudio(),
|
|
favoritos: FakeServicioFavoritos(),
|
|
radio: FakeServicioRadio(),
|
|
servicioEcualizador: FakeServicioEcualizador(),
|
|
servicioGrabacion: FakeServicioGrabacionRadioInactiva(),
|
|
resolverArchivoCustom: archivoCustomVacio,
|
|
iniciarAutomaticamente: false,
|
|
);
|
|
addTearDown(estado.dispose);
|
|
final estadoIdioma = EstadoIdioma();
|
|
addTearDown(estadoIdioma.dispose);
|
|
|
|
await tester.pumpWidget(
|
|
MultiProvider(
|
|
providers: [
|
|
ChangeNotifierProvider<EstadoRadio>.value(value: estado),
|
|
ListenableProvider<EstadoEcualizador>.value(
|
|
value: estado.ecualizador,
|
|
),
|
|
ListenableProvider<EstadoGrabacion>.value(value: estado.grabacion),
|
|
ChangeNotifierProvider<EstadoIdioma>.value(value: estadoIdioma),
|
|
ChangeNotifierProvider<EstadoEntitlement>(
|
|
create: (_) => EstadoEntitlement(prefs: null),
|
|
),
|
|
],
|
|
child: testApp(const PantallaAjustes()),
|
|
),
|
|
);
|
|
await tester.pump();
|
|
|
|
expect(find.byType(Scaffold), findsNothing);
|
|
});
|
|
});
|
|
|
|
group('PluriPushScaffold', () {
|
|
testWidgets('renders exactly one 56px AppBar with a back affordance', (
|
|
tester,
|
|
) async {
|
|
await tester.pumpWidget(
|
|
MaterialApp(
|
|
localizationsDelegates: AppLocalizations.localizationsDelegates,
|
|
supportedLocales: AppLocalizations.supportedLocales,
|
|
home: Navigator(
|
|
onGenerateRoute:
|
|
(settings) => MaterialPageRoute<void>(
|
|
builder:
|
|
(_) => const PluriPushScaffold(
|
|
title: 'Detalle',
|
|
body: SizedBox.shrink(),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
);
|
|
await tester.pumpAndSettle();
|
|
|
|
expect(find.byType(Scaffold), findsOneWidget);
|
|
expect(find.byType(AppBar), findsOneWidget);
|
|
final appBar = tester.widget<AppBar>(find.byType(AppBar));
|
|
expect(appBar.toolbarHeight, PluriPushScaffold.headerHeight);
|
|
expect(PluriPushScaffold.headerHeight, 56);
|
|
expect(find.byIcon(Icons.arrow_back_rounded), findsOneWidget);
|
|
expect(find.text('Detalle'), findsOneWidget);
|
|
});
|
|
|
|
// Design ADR-2's load-bearing contract: PluriPushScaffold has NO
|
|
// bottomNavigationBar parameter. This is a type-system guarantee, not a
|
|
// runtime one — passing `bottomNavigationBar: ...` below would simply
|
|
// fail to compile. There is nothing to assert dynamically; the absence
|
|
// of that parameter IS the enforcement.
|
|
|
|
testWidgets('push navigates via Navigator.push (route depth increases)', (
|
|
tester,
|
|
) async {
|
|
await tester.pumpWidget(
|
|
MaterialApp(
|
|
localizationsDelegates: AppLocalizations.localizationsDelegates,
|
|
supportedLocales: AppLocalizations.supportedLocales,
|
|
home: Builder(
|
|
builder:
|
|
(context) => Material(
|
|
child: Center(
|
|
child: ElevatedButton(
|
|
onPressed:
|
|
() => PluriPushScaffold.push(
|
|
context,
|
|
(_) => const PluriPushScaffold(
|
|
title: 'Empujada',
|
|
body: SizedBox.shrink(),
|
|
),
|
|
),
|
|
child: const Text('abrir'),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
);
|
|
|
|
expect(find.byType(PluriPushScaffold), findsNothing);
|
|
await tester.tap(find.text('abrir'));
|
|
await tester.pumpAndSettle();
|
|
|
|
expect(find.byType(PluriPushScaffold), findsOneWidget);
|
|
expect(find.text('Empujada'), findsOneWidget);
|
|
});
|
|
});
|
|
}
|