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_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_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 archivoCustomVacio() async => File( '${Directory.current.path}/test/fixtures/emisoras_custom_vacio.json', ); EstadoRadio crearEstadoRadio() => EstadoRadio( 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.value(value: estado), ListenableProvider.value(value: estado.ecualizador), ListenableProvider.value(value: estado.grabacion), ListenableProvider.value(value: estado.busqueda), ChangeNotifierProvider.value(value: EstadoIdioma()), if (alarmas != null) ChangeNotifierProvider.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 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( 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, ); }, ); }