Folders over the 50-item cap now show a "Mas..." item that reveals the next page on tap, instead of silently dropping the rest. Paging slices the cheap raw list before building any MediaItem, so items beyond the requested page are never resolved (art, title) -- proven by a call-count test. Also swaps the raw SAF content:// URI shown in settings for a parsed, human-readable folder name with a localized fallback across all 13 locales. servicio_audio.dart is untouched; this stays entirely within the local-music tree/dispatch layer.
593 lines
20 KiB
Dart
593 lines
20 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 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'),
|
|
);
|
|
});
|
|
});
|
|
|
|
// ── 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 — approval test (regression-lock): a device never seen on the
|
|
// stream has no cached platform name, so legacy raw-id fallback holds.
|
|
testWidgets('4.5 no platform name yet falls back to raw id', (
|
|
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(deviceId), findsOneWidget);
|
|
});
|
|
|
|
// 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');
|