fix(chrome): drop the global AppBar, give each root its own title row
The prototype (t4) draws no global app bar anywhere: every root paints a plain ~56px title row inside its own content instead (Alarmas line 325, Ajustes line 511, Explorar line 641). app.dart wrapped every tab in PluriWaveScaffold(appBar: AppBar(title: Text(appTitle), ...)), adding 56dp of chrome and a "PluriWave" title the prototype never shows. Add PluriRootHeader, a shared 56px title-row widget reused by all 5 roots. Extract app.dart's old _mostrarTimerDialog (only reachable from the removed AppBar action) into a free function, showPluriSleepTimerSheet, so every root's header can open the same sheet directly and the sleep-timer feature stays reachable from every tab with no behaviour change. S1, Tier 1 visual-fidelity pass (audit id 2521).
This commit is contained in:
@@ -0,0 +1,208 @@
|
||||
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_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`). Each root still ALSO carries its pre-existing
|
||||
/// `PluriScreenHeader` hero (S2 in this same Tier 1 batch replaces that
|
||||
/// separately), so title assertions below are scoped to [PluriRootHeader]'s
|
||||
/// own subtree — the title string itself still appears twice on screen
|
||||
/// until S2 lands.
|
||||
///
|
||||
/// 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(
|
||||
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),
|
||||
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 draws its own PluriRootHeader with the tab title, no AppBar',
|
||||
(tester) async {
|
||||
_suppressListTileInkAssertion();
|
||||
setLargeSurface(tester);
|
||||
final estado = crearEstadoRadio();
|
||||
addTearDown(estado.dispose);
|
||||
|
||||
await tester.pumpWidget(testApp(estado, const PantallaInicio()));
|
||||
await pumpStable(tester);
|
||||
|
||||
expect(find.byType(PluriRootHeader), findsOneWidget);
|
||||
expect(titleInHeader('Listen'), findsOneWidget);
|
||||
expect(find.byType(AppBar), findsNothing);
|
||||
},
|
||||
);
|
||||
|
||||
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,
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user