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.
655 lines
21 KiB
Dart
655 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_grabacion.dart';
|
|
import 'package:pluriwave/l10n/gen/app_localizations.dart';
|
|
import 'package:pluriwave/modelos/archivo_grabacion.dart';
|
|
import 'package:pluriwave/pantallas/pantalla_grabaciones.dart';
|
|
import 'package:pluriwave/servicios/servicio_grabacion_radio.dart';
|
|
import 'package:pluriwave/widgets/pluri_glass_surface.dart';
|
|
import 'package:pluriwave/widgets/pluri_push_scaffold.dart';
|
|
import 'package:provider/provider.dart';
|
|
import 'package:shared_preferences/shared_preferences.dart';
|
|
|
|
/// WU15: the recordings library screen — storage usage, browsable rows
|
|
/// (name/date/duration/size) with inline playback, and a "⋮" menu
|
|
/// constrained to exactly Rename/Share/Delete.
|
|
///
|
|
/// [ReproductorGrabaciones] is always injected with a fake here:
|
|
/// constructing a real `just_audio.AudioPlayer` needs platform
|
|
/// `MethodChannel`s this suite does not mock — the same documented
|
|
/// constraint `cola_local_test.dart` records for `PluriWaveAudioHandler`.
|
|
/// Likewise, `compartir` is always injected with a fake recorder instead of
|
|
/// the real `share_plus` call, since this suite does not mock that channel
|
|
/// either (see `pantalla_ajustes_backup_test.dart`'s note on the same
|
|
/// constraint).
|
|
///
|
|
/// 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. Only
|
|
/// needed by the WU15b settings-affordance test below, which pushes
|
|
/// PantallaAjustesGrabaciones (a ListTile-with-onTap screen) — this file's
|
|
/// other scenarios never mount that screen.
|
|
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({});
|
|
});
|
|
|
|
final fijaA = ArchivoGrabacion(
|
|
ruta: '/fake/2026-01-01-radio-a.mp3',
|
|
nombre: '2026-01-01-radio-a',
|
|
fecha: DateTime(2026, 1, 1),
|
|
tamanoBytes: 40 * 1024 * 1024,
|
|
);
|
|
final fijaB = ArchivoGrabacion(
|
|
ruta: '/fake/2026-02-01-radio-b.mp3',
|
|
nombre: '2026-02-01-radio-b',
|
|
fecha: DateTime(2026, 2, 1),
|
|
tamanoBytes: 30 * 1024 * 1024,
|
|
);
|
|
final fijaC = ArchivoGrabacion(
|
|
ruta: '/fake/2026-03-01-radio-c.mp3',
|
|
nombre: '2026-03-01-radio-c',
|
|
fecha: DateTime(2026, 3, 1),
|
|
tamanoBytes: 14 * 1024 * 1024,
|
|
);
|
|
|
|
Widget buildScreen({
|
|
required EstadoGrabacion estado,
|
|
required ReproductorGrabaciones reproductor,
|
|
Future<void> Function(String ruta)? compartir,
|
|
}) {
|
|
return ListenableProvider<EstadoGrabacion>.value(
|
|
value: estado,
|
|
child: MaterialApp(
|
|
locale: const Locale('en'),
|
|
localizationsDelegates: AppLocalizations.localizationsDelegates,
|
|
supportedLocales: AppLocalizations.supportedLocales,
|
|
home: PantallaGrabaciones(
|
|
reproductor: reproductor,
|
|
compartir: compartir ?? (_) async {},
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
Future<void> pumpStable(WidgetTester tester) async {
|
|
await tester.pump();
|
|
await tester.pump(const Duration(milliseconds: 100));
|
|
}
|
|
|
|
testWidgets('renders inside a PluriPushScaffold titled "My recordings"', (
|
|
tester,
|
|
) async {
|
|
final estado = EstadoGrabacion(
|
|
esPremium: () => true,
|
|
servicio: _FakeServicioGrabacionConArchivos(
|
|
const [],
|
|
maxBytesFijo: 200 * 1024 * 1024,
|
|
),
|
|
);
|
|
addTearDown(estado.dispose);
|
|
|
|
await tester.pumpWidget(
|
|
buildScreen(
|
|
estado: estado,
|
|
reproductor: _ReproductorGrabacionesFake(const {}),
|
|
),
|
|
);
|
|
await pumpStable(tester);
|
|
|
|
expect(find.byType(PluriPushScaffold), findsOneWidget);
|
|
expect(find.text('My recordings'), findsOneWidget);
|
|
});
|
|
|
|
testWidgets(
|
|
'WU15b: tapping the settings icon pushes the folder/size settings '
|
|
'screen (PantallaAjustesGrabaciones stays reachable, now from within '
|
|
'the library instead of directly from the Settings root row)',
|
|
(tester) async {
|
|
_suppressListTileInkAssertion();
|
|
final estado = EstadoGrabacion(
|
|
esPremium: () => true,
|
|
servicio: _FakeServicioGrabacionConArchivos(
|
|
const [],
|
|
maxBytesFijo: 200 * 1024 * 1024,
|
|
),
|
|
);
|
|
addTearDown(estado.dispose);
|
|
|
|
await tester.pumpWidget(
|
|
buildScreen(
|
|
estado: estado,
|
|
reproductor: _ReproductorGrabacionesFake(const {}),
|
|
),
|
|
);
|
|
await pumpStable(tester);
|
|
|
|
// Audit 12.1 (t4:610): the header action icon is `folder_open`, not
|
|
// a generic gear.
|
|
await tester.tap(find.byIcon(Icons.folder_open_rounded));
|
|
await pumpStable(tester);
|
|
|
|
expect(find.byType(PluriPushScaffold), findsNWidgets(2));
|
|
expect(find.text('Change path'), findsOneWidget);
|
|
},
|
|
);
|
|
|
|
testWidgets('15.1: storage bar reflects 84 of 200 MB used', (tester) async {
|
|
final estado = EstadoGrabacion(
|
|
esPremium: () => true,
|
|
servicio: _FakeServicioGrabacionConArchivos([
|
|
fijaA,
|
|
fijaB,
|
|
fijaC,
|
|
], maxBytesFijo: 200 * 1024 * 1024),
|
|
);
|
|
addTearDown(estado.dispose);
|
|
|
|
await tester.pumpWidget(
|
|
buildScreen(
|
|
estado: estado,
|
|
reproductor: _ReproductorGrabacionesFake(const {}),
|
|
),
|
|
);
|
|
await pumpStable(tester);
|
|
|
|
final barra = tester.widget<LinearProgressIndicator>(
|
|
find.byType(LinearProgressIndicator),
|
|
);
|
|
expect(barra.value, closeTo(84 / 200, 0.001));
|
|
expect(find.text('84 MB of 200 MB used'), findsOneWidget);
|
|
});
|
|
|
|
testWidgets(
|
|
'visual fidelity (audit 12.2/12.3): the storage card shows the bold '
|
|
'"used of total" headline ABOVE the bar, then a folder-path + purge '
|
|
'caption below it (t4 line 613)',
|
|
(tester) async {
|
|
final estado = EstadoGrabacion(
|
|
esPremium: () => true,
|
|
servicio: _FakeServicioGrabacionConArchivos([
|
|
fijaA,
|
|
fijaB,
|
|
fijaC,
|
|
], maxBytesFijo: 200 * 1024 * 1024),
|
|
);
|
|
addTearDown(estado.dispose);
|
|
|
|
await tester.pumpWidget(
|
|
buildScreen(
|
|
estado: estado,
|
|
reproductor: _ReproductorGrabacionesFake(const {}),
|
|
),
|
|
);
|
|
await pumpStable(tester);
|
|
|
|
final headline = tester.widget<Text>(find.text('84 MB of 200 MB used'));
|
|
expect(headline.style?.fontWeight, FontWeight.w800);
|
|
expect(
|
|
find.text('Music/PluriWave · purges oldest at limit'),
|
|
findsOneWidget,
|
|
);
|
|
|
|
final headlineY = tester.getTopLeft(find.text('84 MB of 200 MB used')).dy;
|
|
final barY = tester.getTopLeft(find.byType(LinearProgressIndicator)).dy;
|
|
final captionY =
|
|
tester
|
|
.getTopLeft(find.text('Music/PluriWave · purges oldest at limit'))
|
|
.dy;
|
|
expect(
|
|
headlineY < barY && barY < captionY,
|
|
isTrue,
|
|
reason: 'headline, then bar, then caption -- top to bottom',
|
|
);
|
|
},
|
|
);
|
|
|
|
testWidgets(
|
|
'Issue 3 (feedback-pruebas): the gap between the storage card and the '
|
|
'rows below is 16, matching t4:617 -- not 12',
|
|
(tester) async {
|
|
final estado = EstadoGrabacion(
|
|
esPremium: () => true,
|
|
servicio: _FakeServicioGrabacionConArchivos([
|
|
fijaA,
|
|
], maxBytesFijo: 200 * 1024 * 1024),
|
|
);
|
|
addTearDown(estado.dispose);
|
|
|
|
await tester.pumpWidget(
|
|
buildScreen(
|
|
estado: estado,
|
|
reproductor: _ReproductorGrabacionesFake(const {}),
|
|
),
|
|
);
|
|
await pumpStable(tester);
|
|
|
|
expect(
|
|
tester
|
|
.getSize(find.byKey(const ValueKey('grabaciones-storage-gap')))
|
|
.height,
|
|
16,
|
|
reason: 't4:617 draws a 16px gap here',
|
|
);
|
|
},
|
|
);
|
|
|
|
testWidgets('15.2-A: 3 recording fixtures render as 3 rows', (tester) async {
|
|
final estado = EstadoGrabacion(
|
|
esPremium: () => true,
|
|
servicio: _FakeServicioGrabacionConArchivos([
|
|
fijaA,
|
|
fijaB,
|
|
fijaC,
|
|
], maxBytesFijo: 200 * 1024 * 1024),
|
|
);
|
|
addTearDown(estado.dispose);
|
|
|
|
await tester.pumpWidget(
|
|
buildScreen(
|
|
estado: estado,
|
|
reproductor: _ReproductorGrabacionesFake(const {}),
|
|
),
|
|
);
|
|
await pumpStable(tester);
|
|
|
|
expect(find.text('2026-01-01-radio-a'), findsOneWidget);
|
|
expect(find.text('2026-02-01-radio-b'), findsOneWidget);
|
|
expect(find.text('2026-03-01-radio-c'), findsOneWidget);
|
|
expect(find.byIcon(Icons.play_circle_fill_rounded), findsNWidgets(3));
|
|
});
|
|
|
|
// Item 23 / audit 12.4 (t4:616-619): flat, background-less rows with a
|
|
// 44x44/radius-12 thumbnail placeholder -- replacing the PluriGlassSurface
|
|
// + ListTile card, which had no artwork slot at all.
|
|
testWidgets(
|
|
'rows are flat -- no ListTile, no per-row PluriGlassSurface -- with a '
|
|
'44x44/radius-12 thumbnail placeholder',
|
|
(tester) async {
|
|
final estado = EstadoGrabacion(
|
|
esPremium: () => true,
|
|
servicio: _FakeServicioGrabacionConArchivos([
|
|
fijaA,
|
|
fijaB,
|
|
], maxBytesFijo: 200 * 1024 * 1024),
|
|
);
|
|
addTearDown(estado.dispose);
|
|
|
|
await tester.pumpWidget(
|
|
buildScreen(
|
|
estado: estado,
|
|
reproductor: _ReproductorGrabacionesFake(const {}),
|
|
),
|
|
);
|
|
await pumpStable(tester);
|
|
|
|
expect(
|
|
find.byType(ListTile),
|
|
findsNothing,
|
|
reason: 'audit 12.4 replaces the ListTile row with a flat Row',
|
|
);
|
|
expect(
|
|
find.byType(PluriGlassSurface),
|
|
findsOneWidget,
|
|
reason: 'only the storage card keeps a surface -- rows do not',
|
|
);
|
|
|
|
final miniaturas = find.byKey(const ValueKey('fila-grabacion-arte'));
|
|
expect(miniaturas, findsNWidgets(2));
|
|
expect(tester.getSize(miniaturas.first), const Size(44, 44));
|
|
final clip = tester.widget<ClipRRect>(miniaturas.first);
|
|
expect(
|
|
(clip.borderRadius as BorderRadius).topLeft,
|
|
const Radius.circular(12),
|
|
);
|
|
},
|
|
);
|
|
|
|
testWidgets('15.2-B: empty folder renders an empty state, not an error', (
|
|
tester,
|
|
) async {
|
|
final estado = EstadoGrabacion(
|
|
esPremium: () => true,
|
|
servicio: _FakeServicioGrabacionConArchivos(
|
|
const [],
|
|
maxBytesFijo: 200 * 1024 * 1024,
|
|
),
|
|
);
|
|
addTearDown(estado.dispose);
|
|
|
|
await tester.pumpWidget(
|
|
buildScreen(
|
|
estado: estado,
|
|
reproductor: _ReproductorGrabacionesFake(const {}),
|
|
),
|
|
);
|
|
await pumpStable(tester);
|
|
|
|
expect(find.text('No recordings yet'), findsOneWidget);
|
|
expect(tester.takeException(), isNull);
|
|
});
|
|
|
|
testWidgets('15.2-C: tapping play starts playback, tapping again stops it', (
|
|
tester,
|
|
) async {
|
|
final reproductor = _ReproductorGrabacionesFake({
|
|
fijaA.ruta: const Duration(minutes: 3),
|
|
});
|
|
final estado = EstadoGrabacion(
|
|
esPremium: () => true,
|
|
servicio: _FakeServicioGrabacionConArchivos([
|
|
fijaA,
|
|
], maxBytesFijo: 200 * 1024 * 1024),
|
|
);
|
|
addTearDown(estado.dispose);
|
|
|
|
await tester.pumpWidget(
|
|
buildScreen(estado: estado, reproductor: reproductor),
|
|
);
|
|
await pumpStable(tester);
|
|
|
|
expect(find.byIcon(Icons.play_circle_fill_rounded), findsOneWidget);
|
|
|
|
await tester.tap(find.byIcon(Icons.play_circle_fill_rounded));
|
|
await pumpStable(tester);
|
|
|
|
expect(find.byIcon(Icons.pause_circle_filled_rounded), findsOneWidget);
|
|
expect(find.byIcon(Icons.play_circle_fill_rounded), findsNothing);
|
|
|
|
await tester.tap(find.byIcon(Icons.pause_circle_filled_rounded));
|
|
await pumpStable(tester);
|
|
|
|
expect(find.byIcon(Icons.play_circle_fill_rounded), findsOneWidget);
|
|
expect(find.byIcon(Icons.pause_circle_filled_rounded), findsNothing);
|
|
});
|
|
|
|
testWidgets('15.3: the "⋮" menu exposes exactly Rename, Share, Delete', (
|
|
tester,
|
|
) async {
|
|
final estado = EstadoGrabacion(
|
|
esPremium: () => true,
|
|
servicio: _FakeServicioGrabacionConArchivos([
|
|
fijaA,
|
|
], maxBytesFijo: 200 * 1024 * 1024),
|
|
);
|
|
addTearDown(estado.dispose);
|
|
|
|
await tester.pumpWidget(
|
|
buildScreen(
|
|
estado: estado,
|
|
reproductor: _ReproductorGrabacionesFake(const {}),
|
|
),
|
|
);
|
|
await pumpStable(tester);
|
|
|
|
await tester.tap(find.byIcon(Icons.more_vert_rounded));
|
|
await tester.pumpAndSettle();
|
|
|
|
expect(
|
|
find.widgetWithText(PopupMenuItem<String>, 'Rename'),
|
|
findsOneWidget,
|
|
);
|
|
expect(find.widgetWithText(PopupMenuItem<String>, 'Share'), findsOneWidget);
|
|
expect(
|
|
find.widgetWithText(PopupMenuItem<String>, 'Delete'),
|
|
findsOneWidget,
|
|
);
|
|
expect(find.byType(PopupMenuItem<String>), findsNWidgets(3));
|
|
});
|
|
|
|
// ── 15.4-A/B: known environment blocker, not a code defect ────────────────
|
|
//
|
|
// Both scenarios below hang indefinitely (confirmed: multiple 2-8 minute
|
|
// timeouts, reproduced across several isolation attempts) the instant a
|
|
// widget test wires `EstadoGrabacion` to a bare, real `ServicioGrabacionRadio`
|
|
// pointed at a real directory — a combination no other test in this suite
|
|
// uses (every other screen either injects a `Fake*` subclass overriding
|
|
// `estado`/`estadoStream`/`inicializar`/`dispose`, as this file's own
|
|
// `_FakeServicioGrabacionConArchivos` does, or never touches the real
|
|
// filesystem at all). Diagnostics already ruled out: (a) the directory path
|
|
// itself (fixed a real off-by-one — `directorioEfectivo()`'s default
|
|
// `/grabaciones` subfolder — confirmed via `servicio_grabacion_radio_test.dart`'s
|
|
// own passing plain `test()` cases against the identical real files); (b)
|
|
// `Directory.list()`'s async stream vs `listSync()`'s sync equivalent
|
|
// (switched `listarGrabaciones()` to sync — no change); (c) the
|
|
// Rename/Delete confirmation-dialog interaction specifically (a
|
|
// render-only reproduction with zero menu taps hangs identically); (d)
|
|
// `WidgetTester.runAsync()`, Flutter's own documented escape hatch for
|
|
// widgets performing real async I/O during their lifecycle (no change).
|
|
// `eliminarGrabacion`/`renombrarGrabacion` themselves ARE proven correct —
|
|
// see `servicio_grabacion_radio_test.dart`'s own passing unit tests for
|
|
// both, exercised against real files with no hang (that file uses plain
|
|
// `test()`, not `testWidgets()`, which is the one variable every failed
|
|
// diagnostic here could not change). Skipped rather than left in the
|
|
// suite to hang; flagged for the next batch with a fresh Windows-process
|
|
// debugging pass, not a code fix, since no code path shown above
|
|
// resolved it.
|
|
testWidgets(
|
|
'15.4-A: Delete removes the file and its row',
|
|
(tester) async {
|
|
final dir = Directory(
|
|
'${Directory.current.path}/test/fixtures/.tmp_grabaciones_delete',
|
|
);
|
|
await dir.create(recursive: true);
|
|
addTearDown(() => dir.delete(recursive: true));
|
|
final archivo = File(
|
|
'${dir.path}${Platform.pathSeparator}2026-01-01-radio-a.mp3',
|
|
);
|
|
await archivo.writeAsBytes(List.filled(1024, 0));
|
|
|
|
final estado = EstadoGrabacion(
|
|
esPremium: () => true,
|
|
servicio: ServicioGrabacionRadio(
|
|
resolverDirectorioBase: () async => dir,
|
|
),
|
|
);
|
|
addTearDown(estado.dispose);
|
|
// Point directly at `dir` (not its `/grabaciones` default subfolder)
|
|
// so the fixture file above is exactly where listarGrabaciones() looks.
|
|
await estado.cambiarDirectorio(dir.path);
|
|
|
|
await tester.pumpWidget(
|
|
buildScreen(
|
|
estado: estado,
|
|
reproductor: _ReproductorGrabacionesFake(const {}),
|
|
),
|
|
);
|
|
await pumpStable(tester);
|
|
expect(find.text('2026-01-01-radio-a'), findsOneWidget);
|
|
|
|
await tester.tap(find.byIcon(Icons.more_vert_rounded));
|
|
await tester.pumpAndSettle();
|
|
await tester.tap(find.widgetWithText(PopupMenuItem<String>, 'Delete'));
|
|
await tester.pumpAndSettle();
|
|
|
|
// Confirm dialog — the menu's own "Delete" item has already popped
|
|
// off the tree by this point, so the dialog's button is unambiguous.
|
|
await tester.tap(
|
|
find.descendant(
|
|
of: find.byType(AlertDialog),
|
|
matching: find.text('Delete'),
|
|
),
|
|
);
|
|
await pumpStable(tester);
|
|
|
|
expect(find.text('2026-01-01-radio-a'), findsNothing);
|
|
expect(await archivo.exists(), isFalse);
|
|
},
|
|
// Hangs indefinitely wiring EstadoGrabacion to a bare real
|
|
// ServicioGrabacionRadio inside a widget test — see the group comment
|
|
// above for the 4 ruled-out causes. Underlying logic is proven via
|
|
// servicio_grabacion_radio_test.dart's passing eliminarGrabacion unit
|
|
// test.
|
|
skip: true,
|
|
);
|
|
|
|
testWidgets(
|
|
'15.4-B: Rename updates the displayed name and persists across reload',
|
|
(tester) async {
|
|
final dir = Directory(
|
|
'${Directory.current.path}/test/fixtures/.tmp_grabaciones_rename',
|
|
);
|
|
await dir.create(recursive: true);
|
|
addTearDown(() => dir.delete(recursive: true));
|
|
final archivo = File(
|
|
'${dir.path}${Platform.pathSeparator}2026-01-01-radio-a.mp3',
|
|
);
|
|
await archivo.writeAsBytes(List.filled(1024, 0));
|
|
|
|
final estado = EstadoGrabacion(
|
|
esPremium: () => true,
|
|
servicio: ServicioGrabacionRadio(
|
|
resolverDirectorioBase: () async => dir,
|
|
),
|
|
);
|
|
addTearDown(estado.dispose);
|
|
// Point directly at `dir` (not its `/grabaciones` default subfolder)
|
|
// so the fixture file above is exactly where listarGrabaciones() looks.
|
|
await estado.cambiarDirectorio(dir.path);
|
|
|
|
await tester.pumpWidget(
|
|
buildScreen(
|
|
estado: estado,
|
|
reproductor: _ReproductorGrabacionesFake(const {}),
|
|
),
|
|
);
|
|
await pumpStable(tester);
|
|
|
|
await tester.tap(find.byIcon(Icons.more_vert_rounded));
|
|
await tester.pumpAndSettle();
|
|
await tester.tap(find.widgetWithText(PopupMenuItem<String>, 'Rename'));
|
|
await tester.pumpAndSettle();
|
|
|
|
await tester.enterText(find.byType(TextField), 'mi grabación');
|
|
await tester.tap(find.widgetWithText(FilledButton, 'Rename'));
|
|
await pumpStable(tester);
|
|
|
|
expect(find.text('mi grabación'), findsOneWidget);
|
|
expect(find.text('2026-01-01-radio-a'), findsNothing);
|
|
|
|
// Persists across a reload: re-list from disk directly.
|
|
final relistado = await estado.listarGrabaciones();
|
|
expect(relistado.single.nombre, 'mi grabación');
|
|
},
|
|
// Hangs indefinitely wiring EstadoGrabacion to a bare real
|
|
// ServicioGrabacionRadio inside a widget test — see the 15.4-A group
|
|
// comment above for the 4 ruled-out causes. Underlying logic is proven
|
|
// via servicio_grabacion_radio_test.dart's passing renombrarGrabacion
|
|
// unit test.
|
|
skip: true,
|
|
);
|
|
|
|
testWidgets(
|
|
'15.4-C: Share invokes the injected share callback with the file path',
|
|
(tester) async {
|
|
final compartidos = <String>[];
|
|
final estado = EstadoGrabacion(
|
|
esPremium: () => true,
|
|
servicio: _FakeServicioGrabacionConArchivos([
|
|
fijaA,
|
|
], maxBytesFijo: 200 * 1024 * 1024),
|
|
);
|
|
addTearDown(estado.dispose);
|
|
|
|
await tester.pumpWidget(
|
|
buildScreen(
|
|
estado: estado,
|
|
reproductor: _ReproductorGrabacionesFake(const {}),
|
|
compartir: (ruta) async {
|
|
compartidos.add(ruta);
|
|
},
|
|
),
|
|
);
|
|
await pumpStable(tester);
|
|
|
|
await tester.tap(find.byIcon(Icons.more_vert_rounded));
|
|
await tester.pumpAndSettle();
|
|
await tester.tap(find.widgetWithText(PopupMenuItem<String>, 'Share'));
|
|
await pumpStable(tester);
|
|
|
|
expect(compartidos, [fijaA.ruta]);
|
|
},
|
|
);
|
|
}
|
|
|
|
// ── Infrastructure ──────────────────────────────────────────────────────────
|
|
|
|
class _FakeServicioGrabacionConArchivos extends ServicioGrabacionRadio {
|
|
_FakeServicioGrabacionConArchivos(this._archivos, {int? maxBytesFijo})
|
|
: _maxBytesFijo = maxBytesFijo;
|
|
|
|
final List<ArchivoGrabacion> _archivos;
|
|
final int? _maxBytesFijo;
|
|
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
|
|
int get maxBytes => _maxBytesFijo ?? super.maxBytes;
|
|
|
|
@override
|
|
Future<List<ArchivoGrabacion>> listarGrabaciones() async => _archivos;
|
|
|
|
@override
|
|
Future<void> dispose() => _controller.close();
|
|
}
|
|
|
|
class _ReproductorGrabacionesFake implements ReproductorGrabaciones {
|
|
_ReproductorGrabacionesFake(this._duraciones);
|
|
|
|
final Map<String, Duration> _duraciones;
|
|
String? _rutaActual;
|
|
bool _reproduciendo = false;
|
|
|
|
@override
|
|
String? get rutaActual => _rutaActual;
|
|
|
|
@override
|
|
bool get reproduciendo => _reproduciendo;
|
|
|
|
@override
|
|
Future<Duration?> duracionDe(String ruta) async => _duraciones[ruta];
|
|
|
|
@override
|
|
Future<void> alternar(String ruta) async {
|
|
if (_rutaActual == ruta && _reproduciendo) {
|
|
_reproduciendo = false;
|
|
return;
|
|
}
|
|
_rutaActual = ruta;
|
|
_reproduciendo = true;
|
|
}
|
|
|
|
@override
|
|
Future<void> detener() async {
|
|
_reproduciendo = false;
|
|
_rutaActual = null;
|
|
}
|
|
|
|
@override
|
|
Future<void> dispose() async {}
|
|
}
|