Fix multi-device EQ auto-switching by calling obtenerDispositivoActual() during cargarPersistido() to seed the initial device ID. Add device management modal with rename support, EQ preset editing, and connection status indicator. Translate device UI keys to all 13 locales.
405 lines
14 KiB
Dart
405 lines
14 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/modelos/dispositivo_audio.dart';
|
|
import 'package:pluriwave/modelos/preset_ecualizador.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';
|
|
|
|
// 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({
|
|
bool eqMultiDeviceEnabled = false,
|
|
Map<String, PresetEcualizador> presetsDispositivo = const {},
|
|
}) async {
|
|
final estado = EstadoRadio(
|
|
audio: FakeServicioAudio(),
|
|
favoritos: FakeServicioFavoritos(),
|
|
radio: FakeServicioRadio(),
|
|
servicioEcualizador: FakeServicioEcualizador(
|
|
eqMultiDeviceEnabled: eqMultiDeviceEnabled,
|
|
presetsDispositivo: presetsDispositivo,
|
|
),
|
|
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));
|
|
}
|
|
|
|
// Also update crearEstado to support nombresDispositivos
|
|
Future<EstadoRadio> crearEstadoConNombres({
|
|
bool eqMultiDeviceEnabled = true,
|
|
Map<String, PresetEcualizador>? presetsDispositivo,
|
|
Map<String, String>? nombresDispositivos,
|
|
String? activeDeviceId,
|
|
}) async {
|
|
final fakeDispositivo = activeDeviceId != null
|
|
? (FakeServicioDispositivoAudio()
|
|
..emitirDispositivo(
|
|
DispositivoAudio(
|
|
id: activeDeviceId,
|
|
tipo: TipoDispositivo.bluetoothA2dp,
|
|
nombre: 'BT Speaker',
|
|
),
|
|
))
|
|
: null;
|
|
final estado = EstadoRadio(
|
|
audio: FakeServicioAudio(),
|
|
favoritos: FakeServicioFavoritos(),
|
|
radio: FakeServicioRadio(),
|
|
servicioEcualizador: FakeServicioEcualizador(
|
|
eqMultiDeviceEnabled: eqMultiDeviceEnabled,
|
|
presetsDispositivo: presetsDispositivo ??
|
|
{'bt_a2dp:AA:BB': PresetEcualizador.rock},
|
|
nombresDispositivos: nombresDispositivos ?? {},
|
|
),
|
|
servicioGrabacion: _FakeGrabacion(),
|
|
resolverArchivoCustom: _archivoCustomVacio,
|
|
iniciarAutomaticamente: false,
|
|
dispositivoAudio: fakeDispositivo,
|
|
);
|
|
await estado.ecualizador.cargarPersistido();
|
|
return estado;
|
|
}
|
|
|
|
// ── Phase 7 tests ──────────────────────────────────────────────────────────
|
|
|
|
group('_SeccionEcualizadorAvanzado (Phase 7)', () {
|
|
testWidgets('7.1-A: toggle OFF — advanced EQ section is visible but device '
|
|
'list is not shown', (tester) async {
|
|
setLargeSurface(tester);
|
|
_suppressListTileInkAssertion(); // Must be before pumpWidget.
|
|
final estado = await crearEstado(eqMultiDeviceEnabled: false);
|
|
addTearDown(estado.dispose);
|
|
|
|
await tester.pumpWidget(buildAjustes(estado));
|
|
await pumpStable(tester);
|
|
|
|
// Scroll to find the advanced EQ section.
|
|
await tester.scrollUntilVisible(
|
|
find.text('Advanced Equalization Options'),
|
|
300,
|
|
scrollable: find.byType(Scrollable).first,
|
|
);
|
|
await pumpStable(tester);
|
|
|
|
// The section header must be present.
|
|
expect(find.text('Advanced Equalization Options'), findsOneWidget);
|
|
|
|
// Toggle switch title must be present.
|
|
expect(find.text('Enable per-device EQ'), findsOneWidget);
|
|
|
|
// When toggle is OFF, device list must NOT be rendered.
|
|
expect(find.text('Known audio devices'), findsNothing);
|
|
});
|
|
|
|
testWidgets(
|
|
'7.1-B: toggle ON with known devices — device list is visible',
|
|
(tester) async {
|
|
setLargeSurface(tester);
|
|
_suppressListTileInkAssertion();
|
|
final estado = await crearEstado(
|
|
eqMultiDeviceEnabled: true,
|
|
presetsDispositivo: {
|
|
'bt_a2dp:AA:BB:CC:DD:EE:FF': PresetEcualizador.rock,
|
|
},
|
|
);
|
|
addTearDown(estado.dispose);
|
|
|
|
await tester.pumpWidget(buildAjustes(estado));
|
|
await pumpStable(tester);
|
|
|
|
await tester.scrollUntilVisible(
|
|
find.text('Advanced Equalization Options'),
|
|
300,
|
|
scrollable: find.byType(Scrollable).first,
|
|
);
|
|
await pumpStable(tester);
|
|
|
|
// Section header present.
|
|
expect(find.text('Advanced Equalization Options'), findsOneWidget);
|
|
|
|
// Toggle switch title present.
|
|
expect(find.text('Enable per-device EQ'), findsOneWidget);
|
|
|
|
// Known devices header should appear when toggle is on and there are
|
|
// known devices.
|
|
expect(find.text('Known audio devices'), findsOneWidget);
|
|
|
|
// The device ID should appear in the list.
|
|
expect(
|
|
find.textContaining('bt_a2dp:AA:BB:CC:DD:EE:FF'),
|
|
findsOneWidget,
|
|
);
|
|
},
|
|
);
|
|
|
|
testWidgets(
|
|
'7.1-C: toggle can be flipped — tapping it enables multi-device EQ',
|
|
(tester) async {
|
|
setLargeSurface(tester);
|
|
_suppressListTileInkAssertion();
|
|
final estado = await crearEstado(eqMultiDeviceEnabled: false);
|
|
addTearDown(estado.dispose);
|
|
|
|
await tester.pumpWidget(buildAjustes(estado));
|
|
await pumpStable(tester);
|
|
|
|
await tester.scrollUntilVisible(
|
|
find.text('Advanced Equalization Options'),
|
|
300,
|
|
scrollable: find.byType(Scrollable).first,
|
|
);
|
|
await pumpStable(tester);
|
|
|
|
// Initially OFF.
|
|
expect(estado.ecualizador.eqMultiDeviceEnabled, isFalse);
|
|
|
|
// Tap the Switch widget to toggle on.
|
|
await tester.tap(find.byType(Switch).last);
|
|
await pumpStable(tester);
|
|
|
|
// After tap, toggle should be ON.
|
|
expect(estado.ecualizador.eqMultiDeviceEnabled, isTrue);
|
|
},
|
|
);
|
|
});
|
|
|
|
// ── Phase 3 (eq-device-autoswitch-ux): connection indicator + modal ──────────
|
|
|
|
group('SeccionEcualizadorAvanzado connection indicator Phase 3', () {
|
|
// 3.1 RED — active device row shows green connection dot
|
|
testWidgets('3.1 active device row shows green connection indicator', (tester) async {
|
|
setLargeSurface(tester);
|
|
_suppressListTileInkAssertion();
|
|
const activeId = 'bt_a2dp:AA:BB';
|
|
final estado = await crearEstadoConNombres(
|
|
eqMultiDeviceEnabled: true,
|
|
presetsDispositivo: {
|
|
activeId: PresetEcualizador.rock,
|
|
'wired_headset': PresetEcualizador.jazz,
|
|
},
|
|
activeDeviceId: activeId,
|
|
);
|
|
addTearDown(estado.dispose);
|
|
|
|
await tester.pumpWidget(buildAjustes(estado));
|
|
await pumpStable(tester);
|
|
|
|
await tester.scrollUntilVisible(
|
|
find.text('Known audio devices'),
|
|
300,
|
|
scrollable: find.byType(Scrollable).first,
|
|
);
|
|
await pumpStable(tester);
|
|
|
|
// Green connection dot should be present (Icon with green color for active device)
|
|
final greenIcons = tester.widgetList<Icon>(find.byType(Icon)).where(
|
|
(icon) => icon.color == Colors.green,
|
|
);
|
|
// At least one green icon present
|
|
expect(greenIcons, isNotEmpty);
|
|
});
|
|
|
|
// 3.2 RED — tapping device row opens bottom sheet with TextField and EcualizadorWidget
|
|
testWidgets('3.2 tapping device row opens modal with TextField', (tester) async {
|
|
setLargeSurface(tester);
|
|
_suppressListTileInkAssertion();
|
|
final estado = await crearEstadoConNombres(
|
|
presetsDispositivo: {'bt_a2dp:AA:BB': PresetEcualizador.rock},
|
|
);
|
|
addTearDown(estado.dispose);
|
|
|
|
await tester.pumpWidget(buildAjustes(estado));
|
|
await pumpStable(tester);
|
|
|
|
await tester.scrollUntilVisible(
|
|
find.text('Known audio devices'),
|
|
300,
|
|
scrollable: find.byType(Scrollable).first,
|
|
);
|
|
await pumpStable(tester);
|
|
|
|
// Tap the device row (tap the edit icon or the row itself)
|
|
final editIcons = find.byIcon(Icons.edit_rounded);
|
|
expect(editIcons, findsWidgets);
|
|
await tester.tap(editIcons.first);
|
|
await pumpStable(tester);
|
|
|
|
// Bottom sheet should appear with a TextField
|
|
expect(find.byType(TextField), findsWidgets);
|
|
});
|
|
|
|
// 3.6 RED — renaming in modal and confirming calls renombrarDispositivo
|
|
testWidgets('3.6 confirming rename in modal updates device name', (tester) async {
|
|
setLargeSurface(tester);
|
|
_suppressListTileInkAssertion();
|
|
const deviceId = 'bt_a2dp:AA:BB';
|
|
final estado = await crearEstadoConNombres(
|
|
presetsDispositivo: {deviceId: PresetEcualizador.rock},
|
|
);
|
|
addTearDown(estado.dispose);
|
|
|
|
await tester.pumpWidget(buildAjustes(estado));
|
|
await pumpStable(tester);
|
|
|
|
await tester.scrollUntilVisible(
|
|
find.text('Known audio devices'),
|
|
300,
|
|
scrollable: find.byType(Scrollable).first,
|
|
);
|
|
await pumpStable(tester);
|
|
|
|
// Open modal
|
|
await tester.tap(find.byIcon(Icons.edit_rounded).first);
|
|
await pumpStable(tester);
|
|
|
|
// Enter a new name
|
|
final textField = find.byType(TextField).first;
|
|
await tester.enterText(textField, 'My Living Room Speaker');
|
|
await pumpStable(tester);
|
|
|
|
// Tap confirm/save button
|
|
final saveButton = find.byIcon(Icons.save_rounded);
|
|
expect(saveButton, findsWidgets);
|
|
await tester.tap(saveButton.first);
|
|
await pumpStable(tester);
|
|
|
|
// Device should now have the new name
|
|
expect(
|
|
estado.ecualizador.obtenerNombreDispositivo(deviceId),
|
|
equals('My Living Room Speaker'),
|
|
);
|
|
});
|
|
|
|
// 3.7 RED — dismissing modal without confirming leaves name unchanged
|
|
testWidgets('3.7 dismissing modal without confirming leaves name unchanged', (tester) async {
|
|
setLargeSurface(tester);
|
|
_suppressListTileInkAssertion();
|
|
const deviceId = 'bt_a2dp:AA:BB';
|
|
final estado = await crearEstadoConNombres(
|
|
presetsDispositivo: {deviceId: PresetEcualizador.rock},
|
|
nombresDispositivos: {deviceId: 'Original Name'},
|
|
);
|
|
addTearDown(estado.dispose);
|
|
|
|
await tester.pumpWidget(buildAjustes(estado));
|
|
await pumpStable(tester);
|
|
|
|
await tester.scrollUntilVisible(
|
|
find.text('Known audio devices'),
|
|
300,
|
|
scrollable: find.byType(Scrollable).first,
|
|
);
|
|
await pumpStable(tester);
|
|
|
|
// Open modal
|
|
await tester.tap(find.byIcon(Icons.edit_rounded).first);
|
|
await pumpStable(tester);
|
|
|
|
// Change the text but do NOT confirm
|
|
final textField = find.byType(TextField).first;
|
|
await tester.enterText(textField, 'New Name Not Saved');
|
|
await pumpStable(tester);
|
|
|
|
// Dismiss by pressing back/escape
|
|
await tester.tapAt(const Offset(100, 100)); // tap outside bottom sheet
|
|
await pumpStable(tester);
|
|
|
|
// Name should remain unchanged
|
|
expect(
|
|
estado.ecualizador.obtenerNombreDispositivo(deviceId),
|
|
equals('Original Name'),
|
|
);
|
|
});
|
|
});
|
|
}
|
|
|
|
// ── 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');
|