Moves the AUDIO group (Ecualizador, Salida de audio, Temporizador de
sueno) and the EMISORAS group (Grupos de favoritos, Emisora preferida,
Emisoras personalizadas, Orden de listas) out of pantalla_ajustes.dart
into 7 new lib/pantallas/ajustes/*.dart screens, each wrapped in
PluriPushScaffold. The root now reaches them through FilaAjuste rows
under two new GrupoAjustes cards (lib/pantallas/ajustes/widgets/
fila_ajuste.dart), per design ADR-3.
Verbatim-move rule applied throughout: only each section's panel header
(icon + title, sometimes a status chip) was removed, since the pushed
screen's own 56px header now carries the title. Two sections whose
header row carried a real action (Temporizador de sueno's "Add",
Grupos de favoritos' "Add list", Emisoras personalizadas' "Add") kept
that action in the body instead of dropping it.
size:exception (move-only diff, pre-recorded at design/tasks time):
34 files, ~4250 changed lines excluding the 13 auto-regenerated l10n
files (~90 more lines there) - higher than the 800-1000 estimate
because that estimate covered the 7 production screens but not the
matching 7 new test files (task 3a.2), one of which relocates ~10
pre-existing device-management test cases verbatim. Business logic is
untouched; app.dart's import of pantalla_ajustes.dart is unchanged.
Correction to tasks.md 3a.1/3a.8: those two lines describe the combined
WU3a+WU3b end state ("4 grouped nav lists", "<400 lines"), matching
design ADR-3's own aggregate blast-radius note - not a WU3a-only claim.
This commit converts only the 2 groups that are WU3a's job; the root
is 788 lines with 5 sections (Grabaciones, Musica local, Idioma,
Backup, Info) still inline, reachable, and unchanged, pending WU3b.
Two new ARB keys (settingsGroupAudioTitle, settingsGroupStationsTitle),
en/es only per the WU1 precedent - all 7 detail-screen titles reuse
existing keys. Discovered and worked around, without touching app
code: Directory.systemTemp hangs real dart:io writes in this sandbox,
and pumpAndSettle() cannot settle while a screen shows an indeterminate
CircularProgressIndicator - both are test-only concerns, documented
inline where hit.
Tests: 560 -> 579 (32 in this commit's scope, net +19 after retiring
13 relocated cases from the old combined pantalla_ajustes_test.dart).
flutter analyze: unchanged at 1 pre-existing info. git diff is empty
for navegacion_auto.dart, servicio_ecualizador.dart and
servicio_audio.dart; pantalla_alarma_sonando_dismiss_guard_test.dart
untouched.
290 lines
11 KiB
Dart
290 lines
11 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 now 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. The other 5
|
|
// sections (Grabaciones, Música local, Idioma, Backup, Info) still render
|
|
// inline here: WU3b decomposes GRABACIONES Y MÚSICA / APLICACIÓN the same
|
|
// way, so the root is not yet under 400 lines nor fully "zero inline
|
|
// controls" — that end state is WU3b's completion, not WU3a's (see the
|
|
// apply-progress note on this discrepancy in tasks.md 3a.1/3a.8).
|
|
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) is reachable.
|
|
expect(find.byType(PluriPushScaffold), findsOneWidget);
|
|
expect(find.text('Enable equalizer'), 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);
|
|
});
|
|
});
|
|
|
|
// ── Sections not yet converted (WU3b's job) stay reachable ─────────────────
|
|
group('Sections pending WU3b remain inline and reachable', () {
|
|
testWidgets('Grabaciones, Idioma, Backup and Info still render', (
|
|
tester,
|
|
) async {
|
|
setLargeSurface(tester);
|
|
_suppressListTileInkAssertion();
|
|
final estado = await crearEstado();
|
|
addTearDown(estado.dispose);
|
|
|
|
await tester.pumpWidget(buildAjustes(estado));
|
|
await pumpStable(tester);
|
|
|
|
expect(find.text('Recordings'), findsOneWidget);
|
|
// "Language" legitimately renders twice (pre-existing, unmodified by
|
|
// WU3a): the section header AND the dropdown's own label share the
|
|
// same l10n string.
|
|
expect(find.text('Language'), findsWidgets);
|
|
expect(find.text('Backup'), findsOneWidget);
|
|
expect(find.text('Help and tutorial'), findsOneWidget);
|
|
});
|
|
});
|
|
|
|
// ── android-auto-local-music-paging Phase 7: friendly folder name ────────
|
|
group('_SeccionMusicaLocal — friendly folder name (Phase 7)', () {
|
|
testWidgets(
|
|
'7.1-A: carpeta configurada muestra el nombre amigable derivado de '
|
|
'la URI, nunca la URI cruda',
|
|
(tester) async {
|
|
SharedPreferences.setMockInitialValues({
|
|
'musica_local_uri':
|
|
'content://com.android.externalstorage.documents/tree/'
|
|
'primary%3AMusic%2FMyFolder',
|
|
});
|
|
setLargeSurface(tester);
|
|
_suppressListTileInkAssertion();
|
|
final estado = await crearEstado();
|
|
addTearDown(estado.dispose);
|
|
|
|
await tester.pumpWidget(buildAjustes(estado));
|
|
await pumpStable(tester);
|
|
|
|
await tester.scrollUntilVisible(
|
|
find.text('Local music folder'),
|
|
300,
|
|
scrollable: find.byType(Scrollable).first,
|
|
);
|
|
await pumpStable(tester);
|
|
|
|
expect(find.text('MyFolder'), findsOneWidget);
|
|
expect(find.textContaining('content://'), findsNothing);
|
|
},
|
|
);
|
|
|
|
testWidgets('7.1-B: sin carpeta configurada mantiene el mensaje '
|
|
'localMusicFolderNotConfigured', (tester) async {
|
|
SharedPreferences.setMockInitialValues({});
|
|
setLargeSurface(tester);
|
|
_suppressListTileInkAssertion();
|
|
final estado = await crearEstado();
|
|
addTearDown(estado.dispose);
|
|
|
|
await tester.pumpWidget(buildAjustes(estado));
|
|
await pumpStable(tester);
|
|
|
|
await tester.scrollUntilVisible(
|
|
find.text('Local music folder'),
|
|
300,
|
|
scrollable: find.byType(Scrollable).first,
|
|
);
|
|
await pumpStable(tester);
|
|
|
|
expect(find.text('No folder selected'), 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');
|