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/modelos/archivo_grabacion.dart'; import 'package:pluriwave/pantallas/pantalla_ajustes.dart'; import 'package:pluriwave/servicios/servicio_grabacion_radio.dart'; import 'package:provider/provider.dart'; import 'package:shared_preferences/shared_preferences.dart'; import '../helpers/fakes.dart'; /// S8 (Tier 1 visual fidelity): the prototype puts a trailing "current /// value" on nearly every settings row (t4 lines 512-539, 625 — e.g. /// "3 guardados", "Alfabético", "Español", "7 · 84 MB"). Wires 8 of the 12 /// built rows to real, already-available state. The other 4 (Salida de /// audio, Música local, Backup, Info's version) are deliberately left /// without a value — see the apply-progress note for why each lacks a /// low-risk, deterministically-testable data source. /// /// 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); } /// Returns a fixed, in-memory recordings list — `listarGrabaciones()`'s real /// implementation touches the filesystem directly (`Directory.listSync`), /// which the project's own convention forbids exercising bare in a widget /// test. class _FakeServicioGrabacionConArchivos extends ServicioGrabacionRadio { final _controller = StreamController.broadcast(); @override EstadoGrabacionRadio get estado => const EstadoGrabacionRadio.inactiva(); @override Stream get estadoStream => _controller.stream; @override Future inicializar() async {} @override Future> listarGrabaciones() async => [ ArchivoGrabacion( ruta: '/a.m4a', nombre: 'a', fecha: DateTime(2026, 1, 1), tamanoBytes: 2 * 1024 * 1024, ), ArchivoGrabacion( ruta: '/b.m4a', nombre: 'b', fecha: DateTime(2026, 1, 2), tamanoBytes: 5 * 1024 * 1024, ), ]; @override Future dispose() => _controller.close(); } void main() { setUp(() { SharedPreferences.setMockInitialValues({}); }); Future archivoCustomVacio() async => File( '${Directory.current.path}/test/fixtures/emisoras_custom_vacio.json', ); 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)); } Widget buildAjustes(EstadoRadio estado, EstadoIdioma idioma) { return MultiProvider( providers: [ ChangeNotifierProvider.value(value: estado), ListenableProvider.value(value: estado.ecualizador), ListenableProvider.value(value: estado.grabacion), ChangeNotifierProvider.value(value: idioma), ], child: MaterialApp( locale: const Locale('en'), localizationsDelegates: AppLocalizations.localizationsDelegates, supportedLocales: AppLocalizations.supportedLocales, home: const Scaffold(body: PantallaAjustes()), ), ); } testWidgets('settings rows show their current value: EQ on, favourite groups ' 'count, preferred station name, custom stations count, sort order, ' 'recordings count · size, and the current language', (tester) async { setLargeSurface(tester); _suppressListTileInkAssertion(); final estado = EstadoRadio( audio: FakeServicioAudio(), favoritos: FakeServicioFavoritos(), radio: FakeServicioRadio(), servicioEcualizador: FakeServicioEcualizador(), servicioGrabacion: _FakeServicioGrabacionConArchivos(), resolverArchivoCustom: archivoCustomVacio, iniciarAutomaticamente: false, ); addTearDown(estado.dispose); await estado.ecualizador.cargarPersistido(); await estado.ecualizador.cambiarActivo(true); final favoritos = estado.favoritos as FakeServicioFavoritos; await favoritos.crearGrupo('Rock'); await favoritos.crearGrupo('Jazz'); await estado.cargarGruposFavoritos(); final preferida = emisoraDemo(uuid: 'pref-1', nombre: 'Radio Horizonte'); await favoritos.agregar(preferida); await estado.cargarFavoritos(); await estado.cambiarEmisoraPreferida(preferida); await estado.ordenarFavoritos(OrdenEmisoras.calidad); final idioma = EstadoIdioma(); // EstadoIdioma's constructor kicks off its own async `_cargar()` read // from SharedPreferences; without waiting for it to settle first, it // can resolve AFTER `seleccionarLocale` below and clobber the // selection back to null (a real race, not a test flake). `tester. // pump()`, NOT a bare `Future.delayed` — a real Timer/delay never // fires inside `testWidgets`' fake-async zone without something // driving fake time forward, and hangs the whole test. await tester.pump(); await idioma.seleccionarLocale(const Locale('en')); await tester.pumpWidget(buildAjustes(estado, idioma)); await pumpStable(tester); // Lets the recordings FutureBuilder resolve. await tester.pump(); final l10n = await AppLocalizations.delegate.load(const Locale('en')); expect(find.text(l10n.equalizerActive), findsOneWidget); // 3, not 2 — FakeServicioFavoritos seeds a protected "unassigned" // group by default, on top of the 2 this test creates. expect(find.text('3'), findsOneWidget); // favourite groups expect(find.text('Radio Horizonte'), findsOneWidget); expect(find.text(l10n.stationOrderByQuality), findsOneWidget); expect(find.text('2 · 7 MB'), findsOneWidget); expect(find.text('English'), findsOneWidget); }); }