deviceToMap handed the builtin_speaker id to EVERY output type its `when` did not name. A car stereo on LE Audio (TYPE_BLE_HEADSET) or an automotive bus (TYPE_BUS) therefore arrived in Dart under the phone speaker's own id, carrying a type that maps to `desconocido` -- which slipped past the type-only esBase guard and persisted a device entry keyed builtin_speaker. From that moment on, every playback through the phone's own speaker matched that entry, so the green active-output dot stayed pinned to whatever the user had renamed it to (a car, in the reported case) whether or not anything was connected. The dot was never wrong; the row was poisoned. Give unnamed output types their own `other:<type>:<address>` id namespace, and match esBase by id as well as by type so no future native regression can re-create the collision. A guarded one-time migration purges what the collision already persisted from all three device-keyed maps. Fix the ranking too: builtin_speaker sat inside the priority list as a peer, so any type absent from that list sorted BELOW the always-present speaker and could never win. The speaker is now the explicit last resort, externally connected outputs outrank it, and virtual or call-only sinks (earpiece, telephony, remote submix, SCO) are ranked below it so they can never be reported as where music is playing. Route every AudioDeviceInfo.getAddress read through a version-guarded helper. It is API 28 with minSdk 24, and two pre-existing unguarded calls in this same method were latent NoSuchMethodError crashes on Android 7-8.1. Android lint for :app goes from 8 errors to 6. Also lets the user manage the list, which is how they recover from a bad entry without waiting for a release: a remove action clears a device's preset, name and matrix entries, unnamed rows show their transport and address tail instead of a raw bt_a2dp:AA:BB:... id, and the green dot finally carries a tooltip and a semantics label saying what it means. Device QA pending for wired and USB outputs: no jack or adapter available to exercise those paths. Their detection is unchanged by this commit.
593 lines
21 KiB
Dart
593 lines
21 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 {},
|
|
FakeServicioDispositivoAudio? dispositivoAudio,
|
|
}) async {
|
|
final estado = EstadoRadio(
|
|
audio: FakeServicioAudio(),
|
|
favoritos: FakeServicioFavoritos(),
|
|
radio: FakeServicioRadio(),
|
|
servicioEcualizador: FakeServicioEcualizador(
|
|
eqMultiDeviceEnabled: eqMultiDeviceEnabled,
|
|
presetsDispositivo: presetsDispositivo,
|
|
),
|
|
servicioGrabacion: _FakeGrabacion(),
|
|
resolverArchivoCustom: _archivoCustomVacio,
|
|
iniciarAutomaticamente: false,
|
|
dispositivoAudio: dispositivoAudio,
|
|
);
|
|
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,
|
|
String nombrePlataforma = 'BT Speaker',
|
|
}) async {
|
|
final fakeDispositivo = activeDeviceId != null
|
|
? (FakeServicioDispositivoAudio()
|
|
..emitirDispositivo(
|
|
DispositivoAudio(
|
|
id: activeDeviceId,
|
|
tipo: TipoDispositivo.bluetoothA2dp,
|
|
nombre: nombrePlataforma,
|
|
),
|
|
))
|
|
: 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 row is listed. An unnamed device shows its transport plus
|
|
// the tail of its address, not the raw id.
|
|
expect(find.text('Bluetooth · 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'),
|
|
);
|
|
});
|
|
});
|
|
|
|
// ── bt-device-identity Phase 4: display fix + permission trigger ─────────
|
|
|
|
group('_SeccionEcualizadorAvanzado — bt-device-identity Phase 4', () {
|
|
// 4.1 — new behavior: the cached platform name (not '') now feeds
|
|
// nombreVisible, so a device with no custom rename shows its real name.
|
|
testWidgets('4.1 platform name displays with no custom rename', (
|
|
tester,
|
|
) async {
|
|
setLargeSurface(tester);
|
|
_suppressListTileInkAssertion();
|
|
const deviceId = 'bt_a2dp:AA:BB:CC:DD:EE:FF';
|
|
final estado = await crearEstadoConNombres(
|
|
presetsDispositivo: {deviceId: PresetEcualizador.rock},
|
|
activeDeviceId: deviceId,
|
|
nombrePlataforma: 'AirPods Pro',
|
|
);
|
|
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);
|
|
|
|
expect(find.text('AirPods Pro'), findsOneWidget);
|
|
expect(find.text(deviceId), findsNothing);
|
|
});
|
|
|
|
// 4.4 — triangulation companion: custom rename still wins even though
|
|
// the row now also has a cached platform name available.
|
|
testWidgets('4.4 custom rename overrides platform name', (tester) async {
|
|
setLargeSurface(tester);
|
|
_suppressListTileInkAssertion();
|
|
const deviceId = 'bt_a2dp:AA:BB:CC:DD:EE:FF';
|
|
final estado = await crearEstadoConNombres(
|
|
presetsDispositivo: {deviceId: PresetEcualizador.rock},
|
|
activeDeviceId: deviceId,
|
|
nombrePlataforma: 'AirPods Pro',
|
|
nombresDispositivos: {deviceId: 'My Headphones'},
|
|
);
|
|
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);
|
|
|
|
expect(find.text('My Headphones'), findsOneWidget);
|
|
expect(find.text('AirPods Pro'), findsNothing);
|
|
});
|
|
|
|
// 4.5 — a device never seen on the stream has no cached platform name, so
|
|
// the row falls back to a humanized transport + address tail instead of the
|
|
// raw id, which told the user nothing.
|
|
testWidgets('4.5 no platform name yet shows a humanized transport label', (
|
|
tester,
|
|
) async {
|
|
setLargeSurface(tester);
|
|
_suppressListTileInkAssertion();
|
|
const deviceId = 'bt_a2dp:AA:BB:CC:DD:EE:FF';
|
|
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);
|
|
|
|
expect(find.text('Bluetooth · EE:FF'), findsOneWidget);
|
|
expect(find.text(deviceId), findsNothing);
|
|
});
|
|
|
|
// 4.6 — permission trigger point: turning the toggle ON requests
|
|
// BLUETOOTH_CONNECT; turning it back OFF must not re-fire the request.
|
|
testWidgets('4.6 permission call fires on device-management open', (
|
|
tester,
|
|
) async {
|
|
setLargeSurface(tester);
|
|
_suppressListTileInkAssertion();
|
|
final fakeDispositivo = FakeServicioDispositivoAudio();
|
|
final estado = await crearEstado(
|
|
eqMultiDeviceEnabled: false,
|
|
dispositivoAudio: fakeDispositivo,
|
|
);
|
|
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);
|
|
|
|
expect(fakeDispositivo.solicitarPermisoBluetoothCalls, equals(0));
|
|
|
|
// Toggle ON: permission requested exactly once.
|
|
await tester.tap(find.byType(Switch).last);
|
|
await pumpStable(tester);
|
|
|
|
expect(estado.ecualizador.eqMultiDeviceEnabled, isTrue);
|
|
expect(fakeDispositivo.solicitarPermisoBluetoothCalls, equals(1));
|
|
|
|
// Toggle OFF again: no further permission request.
|
|
await tester.tap(find.byType(Switch).last);
|
|
await pumpStable(tester);
|
|
|
|
expect(estado.ecualizador.eqMultiDeviceEnabled, isFalse);
|
|
expect(fakeDispositivo.solicitarPermisoBluetoothCalls, equals(1));
|
|
});
|
|
});
|
|
|
|
// ── 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');
|