Unverified Commit e98e0b40 authored by Justin McCandless's avatar Justin McCandless Committed by GitHub

EditableText action handlers swallow errors (#66851)

Errors that happen in user-defined callbacks (like onChanged) will now make it to the console.
parent 571b190f
...@@ -1806,7 +1806,16 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien ...@@ -1806,7 +1806,16 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien
void _finalizeEditing(TextInputAction action, {required bool shouldUnfocus}) { void _finalizeEditing(TextInputAction action, {required bool shouldUnfocus}) {
// Take any actions necessary now that the user has completed editing. // Take any actions necessary now that the user has completed editing.
if (widget.onEditingComplete != null) { 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 { } else {
// Default behavior if the developer did not provide an // Default behavior if the developer did not provide an
// onEditingComplete callback: Finalize editing and remove focus, or move // onEditingComplete callback: Finalize editing and remove focus, or move
...@@ -1838,8 +1847,18 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien ...@@ -1838,8 +1847,18 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien
} }
// Invoke optional callback with the user's submitted content. // Invoke optional callback with the user's submitted content.
if (widget.onSubmitted != null) if (widget.onSubmitted != null) {
widget.onSubmitted!(_value.text); 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() { void _updateRemoteEditingValueIfNeeded() {
...@@ -2053,8 +2072,18 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien ...@@ -2053,8 +2072,18 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien
); );
_selectionOverlay!.handlesVisible = widget.showSelectionHandles; _selectionOverlay!.handlesVisible = widget.showSelectionHandles;
_selectionOverlay!.showHandles(); _selectionOverlay!.showHandles();
if (widget.onSelectionChanged != null) if (widget.onSelectionChanged != null) {
widget.onSelectionChanged!(selection, cause); 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 ...@@ -2178,8 +2207,18 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien
_value = _lastFormattedValue!; _value = _lastFormattedValue!;
} }
if (textChanged && widget.onChanged != null) if (textChanged && widget.onChanged != null) {
widget.onChanged!(value.text); try {
widget.onChanged!(value.text);
} catch (exception, stack) {
FlutterError.reportError(FlutterErrorDetails(
exception: exception,
stack: stack,
library: 'widgets',
context: ErrorDescription('while calling onChanged'),
));
}
}
_lastFormattedUnmodifiedTextEditingValue = _receivedRemoteTextEditingValue; _lastFormattedUnmodifiedTextEditingValue = _receivedRemoteTextEditingValue;
} }
......
...@@ -5776,6 +5776,125 @@ void main() { ...@@ -5776,6 +5776,125 @@ void main() {
expect(state.currentTextEditingValue.text, '12345'); expect(state.currentTextEditingValue.text, '12345');
expect(state.currentTextEditingValue.composing, TextRange.empty); 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 { class MockTextFormatter extends TextInputFormatter {
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment