EditableText action handlers swallow errors (#66851)

Errors that happen in user-defined callbacks (like onChanged) will now make it to the console.
This commit is contained in:
Justin McCandless 2020-10-07 13:40:38 -07:00 committed by GitHub
parent 571b190f07
commit e98e0b409f
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
2 changed files with 165 additions and 7 deletions

View file

@ -1806,7 +1806,16 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien
void _finalizeEditing(TextInputAction action, {required bool shouldUnfocus}) {
// Take any actions necessary now that the user has completed editing.
if (widget.onEditingComplete != null) {
widget.onEditingComplete!();
try {
widget.onEditingComplete!();
} catch (exception, stack) {
FlutterError.reportError(FlutterErrorDetails(
exception: exception,
stack: stack,
library: 'widgets',
context: ErrorDescription('while calling onEditingComplete for $action'),
));
}
} else {
// Default behavior if the developer did not provide an
// onEditingComplete callback: Finalize editing and remove focus, or move
@ -1838,8 +1847,18 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien
}
// Invoke optional callback with the user's submitted content.
if (widget.onSubmitted != null)
widget.onSubmitted!(_value.text);
if (widget.onSubmitted != null) {
try {
widget.onSubmitted!(_value.text);
} catch (exception, stack) {
FlutterError.reportError(FlutterErrorDetails(
exception: exception,
stack: stack,
library: 'widgets',
context: ErrorDescription('while calling onSubmitted for $action'),
));
}
}
}
void _updateRemoteEditingValueIfNeeded() {
@ -2053,8 +2072,18 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien
);
_selectionOverlay!.handlesVisible = widget.showSelectionHandles;
_selectionOverlay!.showHandles();
if (widget.onSelectionChanged != null)
widget.onSelectionChanged!(selection, cause);
if (widget.onSelectionChanged != null) {
try {
widget.onSelectionChanged!(selection, cause);
} catch (exception, stack) {
FlutterError.reportError(FlutterErrorDetails(
exception: exception,
stack: stack,
library: 'widgets',
context: ErrorDescription('while calling onSelectionChanged for $cause'),
));
}
}
}
}
@ -2178,8 +2207,18 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien
_value = _lastFormattedValue!;
}
if (textChanged && widget.onChanged != null)
widget.onChanged!(value.text);
if (textChanged && widget.onChanged != null) {
try {
widget.onChanged!(value.text);
} catch (exception, stack) {
FlutterError.reportError(FlutterErrorDetails(
exception: exception,
stack: stack,
library: 'widgets',
context: ErrorDescription('while calling onChanged'),
));
}
}
_lastFormattedUnmodifiedTextEditingValue = _receivedRemoteTextEditingValue;
}

View file

@ -5776,6 +5776,125 @@ void main() {
expect(state.currentTextEditingValue.text, '12345');
expect(state.currentTextEditingValue.composing, TextRange.empty);
});
group('callback errors', () {
const String errorText = 'Test EditableText callback error';
testWidgets('onSelectionChanged can throw errors', (WidgetTester tester) async {
await tester.pumpWidget(MaterialApp(
home: EditableText(
showSelectionHandles: true,
maxLines: 2,
controller: TextEditingController(
text: 'flutter is the best!',
),
focusNode: FocusNode(),
cursorColor: Colors.red,
backgroundCursorColor: Colors.blue,
style: Typography.material2018(platform: TargetPlatform.android).black.subtitle1.copyWith(fontFamily: 'Roboto'),
keyboardType: TextInputType.text,
selectionControls: materialTextSelectionControls,
onSelectionChanged: (TextSelection selection, SelectionChangedCause cause) {
throw FlutterError(errorText);
},
),
));
// Interact with the field to establish the input connection.
await tester.tap(find.byType(EditableText));
final dynamic error = tester.takeException();
expect(error, isFlutterError);
expect(error.toString(), contains(errorText));
});
testWidgets('onChanged can throw errors', (WidgetTester tester) async {
await tester.pumpWidget(MaterialApp(
home: EditableText(
showSelectionHandles: true,
maxLines: 2,
controller: TextEditingController(
text: 'flutter is the best!',
),
focusNode: FocusNode(),
cursorColor: Colors.red,
backgroundCursorColor: Colors.blue,
style: Typography.material2018(platform: TargetPlatform.android).black.subtitle1.copyWith(fontFamily: 'Roboto'),
keyboardType: TextInputType.text,
onChanged: (String text) {
throw FlutterError(errorText);
},
),
));
// Modify the text and expect an error from onChanged.
await tester.enterText(find.byType(EditableText), '...');
final dynamic error = tester.takeException();
expect(error, isFlutterError);
expect(error.toString(), contains(errorText));
});
testWidgets('onEditingComplete can throw errors', (WidgetTester tester) async {
await tester.pumpWidget(MaterialApp(
home: EditableText(
showSelectionHandles: true,
maxLines: 2,
controller: TextEditingController(
text: 'flutter is the best!',
),
focusNode: FocusNode(),
cursorColor: Colors.red,
backgroundCursorColor: Colors.blue,
style: Typography.material2018(platform: TargetPlatform.android).black.subtitle1.copyWith(fontFamily: 'Roboto'),
keyboardType: TextInputType.text,
onEditingComplete: () {
throw FlutterError(errorText);
},
),
));
// Interact with the field to establish the input connection.
final Offset topLeft = tester.getTopLeft(find.byType(EditableText));
await tester.tapAt(topLeft + const Offset(0.0, 5.0));
await tester.pump();
// Submit and expect an error from onEditingComplete.
await tester.testTextInput.receiveAction(TextInputAction.done);
final dynamic error = tester.takeException();
expect(error, isFlutterError);
expect(error.toString(), contains(errorText));
});
testWidgets('onSubmitted can throw errors', (WidgetTester tester) async {
await tester.pumpWidget(MaterialApp(
home: EditableText(
showSelectionHandles: true,
maxLines: 2,
controller: TextEditingController(
text: 'flutter is the best!',
),
focusNode: FocusNode(),
cursorColor: Colors.red,
backgroundCursorColor: Colors.blue,
style: Typography.material2018(platform: TargetPlatform.android).black.subtitle1.copyWith(fontFamily: 'Roboto'),
keyboardType: TextInputType.text,
onSubmitted: (String text) {
throw FlutterError(errorText);
},
),
));
// Interact with the field to establish the input connection.
final Offset topLeft = tester.getTopLeft(find.byType(EditableText));
await tester.tapAt(topLeft + const Offset(0.0, 5.0));
await tester.pump();
// Submit and expect an error from onSubmitted.
await tester.testTextInput.receiveAction(TextInputAction.done);
final dynamic error = tester.takeException();
expect(error, isFlutterError);
expect(error.toString(), contains(errorText));
});
});
}
class MockTextFormatter extends TextInputFormatter {