Files
pluriwave/test/pantallas/pantalla_ajustes_row_values_test.dart
FreeTLab d81fabbe27 refactor(iap): make esPremium a required constructor parameter
EstadoAlarmas, EstadoGrabacion and EstadoRadio defaulted `esPremium` to
`() => true`, so any construction site that forgot to wire entitlement
compiled fine and silently ran ungated — failing OPEN to premium and
disabling the paywall with no test able to catch it.

The parameter is now required with no default. Production wiring in
app.dart was already correct and is unchanged; the 184 pre-existing test
call sites now pass `() => true` explicitly, which is exactly the old
implicit default, so every assertion is untouched.

EstadoRadio has no gate of its own but constructs EstadoGrabacion, so it
inherits the same contract.

The one test that existed to pin the old default is renamed to describe
what it still covers (the premium path through iniciar() with no
duracion); its assertions are unchanged.
2026-08-10 22:06:36 +02:00

180 lines
6.7 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_entitlement.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<EstadoGrabacionRadio>.broadcast();
@override
EstadoGrabacionRadio get estado => const EstadoGrabacionRadio.inactiva();
@override
Stream<EstadoGrabacionRadio> get estadoStream => _controller.stream;
@override
Future<void> inicializar() async {}
@override
Future<List<ArchivoGrabacion>> 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<void> dispose() => _controller.close();
}
void main() {
setUp(() {
SharedPreferences.setMockInitialValues({});
});
Future<File> 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<void> pumpStable(WidgetTester tester) async {
await tester.pump();
await tester.pump(const Duration(milliseconds: 100));
}
Widget buildAjustes(EstadoRadio estado, EstadoIdioma idioma) {
return MultiProvider(
providers: [
ChangeNotifierProvider<EstadoRadio>.value(value: estado),
ListenableProvider<EstadoEcualizador>.value(value: estado.ecualizador),
ListenableProvider<EstadoGrabacion>.value(value: estado.grabacion),
ChangeNotifierProvider<EstadoIdioma>.value(value: idioma),
ChangeNotifierProvider<EstadoEntitlement>(
create: (_) => EstadoEntitlement(prefs: null),
),
],
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(
esPremium: () => true,
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);
});
}