Pre-existing bug, reproduces identically before this branch's changes: _editarGrupo (pantalla_ajustes_grupos_favoritos.dart) and _editarTamanoMaximo (pantalla_ajustes_grabaciones.dart) each created a TextEditingController, awaited showModalBottomSheet, then disposed the controller immediately on resolve - racing the sheet's own close animation, which still holds a bound TextField for a couple more frames. Manifests as "A TextEditingController was used after being disposed" plus a couple of cascading framework-internal symptoms. Fix: extract each sheet's content into its own StatefulWidget (_HojaEditarGrupo, _HojaTamanoMaximo) that owns the controller in its own State. Flutter only calls State.dispose() once the widget is actually removed from the tree, i.e. after the close animation finishes, so there is no dispose-timing decision left for the caller to get wrong. _editarGrupo's own test previously suppressed the crash via a FlutterError.onError override instead of fixing it; that suppression is removed here. _editarTamanoMaximo had no coverage at all for this interaction; added it. Strict TDD: confirmed both sites fail without the fix (RED) before applying it (GREEN).
129 lines
4.5 KiB
Dart
129 lines
4.5 KiB
Dart
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/pantallas/ajustes/pantalla_ajustes_grabaciones.dart';
|
|
import 'package:pluriwave/widgets/pluri_push_scaffold.dart';
|
|
import 'package:provider/provider.dart';
|
|
import 'package:shared_preferences/shared_preferences.dart';
|
|
|
|
import '../../helpers/fakes_alarmas.dart';
|
|
|
|
/// 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);
|
|
}
|
|
|
|
/// WU3b task 3b.1: the GRABACIONES Y MÚSICA detail screen for "Grabaciones"
|
|
/// renders inside a [PluriPushScaffold] and its moved controls still respond
|
|
/// exactly as they did inside the old `_SeccionGrabaciones`.
|
|
void main() {
|
|
setUp(() {
|
|
SharedPreferences.setMockInitialValues({});
|
|
});
|
|
|
|
Widget buildScreen(EstadoGrabacion estado) {
|
|
return ListenableProvider<EstadoGrabacion>.value(
|
|
value: estado,
|
|
child: MaterialApp(
|
|
locale: const Locale('en'),
|
|
localizationsDelegates: AppLocalizations.localizationsDelegates,
|
|
supportedLocales: AppLocalizations.supportedLocales,
|
|
home: const PantallaAjustesGrabaciones(),
|
|
),
|
|
);
|
|
}
|
|
|
|
testWidgets('renders inside a PluriPushScaffold titled "Recordings"', (
|
|
tester,
|
|
) async {
|
|
_suppressListTileInkAssertion();
|
|
final estado = EstadoGrabacion(
|
|
servicio: FakeServicioGrabacionRadioInactiva(),
|
|
);
|
|
addTearDown(estado.dispose);
|
|
|
|
await tester.pumpWidget(buildScreen(estado));
|
|
await tester.pump();
|
|
await tester.pump(const Duration(milliseconds: 100));
|
|
|
|
expect(find.byType(PluriPushScaffold), findsOneWidget);
|
|
expect(find.text('Recordings'), findsOneWidget);
|
|
});
|
|
|
|
testWidgets('moved control still responds: restore default path clears the '
|
|
'configured directory', (tester) async {
|
|
_suppressListTileInkAssertion();
|
|
final estado = EstadoGrabacion(
|
|
servicio: FakeServicioGrabacionRadioInactiva(),
|
|
);
|
|
addTearDown(estado.dispose);
|
|
await estado.cambiarDirectorio('/tmp/custom-recordings');
|
|
expect(estado.directorioConfigurado, '/tmp/custom-recordings');
|
|
|
|
await tester.pumpWidget(buildScreen(estado));
|
|
await tester.pump();
|
|
await tester.pump(const Duration(milliseconds: 100));
|
|
|
|
await tester.tap(find.byIcon(Icons.restore_rounded));
|
|
await tester.pump();
|
|
await tester.pump(const Duration(milliseconds: 100));
|
|
|
|
expect(estado.directorioConfigurado, isNull);
|
|
expect(
|
|
find.text('The default internal folder will be used'),
|
|
findsOneWidget,
|
|
);
|
|
|
|
// SnackBar's own dismiss Timer is not frame-scheduled — let it resolve
|
|
// before teardown (WU3a batch discovery) instead of leaving a pending
|
|
// Timer behind.
|
|
await tester.pump(const Duration(seconds: 5));
|
|
await tester.pumpAndSettle();
|
|
});
|
|
|
|
testWidgets(
|
|
'moved control still responds: editing the maximum recording size '
|
|
'persists it',
|
|
(tester) async {
|
|
_suppressListTileInkAssertion();
|
|
final estado = EstadoGrabacion(
|
|
servicio: FakeServicioGrabacionRadioInactiva(),
|
|
);
|
|
addTearDown(estado.dispose);
|
|
expect(estado.maxBytes, 500 * 1024 * 1024, reason: 'default, not 250');
|
|
|
|
await tester.pumpWidget(buildScreen(estado));
|
|
await tester.pump();
|
|
await tester.pump(const Duration(milliseconds: 100));
|
|
|
|
await tester.tap(find.text('Maximum recording size'));
|
|
await tester.pumpAndSettle();
|
|
await tester.enterText(find.byType(TextField), '250');
|
|
await tester.pumpAndSettle();
|
|
await tester.tap(find.text('Save quick access'));
|
|
await tester.pumpAndSettle();
|
|
|
|
expect(estado.maxBytes, 250 * 1024 * 1024);
|
|
expect(find.text('Recording limit updated to 250 MB'), findsOneWidget);
|
|
|
|
// SnackBar's own dismiss Timer is not frame-scheduled — let it resolve
|
|
// before teardown (WU3a batch discovery) instead of leaving a pending
|
|
// Timer behind.
|
|
await tester.pump(const Duration(seconds: 5));
|
|
await tester.pumpAndSettle();
|
|
},
|
|
);
|
|
}
|