fix(ajustes): dispose sheet text controllers after the close animation
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).
This commit is contained in:
@@ -28,14 +28,6 @@ void _suppressListTileInkAssertion() {
|
||||
/// 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`.
|
||||
///
|
||||
/// This file deliberately does NOT interact with "Maximum recording size"
|
||||
/// (`_editarTamanoMaximo`): that control has a pre-existing, out-of-scope
|
||||
/// controller-dispose race (documented in `pantalla_ajustes_grabaciones.dart`
|
||||
/// and, for the analogous `_editarGrupo` case, in
|
||||
/// `pantalla_ajustes_grupos_favoritos_test.dart`) that this move does not
|
||||
/// fix. "Restore default path" is exercised instead — a real, moved
|
||||
/// capability with no such hazard.
|
||||
void main() {
|
||||
setUp(() {
|
||||
SharedPreferences.setMockInitialValues({});
|
||||
@@ -100,4 +92,37 @@ void main() {
|
||||
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();
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user