Audit 11.1 (t4:566): the master enable switch moves to PluriPushScaffold's header actions -- was the body's first SwitchListTile row. Its realtime/pending explainer subtitle stays behind as a plain caption so no information is lost. Audit 11.2 (t4:571): explainer banner radius is 16 (a local one-off, matching neither of the 3 named tokens), not radiusSm's 14. Audit 11.3 (t4:574-577): preset chips are solid brand-teal with dark text when active, listSurface + a faint border when not -- was Material's own ChoiceChip theming (primaryContainer/grey). Audit 11.5 (t4:585): band sliders use a new 20x20 _GlowSliderThumbShape (brand-teal blurred glow + solid thumb), replacing the Material default round thumb. Audit 11.7 (t4:584): the dB label is brand teal at 90% alpha, not liveGreen -- a leftover wrong colour family 11.6's slider-only fix never touched. Updated 2 pre-existing tests (pantalla_ajustes_ecualizador_test.dart, pantalla_ajustes_test.dart) to locate the enable switch by key instead of by the "Enable equalizer" text it no longer renders next to.
332 lines
12 KiB
Dart
332 lines
12 KiB
Dart
import 'dart:async';
|
|
import 'dart:io';
|
|
|
|
import 'package:flutter/material.dart';
|
|
import 'package:flutter_test/flutter_test.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/servicios/servicio_grabacion_radio.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';
|
|
|
|
// Pre-existing project constraint: PluriGlassSurface (a glassmorphism
|
|
// DecoratedBox) is used as the card-style container throughout PantallaAjustes.
|
|
// Flutter asserts that ListTile's ink is visible, but the assertion fires
|
|
// as a warning (not a correctness bug) — ink animations are simply not visible
|
|
// behind the backdrop-filter blur in production either. We suppress it here so
|
|
// functional tests can run against the existing UI structure.
|
|
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({});
|
|
});
|
|
|
|
// ── Helpers ────────────────────────────────────────────────────────────────
|
|
|
|
Widget buildAjustes(EstadoRadio estado, {EstadoIdioma? idioma}) {
|
|
final estadoIdioma = idioma ?? EstadoIdioma();
|
|
return MultiProvider(
|
|
providers: [
|
|
ChangeNotifierProvider<EstadoRadio>.value(value: estado),
|
|
ListenableProvider<EstadoEcualizador>.value(value: estado.ecualizador),
|
|
ListenableProvider<EstadoGrabacion>.value(value: estado.grabacion),
|
|
ChangeNotifierProvider<EstadoIdioma>.value(value: estadoIdioma),
|
|
],
|
|
child: MaterialApp(
|
|
locale: const Locale('en'),
|
|
localizationsDelegates: AppLocalizations.localizationsDelegates,
|
|
supportedLocales: AppLocalizations.supportedLocales,
|
|
home: const Scaffold(body: PantallaAjustes()),
|
|
),
|
|
);
|
|
}
|
|
|
|
Future<EstadoRadio> crearEstado() async {
|
|
final estado = EstadoRadio(
|
|
audio: FakeServicioAudio(),
|
|
favoritos: FakeServicioFavoritos(),
|
|
radio: FakeServicioRadio(),
|
|
servicioEcualizador: FakeServicioEcualizador(),
|
|
servicioGrabacion: _FakeGrabacion(),
|
|
resolverArchivoCustom: _archivoCustomVacio,
|
|
iniciarAutomaticamente: false,
|
|
);
|
|
await estado.ecualizador.cargarPersistido();
|
|
return estado;
|
|
}
|
|
|
|
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.pumpAndSettle(const Duration(milliseconds: 100));
|
|
}
|
|
|
|
// ── WU3a: AUDIO + EMISORAS become grouped nav rows ─────────────────────────
|
|
//
|
|
// Design ADR-3: the root carries zero inline controls for the 7 sections
|
|
// WU3a moved (Ecualizador, Salida de audio, Temporizador de sueño, Grupos
|
|
// de favoritos, Emisora preferida, Emisoras personalizadas, Orden de
|
|
// listas) — each is reached through a FilaAjuste row instead. WU3b (below)
|
|
// completes the remaining 5 sections, so the root is now exactly 4
|
|
// GrupoAjustes cards, under 400 lines.
|
|
group('WU3a — AUDIO and EMISORAS groups', () {
|
|
testWidgets('AUDIO group renders exactly 3 nav rows, no inline controls', (
|
|
tester,
|
|
) async {
|
|
setLargeSurface(tester);
|
|
_suppressListTileInkAssertion();
|
|
final estado = await crearEstado();
|
|
addTearDown(estado.dispose);
|
|
|
|
await tester.pumpWidget(buildAjustes(estado));
|
|
await pumpStable(tester);
|
|
|
|
expect(find.text('AUDIO'), findsOneWidget);
|
|
expect(find.text('Equalizer'), findsOneWidget);
|
|
expect(find.text('Advanced Equalization Options'), findsOneWidget);
|
|
expect(find.text('Sleep timer'), findsOneWidget);
|
|
|
|
// Zero inline controls: the old always-visible enable switch and
|
|
// device-management toggle are gone from the root.
|
|
expect(find.text('Enable equalizer'), findsNothing);
|
|
expect(find.text('Enable per-device EQ'), findsNothing);
|
|
});
|
|
|
|
testWidgets(
|
|
'STATIONS group renders exactly 4 nav rows, no inline controls',
|
|
(tester) async {
|
|
setLargeSurface(tester);
|
|
_suppressListTileInkAssertion();
|
|
final estado = await crearEstado();
|
|
addTearDown(estado.dispose);
|
|
|
|
await tester.pumpWidget(buildAjustes(estado));
|
|
await pumpStable(tester);
|
|
|
|
expect(find.text('STATIONS'), findsOneWidget);
|
|
expect(find.text('Favorite lists'), findsOneWidget);
|
|
expect(find.text('Preferred station'), findsOneWidget);
|
|
expect(find.text('Custom stations'), findsOneWidget);
|
|
expect(find.text('Station order'), findsOneWidget);
|
|
|
|
// Zero inline controls: the descriptive body copy that used to sit
|
|
// directly under each header is gone from the root now. (A
|
|
// DropdownButtonFormField still exists on the page — Idioma's
|
|
// language picker, a section WU3b converts, not WU3a.)
|
|
expect(
|
|
find.text(
|
|
'Preselected for new alarms and available for quick playback.',
|
|
),
|
|
findsNothing,
|
|
);
|
|
},
|
|
);
|
|
|
|
testWidgets('tapping the Ecualizador row pushes its detail screen', (
|
|
tester,
|
|
) async {
|
|
setLargeSurface(tester);
|
|
_suppressListTileInkAssertion();
|
|
final estado = await crearEstado();
|
|
addTearDown(estado.dispose);
|
|
|
|
await tester.pumpWidget(buildAjustes(estado));
|
|
await pumpStable(tester);
|
|
|
|
await tester.tap(find.text('Equalizer'));
|
|
await pumpStable(tester);
|
|
|
|
// Pushed, not index-switched: exactly one PluriPushScaffold now exists,
|
|
// and its moved control (the enable switch, audit 11.1 -- now a
|
|
// header action, t4 line 566) is reachable.
|
|
expect(find.byType(PluriPushScaffold), findsOneWidget);
|
|
expect(find.byKey(const ValueKey('eq-master-switch')), findsOneWidget);
|
|
});
|
|
|
|
testWidgets('tapping the Orden de listas row pushes its detail screen', (
|
|
tester,
|
|
) async {
|
|
setLargeSurface(tester);
|
|
_suppressListTileInkAssertion();
|
|
final estado = await crearEstado();
|
|
addTearDown(estado.dispose);
|
|
|
|
await tester.pumpWidget(buildAjustes(estado));
|
|
await pumpStable(tester);
|
|
|
|
await tester.tap(find.text('Station order'));
|
|
await pumpStable(tester);
|
|
|
|
expect(find.byType(PluriPushScaffold), findsOneWidget);
|
|
expect(find.text('By name'), findsOneWidget);
|
|
expect(find.text('By quality'), findsOneWidget);
|
|
});
|
|
});
|
|
|
|
// ── WU3b: GRABACIONES Y MÚSICA + APLICACIÓN become grouped nav rows ───────
|
|
//
|
|
// Design ADR-3: the root now carries zero inline controls for the final 5
|
|
// sections (Grabaciones, Música local, Idioma, Backup, Info) — each is
|
|
// reached through a FilaAjuste row, matching WU3a's AUDIO/EMISORAS
|
|
// treatment. All 12 sections are decomposed now; the root is exactly 4
|
|
// GrupoAjustes cards and crosses under 400 lines (tasks.md 3b.5). The two
|
|
// "Phase 7" friendly-folder-name scenarios that used to live in this file
|
|
// are relocated verbatim to `ajustes/pantalla_ajustes_musica_local_test.dart`,
|
|
// now targeting the isolated pushed screen directly.
|
|
group('WU3b — GRABACIONES Y MÚSICA and APLICACIÓN groups', () {
|
|
testWidgets(
|
|
'RECORDINGS & MUSIC group renders exactly 2 nav rows, no inline '
|
|
'controls',
|
|
(tester) async {
|
|
setLargeSurface(tester);
|
|
_suppressListTileInkAssertion();
|
|
final estado = await crearEstado();
|
|
addTearDown(estado.dispose);
|
|
|
|
await tester.pumpWidget(buildAjustes(estado));
|
|
await pumpStable(tester);
|
|
|
|
expect(find.text('RECORDINGS & MUSIC'), findsOneWidget);
|
|
expect(find.text('Recordings'), findsOneWidget);
|
|
expect(find.text('Local music (Android Auto)'), findsOneWidget);
|
|
|
|
// Zero inline controls: the folder-path row, path action buttons and
|
|
// max-size row are gone from the root now.
|
|
expect(find.text('Change path'), findsNothing);
|
|
expect(find.text('Maximum recording size'), findsNothing);
|
|
},
|
|
);
|
|
|
|
testWidgets(
|
|
'APPLICATION group renders exactly 3 nav rows, no inline controls',
|
|
(tester) async {
|
|
setLargeSurface(tester);
|
|
_suppressListTileInkAssertion();
|
|
final estado = await crearEstado();
|
|
addTearDown(estado.dispose);
|
|
|
|
await tester.pumpWidget(buildAjustes(estado));
|
|
await pumpStable(tester);
|
|
|
|
expect(find.text('APPLICATION'), findsOneWidget);
|
|
// "Language" now renders exactly once at the root (the FilaAjuste
|
|
// row only) — the old inline dropdown's duplicate label is gone.
|
|
expect(find.text('Language'), findsOneWidget);
|
|
expect(find.text('Backup'), findsOneWidget);
|
|
expect(find.text('Info'), findsOneWidget);
|
|
|
|
// Zero inline controls: the language dropdown and export/import rows
|
|
// are gone from the root now.
|
|
expect(find.byType(DropdownButtonFormField<String>), findsNothing);
|
|
expect(find.text('Export configuration'), findsNothing);
|
|
},
|
|
);
|
|
|
|
testWidgets(
|
|
'tapping the Grabaciones row pushes the recordings LIBRARY, not the '
|
|
'folder/size settings form (WU15b — the approved mockup screen '
|
|
'"Ajustes > Grabaciones" depicts the library)',
|
|
(tester) async {
|
|
setLargeSurface(tester);
|
|
_suppressListTileInkAssertion();
|
|
final estado = await crearEstado();
|
|
addTearDown(estado.dispose);
|
|
|
|
await tester.pumpWidget(buildAjustes(estado));
|
|
await pumpStable(tester);
|
|
|
|
await tester.tap(find.text('Recordings'));
|
|
await pumpStable(tester);
|
|
|
|
expect(find.byType(PluriPushScaffold), findsOneWidget);
|
|
expect(find.text('My recordings'), findsOneWidget);
|
|
// The folder/size settings form is reachable FROM the library now,
|
|
// not directly from the Settings root row — see
|
|
// pantalla_grabaciones_test.dart for that entry point.
|
|
expect(find.text('Change path'), findsNothing);
|
|
},
|
|
);
|
|
|
|
testWidgets('tapping the Info row pushes its detail screen', (
|
|
tester,
|
|
) async {
|
|
setLargeSurface(tester);
|
|
_suppressListTileInkAssertion();
|
|
final estado = await crearEstado();
|
|
addTearDown(estado.dispose);
|
|
|
|
await tester.pumpWidget(buildAjustes(estado));
|
|
await pumpStable(tester);
|
|
|
|
await tester.tap(find.text('Info'));
|
|
await pumpStable(tester);
|
|
|
|
expect(find.byType(PluriPushScaffold), findsOneWidget);
|
|
expect(find.text('Help and tutorial'), findsOneWidget);
|
|
});
|
|
|
|
testWidgets('root now contains exactly 4 GrupoAjustes cards', (
|
|
tester,
|
|
) async {
|
|
setLargeSurface(tester);
|
|
_suppressListTileInkAssertion();
|
|
final estado = await crearEstado();
|
|
addTearDown(estado.dispose);
|
|
|
|
await tester.pumpWidget(buildAjustes(estado));
|
|
await pumpStable(tester);
|
|
|
|
expect(find.text('AUDIO'), findsOneWidget);
|
|
expect(find.text('STATIONS'), findsOneWidget);
|
|
expect(find.text('RECORDINGS & MUSIC'), findsOneWidget);
|
|
expect(find.text('APPLICATION'), findsOneWidget);
|
|
});
|
|
});
|
|
}
|
|
|
|
// ── Infrastructure ──────────────────────────────────────────────────────────
|
|
|
|
class _FakeGrabacion extends ServicioGrabacionRadio {
|
|
final _controller = StreamController<EstadoGrabacionRadio>.broadcast();
|
|
|
|
@override
|
|
EstadoGrabacionRadio get estado => const EstadoGrabacionRadio.inactiva();
|
|
|
|
@override
|
|
Stream<EstadoGrabacionRadio> get estadoStream => _controller.stream;
|
|
|
|
@override
|
|
Future<void> inicializar() async {}
|
|
|
|
@override
|
|
Future<void> dispose() => _controller.close();
|
|
}
|
|
|
|
Future<File> _archivoCustomVacio() async =>
|
|
File('${Directory.current.path}/test/fixtures/emisoras_custom_vacio.json');
|