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 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( audio: FakeServicioAudio(), favoritos: FakeServicioFavoritos(), radio: FakeServicioRadio(), servicioEcualizador: FakeServicioEcualizador(), servicioGrabacion: FakeServicioGrabacionRadioInactiva(), resolverArchivoCustom: archivoCustomVacio, iniciarAutomaticamente: false, ); addTearDown(estado.dispose); await tester.pumpWidget( MultiProvider( providers: [ ChangeNotifierProvider.value(value: estado), ListenableProvider.value( value: estado.ecualizador, ), ListenableProvider.value(value: estado.grabacion), ListenableProvider.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( audio: FakeServicioAudio(), favoritos: FakeServicioFavoritos(), radio: FakeServicioRadio(), servicioEcualizador: FakeServicioEcualizador(), servicioGrabacion: FakeServicioGrabacionRadioInactiva(), resolverArchivoCustom: archivoCustomVacio, iniciarAutomaticamente: false, ); addTearDown(estado.dispose); await tester.pumpWidget( MultiProvider( providers: [ ChangeNotifierProvider.value(value: estado), ListenableProvider.value( value: estado.ecualizador, ), ListenableProvider.value(value: estado.grabacion), ListenableProvider.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( audio: FakeServicioAudio(), favoritos: FakeServicioFavoritos(), radio: FakeServicioRadio(), servicioEcualizador: FakeServicioEcualizador(), servicioGrabacion: FakeServicioGrabacionRadioInactiva(), resolverArchivoCustom: archivoCustomVacio, iniciarAutomaticamente: false, ); addTearDown(estado.dispose); await tester.pumpWidget( ChangeNotifierProvider.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( servicio: ServicioAlarmas(reloj: DateTime.now), android: android, iniciarAutomaticamente: false, ); addTearDown(estadoAlarmas.dispose); addTearDown(android.dispose); await tester.pumpWidget( ChangeNotifierProvider.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( 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.value(value: estado), ListenableProvider.value( value: estado.ecualizador, ), ListenableProvider.value(value: estado.grabacion), ChangeNotifierProvider.value(value: estadoIdioma), ChangeNotifierProvider( 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( 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(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); }); }); }