Unverified Commit 5a69de82 authored by Pedro Massango's avatar Pedro Massango Committed by GitHub

FormField should autovalidate only if its content was changed (fixed) (#59766)

parent 9c4a5ef1
...@@ -1437,7 +1437,7 @@ class DropdownButtonFormField<T> extends FormField<T> { ...@@ -1437,7 +1437,7 @@ class DropdownButtonFormField<T> extends FormField<T> {
/// Creates a [DropdownButton] widget that is a [FormField], wrapped in an /// Creates a [DropdownButton] widget that is a [FormField], wrapped in an
/// [InputDecorator]. /// [InputDecorator].
/// ///
/// For a description of the `onSaved`, `validator`, or `autovalidate` /// For a description of the `onSaved`, `validator`, or `autovalidateMode`
/// parameters, see [FormField]. For the rest (other than [decoration]), see /// parameters, see [FormField]. For the rest (other than [decoration]), see
/// [DropdownButton]. /// [DropdownButton].
/// ///
...@@ -1469,6 +1469,7 @@ class DropdownButtonFormField<T> extends FormField<T> { ...@@ -1469,6 +1469,7 @@ class DropdownButtonFormField<T> extends FormField<T> {
FormFieldSetter<T> onSaved, FormFieldSetter<T> onSaved,
FormFieldValidator<T> validator, FormFieldValidator<T> validator,
bool autovalidate = false, bool autovalidate = false,
AutovalidateMode autovalidateMode,
}) : assert(items == null || items.isEmpty || value == null || }) : assert(items == null || items.isEmpty || value == null ||
items.where((DropdownMenuItem<T> item) { items.where((DropdownMenuItem<T> item) {
return item.value == value; return item.value == value;
...@@ -1484,13 +1485,21 @@ class DropdownButtonFormField<T> extends FormField<T> { ...@@ -1484,13 +1485,21 @@ class DropdownButtonFormField<T> extends FormField<T> {
assert(isExpanded != null), assert(isExpanded != null),
assert(itemHeight == null || itemHeight >= kMinInteractiveDimension), assert(itemHeight == null || itemHeight >= kMinInteractiveDimension),
assert(autofocus != null), assert(autofocus != null),
assert(autovalidate != null),
assert(
autovalidate == false ||
autovalidate == true && autovalidateMode == null,
'autovalidate and autovalidateMode should not be used together.'
),
decoration = decoration ?? InputDecoration(focusColor: focusColor), decoration = decoration ?? InputDecoration(focusColor: focusColor),
super( super(
key: key, key: key,
onSaved: onSaved, onSaved: onSaved,
initialValue: value, initialValue: value,
validator: validator, validator: validator,
autovalidate: autovalidate, autovalidateMode: autovalidate
? AutovalidateMode.always
: (autovalidateMode ?? AutovalidateMode.disabled),
builder: (FormFieldState<T> field) { builder: (FormFieldState<T> field) {
final _DropdownButtonFormFieldState<T> state = field as _DropdownButtonFormFieldState<T>; final _DropdownButtonFormFieldState<T> state = field as _DropdownButtonFormFieldState<T>;
final InputDecoration decorationArg = decoration ?? InputDecoration(focusColor: focusColor); final InputDecoration decorationArg = decoration ?? InputDecoration(focusColor: focusColor);
......
...@@ -180,6 +180,7 @@ class TextFormField extends FormField<String> { ...@@ -180,6 +180,7 @@ class TextFormField extends FormField<String> {
InputCounterWidgetBuilder buildCounter, InputCounterWidgetBuilder buildCounter,
ScrollPhysics scrollPhysics, ScrollPhysics scrollPhysics,
Iterable<String> autofillHints, Iterable<String> autofillHints,
AutovalidateMode autovalidateMode,
}) : assert(initialValue == null || controller == null), }) : assert(initialValue == null || controller == null),
assert(textAlign != null), assert(textAlign != null),
assert(autofocus != null), assert(autofocus != null),
...@@ -189,6 +190,11 @@ class TextFormField extends FormField<String> { ...@@ -189,6 +190,11 @@ class TextFormField extends FormField<String> {
assert(autocorrect != null), assert(autocorrect != null),
assert(enableSuggestions != null), assert(enableSuggestions != null),
assert(autovalidate != null), assert(autovalidate != null),
assert(
autovalidate == false ||
autovalidate == true && autovalidateMode == null,
'autovalidate and autovalidateMode should not be used together.'
),
assert(maxLengthEnforced != null), assert(maxLengthEnforced != null),
assert(scrollPadding != null), assert(scrollPadding != null),
assert(maxLines == null || maxLines > 0), assert(maxLines == null || maxLines > 0),
...@@ -210,8 +216,10 @@ class TextFormField extends FormField<String> { ...@@ -210,8 +216,10 @@ class TextFormField extends FormField<String> {
initialValue: controller != null ? controller.text : (initialValue ?? ''), initialValue: controller != null ? controller.text : (initialValue ?? ''),
onSaved: onSaved, onSaved: onSaved,
validator: validator, validator: validator,
autovalidate: autovalidate,
enabled: enabled ?? decoration?.enabled ?? true, enabled: enabled ?? decoration?.enabled ?? true,
autovalidateMode: autovalidate
? AutovalidateMode.always
: (autovalidateMode ?? AutovalidateMode.disabled),
builder: (FormFieldState<String> field) { builder: (FormFieldState<String> field) {
final _TextFormFieldState state = field as _TextFormFieldState; final _TextFormFieldState state = field as _TextFormFieldState;
final InputDecoration effectiveDecoration = (decoration ?? const InputDecoration()) final InputDecoration effectiveDecoration = (decoration ?? const InputDecoration())
......
...@@ -81,7 +81,17 @@ class Form extends StatefulWidget { ...@@ -81,7 +81,17 @@ class Form extends StatefulWidget {
this.autovalidate = false, this.autovalidate = false,
this.onWillPop, this.onWillPop,
this.onChanged, this.onChanged,
AutovalidateMode autovalidateMode,
}) : assert(child != null), }) : assert(child != null),
assert(autovalidate != null),
assert(
autovalidate == false ||
autovalidate == true && autovalidateMode == null,
'autovalidate and autovalidateMode should not be used together.'
),
autovalidateMode = autovalidate
? AutovalidateMode.always
: (autovalidateMode ?? AutovalidateMode.disabled),
super(key: key); super(key: key);
/// Returns the closest [FormState] which encloses the given context. /// Returns the closest [FormState] which encloses the given context.
...@@ -127,6 +137,12 @@ class Form extends StatefulWidget { ...@@ -127,6 +137,12 @@ class Form extends StatefulWidget {
/// will rebuild. /// will rebuild.
final VoidCallback onChanged; final VoidCallback onChanged;
/// Used to enable/disable form fields auto validation and update their error
/// text.
///
/// {@macro flutter.widgets.form.autovalidateMode}
final AutovalidateMode autovalidateMode;
@override @override
FormState createState() => FormState(); FormState createState() => FormState();
} }
...@@ -139,6 +155,7 @@ class Form extends StatefulWidget { ...@@ -139,6 +155,7 @@ class Form extends StatefulWidget {
/// Typically obtained via [Form.of]. /// Typically obtained via [Form.of].
class FormState extends State<Form> { class FormState extends State<Form> {
int _generation = 0; int _generation = 0;
bool _hasInteractedByUser = false;
final Set<FormFieldState<dynamic>> _fields = <FormFieldState<dynamic>>{}; final Set<FormFieldState<dynamic>> _fields = <FormFieldState<dynamic>>{};
// Called when a form field has changed. This will cause all form fields // Called when a form field has changed. This will cause all form fields
...@@ -146,6 +163,10 @@ class FormState extends State<Form> { ...@@ -146,6 +163,10 @@ class FormState extends State<Form> {
void _fieldDidChange() { void _fieldDidChange() {
if (widget.onChanged != null) if (widget.onChanged != null)
widget.onChanged(); widget.onChanged();
_hasInteractedByUser = _fields
.any((FormFieldState<dynamic> field) => field._hasInteractedByUser);
_forceRebuild(); _forceRebuild();
} }
...@@ -165,8 +186,19 @@ class FormState extends State<Form> { ...@@ -165,8 +186,19 @@ class FormState extends State<Form> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
if (widget.autovalidate) switch (widget.autovalidateMode) {
case AutovalidateMode.always:
_validate();
break;
case AutovalidateMode.onUserInteraction:
if (_hasInteractedByUser) {
_validate(); _validate();
}
break;
case AutovalidateMode.disabled:
break;
}
return WillPopScope( return WillPopScope(
onWillPop: widget.onWillPop, onWillPop: widget.onWillPop,
child: _FormScope( child: _FormScope(
...@@ -188,11 +220,12 @@ class FormState extends State<Form> { ...@@ -188,11 +220,12 @@ class FormState extends State<Form> {
/// ///
/// The [Form.onChanged] callback will be called. /// The [Form.onChanged] callback will be called.
/// ///
/// If the form's [Form.autovalidate] property is true, the fields will all be /// If the form's [Form.autovalidateMode] property is [AutovalidateMode.always],
/// revalidated after being reset. /// the fields will all be revalidated after being reset.
void reset() { void reset() {
for (final FormFieldState<dynamic> field in _fields) for (final FormFieldState<dynamic> field in _fields)
field.reset(); field.reset();
_hasInteractedByUser = false;
_fieldDidChange(); _fieldDidChange();
} }
...@@ -201,6 +234,7 @@ class FormState extends State<Form> { ...@@ -201,6 +234,7 @@ class FormState extends State<Form> {
/// ///
/// The form will rebuild to report the results. /// The form will rebuild to report the results.
bool validate() { bool validate() {
_hasInteractedByUser = true;
_forceRebuild(); _forceRebuild();
return _validate(); return _validate();
} }
...@@ -287,7 +321,16 @@ class FormField<T> extends StatefulWidget { ...@@ -287,7 +321,16 @@ class FormField<T> extends StatefulWidget {
this.initialValue, this.initialValue,
this.autovalidate = false, this.autovalidate = false,
this.enabled = true, this.enabled = true,
AutovalidateMode autovalidateMode,
}) : assert(builder != null), }) : assert(builder != null),
assert(
autovalidate == false ||
autovalidate == true && autovalidateMode == null,
'autovalidate and autovalidateMode should not be used together.'
),
autovalidateMode = autovalidate
? AutovalidateMode.always
: (autovalidateMode ?? AutovalidateMode.disabled),
super(key: key); super(key: key);
/// An optional method to call with the final value when the form is saved via /// An optional method to call with the final value when the form is saved via
...@@ -325,11 +368,26 @@ class FormField<T> extends StatefulWidget { ...@@ -325,11 +368,26 @@ class FormField<T> extends StatefulWidget {
/// Whether the form is able to receive user input. /// Whether the form is able to receive user input.
/// ///
/// Defaults to true. If [autovalidate] is true, the field will be validated. /// Defaults to true. If [autovalidateMode] is not [AutovalidateMode.disabled],
/// Likewise, if this field is false, the widget will not be validated /// the field will be auto validated. Likewise, if this field is false, the widget
/// regardless of [autovalidate]. /// will not be validated regardless of [autovalidateMode].
final bool enabled; final bool enabled;
/// Used to enable/disable this form field auto validation and update its
/// error text.
///
/// {@template flutter.widgets.form.autovalidateMode}
/// If [AutovalidateMode.onUserInteraction] this form field will only
/// auto-validate after its content changes, if [AutovalidateMode.always] it
/// will auto validate even without user interaction and
/// if [AutovalidateMode.disabled] the auto validation will be disabled.
///
/// Defaults to [AutovalidateMode.disabled] if [autovalidate] is false which
/// means no auto validation will occur. If [autovalidate] is true then this
/// is set to [AutovalidateMode.always] for backward compatibility.
/// {@endtemplate}
final AutovalidateMode autovalidateMode;
@override @override
FormFieldState<T> createState() => FormFieldState<T>(); FormFieldState<T> createState() => FormFieldState<T>();
} }
...@@ -339,6 +397,7 @@ class FormField<T> extends StatefulWidget { ...@@ -339,6 +397,7 @@ class FormField<T> extends StatefulWidget {
class FormFieldState<T> extends State<FormField<T>> { class FormFieldState<T> extends State<FormField<T>> {
T _value; T _value;
String _errorText; String _errorText;
bool _hasInteractedByUser = false;
/// The current value of the form field. /// The current value of the form field.
T get value => _value; T get value => _value;
...@@ -371,8 +430,10 @@ class FormFieldState<T> extends State<FormField<T>> { ...@@ -371,8 +430,10 @@ class FormFieldState<T> extends State<FormField<T>> {
void reset() { void reset() {
setState(() { setState(() {
_value = widget.initialValue; _value = widget.initialValue;
_hasInteractedByUser = false;
_errorText = null; _errorText = null;
}); });
Form.of(context)?._fieldDidChange();
} }
/// Calls [FormField.validator] to set the [errorText]. Returns true if there /// Calls [FormField.validator] to set the [errorText]. Returns true if there
...@@ -397,11 +458,13 @@ class FormFieldState<T> extends State<FormField<T>> { ...@@ -397,11 +458,13 @@ class FormFieldState<T> extends State<FormField<T>> {
/// Updates this field's state to the new value. Useful for responding to /// Updates this field's state to the new value. Useful for responding to
/// child widget changes, e.g. [Slider]'s [Slider.onChanged] argument. /// child widget changes, e.g. [Slider]'s [Slider.onChanged] argument.
/// ///
/// Triggers the [Form.onChanged] callback and, if the [Form.autovalidate] /// Triggers the [Form.onChanged] callback and, if [Form.autovalidateMode] is
/// field is set, revalidates all the fields of the form. /// [AutovalidateMode.always] or [AutovalidateMode.onUserInteraction],
/// revalidates all the fields of the form.
void didChange(T value) { void didChange(T value) {
setState(() { setState(() {
_value = value; _value = value;
_hasInteractedByUser = true;
}); });
Form.of(context)?._fieldDidChange(); Form.of(context)?._fieldDidChange();
} }
...@@ -432,10 +495,34 @@ class FormFieldState<T> extends State<FormField<T>> { ...@@ -432,10 +495,34 @@ class FormFieldState<T> extends State<FormField<T>> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
// Only autovalidate if the widget is also enabled if (widget.enabled) {
if (widget.autovalidate && widget.enabled) switch (widget.autovalidateMode) {
case AutovalidateMode.always:
_validate(); _validate();
break;
case AutovalidateMode.onUserInteraction:
if (_hasInteractedByUser) {
_validate();
}
break;
case AutovalidateMode.disabled:
break;
}
}
Form.of(context)?._register(this); Form.of(context)?._register(this);
return widget.builder(this); return widget.builder(this);
} }
} }
/// Used to configure the auto validation of [FormField] and [Form] widgets.
enum AutovalidateMode {
/// No auto validation will occur.
disabled,
/// Used to auto-validate [Form] and [FormField] even without user interaction.
always,
/// Used to auto-validate [Form] and [FormField] only after each user
/// interaction.
onUserInteraction,
}
...@@ -29,7 +29,7 @@ Finder _iconRichText(Key iconKey) { ...@@ -29,7 +29,7 @@ Finder _iconRichText(Key iconKey) {
Widget buildFormFrame({ Widget buildFormFrame({
Key buttonKey, Key buttonKey,
bool autovalidate = false, AutovalidateMode autovalidateMode = AutovalidateMode.disabled,
int elevation = 8, int elevation = 8,
String value = 'two', String value = 'two',
ValueChanged<String> onChanged, ValueChanged<String> onChanged,
...@@ -55,7 +55,7 @@ Widget buildFormFrame({ ...@@ -55,7 +55,7 @@ Widget buildFormFrame({
child: RepaintBoundary( child: RepaintBoundary(
child: DropdownButtonFormField<String>( child: DropdownButtonFormField<String>(
key: buttonKey, key: buttonKey,
autovalidate: autovalidate, autovalidateMode: autovalidateMode,
elevation: elevation, elevation: elevation,
value: value, value: value,
hint: hint, hint: hint,
...@@ -180,7 +180,7 @@ void main() { ...@@ -180,7 +180,7 @@ void main() {
_validateCalled++; _validateCalled++;
return currentValue == null ? 'Must select value' : null; return currentValue == null ? 'Must select value' : null;
}, },
autovalidate: true, autovalidateMode: AutovalidateMode.always,
), ),
), ),
); );
...@@ -763,4 +763,57 @@ void main() { ...@@ -763,4 +763,57 @@ void main() {
expect(currentValue, equals('one')); expect(currentValue, equals('one'));
expect(find.text(currentValue), findsOneWidget); expect(find.text(currentValue), findsOneWidget);
}); });
testWidgets('autovalidateMode is passed to super', (WidgetTester tester) async {
int _validateCalled = 0;
await tester.pumpWidget(
MaterialApp(
home: Material(
child: Center(
child: DropdownButtonFormField<String>(
autovalidateMode: AutovalidateMode.always,
items: menuItems.map((String value) {
return DropdownMenuItem<String>(
value: value,
child: Text(value),
);
}).toList(),
onChanged: onChanged,
validator: (String value) {
_validateCalled++;
return null;
},
),
),
),
),
);
expect(_validateCalled, 1);
});
testWidgets('autovalidateMode and autovalidate should not be used at the same time', (WidgetTester tester) async {
Widget builder() {
return MaterialApp(
home: Material(
child: Center(
child: DropdownButtonFormField<String>(
autovalidate: true,
autovalidateMode: AutovalidateMode.always,
items: menuItems.map((String value) {
return DropdownMenuItem<String>(
value: value,
child: Text(value),
);
}).toList(),
onChanged: onChanged,
),
),
),
);
}
expect(() => builder(), throwsAssertionError);
});
} }
...@@ -2169,6 +2169,7 @@ void main() { ...@@ -2169,6 +2169,7 @@ void main() {
icon: Container(), icon: Container(),
items: itemValues.map<DropdownMenuItem<String>>((String value) { items: itemValues.map<DropdownMenuItem<String>>((String value) {
return DropdownMenuItem<String>( return DropdownMenuItem<String>(
value: value,
child: Text(value), child: Text(value),
); );
}).toList(), }).toList(),
......
...@@ -191,7 +191,7 @@ void main() { ...@@ -191,7 +191,7 @@ void main() {
expect(_value, 'Soup'); expect(_value, 'Soup');
}); });
testWidgets('autovalidate is passed to super', (WidgetTester tester) async { testWidgets('autovalidateMode is passed to super', (WidgetTester tester) async {
int _validateCalled = 0; int _validateCalled = 0;
await tester.pumpWidget( await tester.pumpWidget(
...@@ -199,7 +199,7 @@ void main() { ...@@ -199,7 +199,7 @@ void main() {
home: Material( home: Material(
child: Center( child: Center(
child: TextFormField( child: TextFormField(
autovalidate: true, autovalidateMode: AutovalidateMode.always,
validator: (String value) { validator: (String value) {
_validateCalled++; _validateCalled++;
return null; return null;
...@@ -225,7 +225,7 @@ void main() { ...@@ -225,7 +225,7 @@ void main() {
child: Center( child: Center(
child: TextFormField( child: TextFormField(
enabled: true, enabled: true,
autovalidate: true, autovalidateMode: AutovalidateMode.always,
validator: (String value) { validator: (String value) {
_validateCalled += 1; _validateCalled += 1;
return null; return null;
...@@ -444,4 +444,46 @@ void main() { ...@@ -444,4 +444,46 @@ void main() {
final TextField widget = tester.widget(find.byType(TextField)); final TextField widget = tester.widget(find.byType(TextField));
expect(widget.autofillHints, equals(const <String>[AutofillHints.countryName])); expect(widget.autofillHints, equals(const <String>[AutofillHints.countryName]));
}); });
testWidgets('autovalidateMode is passed to super', (WidgetTester tester) async {
int _validateCalled = 0;
await tester.pumpWidget(
MaterialApp(
home: Material(
child: Scaffold(
body: TextFormField(
autovalidateMode: AutovalidateMode.onUserInteraction,
validator: (String value) {
_validateCalled++;
return null;
},
),
),
),
),
);
expect(_validateCalled, 0);
await tester.enterText(find.byType(TextField), 'a');
await tester.pump();
expect(_validateCalled, 1);
});
testWidgets('autovalidateMode and autovalidate should not be used at the same time', (WidgetTester tester) async {
expect(() async {
await tester.pumpWidget(
MaterialApp(
home: Material(
child: Scaffold(
body: TextFormField(
autovalidate: true,
autovalidateMode: AutovalidateMode.onUserInteraction,
),
),
),
),
);
}, throwsAssertionError);
});
} }
...@@ -89,7 +89,7 @@ void main() { ...@@ -89,7 +89,7 @@ void main() {
final GlobalKey<FormState> formKey = GlobalKey<FormState>(); final GlobalKey<FormState> formKey = GlobalKey<FormState>();
String errorText(String value) => value + '/error'; String errorText(String value) => value + '/error';
Widget builder(bool autovalidate) { Widget builder(AutovalidateMode autovalidateMode) {
return MaterialApp( return MaterialApp(
home: MediaQuery( home: MediaQuery(
data: const MediaQueryData(devicePixelRatio: 1.0), data: const MediaQueryData(devicePixelRatio: 1.0),
...@@ -99,7 +99,7 @@ void main() { ...@@ -99,7 +99,7 @@ void main() {
child: Material( child: Material(
child: Form( child: Form(
key: formKey, key: formKey,
autovalidate: autovalidate, autovalidateMode: autovalidateMode,
child: TextFormField( child: TextFormField(
validator: errorText, validator: errorText,
), ),
...@@ -112,11 +112,11 @@ void main() { ...@@ -112,11 +112,11 @@ void main() {
} }
// Start off not autovalidating. // Start off not autovalidating.
await tester.pumpWidget(builder(false)); await tester.pumpWidget(builder(AutovalidateMode.disabled));
Future<void> checkErrorText(String testValue) async { Future<void> checkErrorText(String testValue) async {
formKey.currentState.reset(); formKey.currentState.reset();
await tester.pumpWidget(builder(false)); await tester.pumpWidget(builder(AutovalidateMode.disabled));
await tester.enterText(find.byType(TextFormField), testValue); await tester.enterText(find.byType(TextFormField), testValue);
await tester.pump(); await tester.pump();
...@@ -128,7 +128,7 @@ void main() { ...@@ -128,7 +128,7 @@ void main() {
// Try again with autovalidation. Should validate immediately. // Try again with autovalidation. Should validate immediately.
formKey.currentState.reset(); formKey.currentState.reset();
await tester.pumpWidget(builder(true)); await tester.pumpWidget(builder(AutovalidateMode.always));
await tester.enterText(find.byType(TextFormField), testValue); await tester.enterText(find.byType(TextFormField), testValue);
await tester.pump(); await tester.pump();
...@@ -160,13 +160,13 @@ void main() { ...@@ -160,13 +160,13 @@ void main() {
key: fieldKey1, key: fieldKey1,
initialValue: validString, initialValue: validString,
validator: validator, validator: validator,
autovalidate: true autovalidateMode: AutovalidateMode.always,
), ),
TextFormField( TextFormField(
key: fieldKey2, key: fieldKey2,
initialValue: validString, initialValue: validString,
validator: validator, validator: validator,
autovalidate: true autovalidateMode: AutovalidateMode.always,
), ),
], ],
), ),
...@@ -207,13 +207,13 @@ void main() { ...@@ -207,13 +207,13 @@ void main() {
key: fieldKey1, key: fieldKey1,
initialValue: validString, initialValue: validString,
validator: validator, validator: validator,
autovalidate: false, autovalidateMode: AutovalidateMode.disabled,
), ),
TextFormField( TextFormField(
key: fieldKey2, key: fieldKey2,
initialValue: '', initialValue: '',
validator: validator, validator: validator,
autovalidate: false, autovalidateMode: AutovalidateMode.disabled,
), ),
], ],
), ),
...@@ -249,7 +249,7 @@ void main() { ...@@ -249,7 +249,7 @@ void main() {
child: Material( child: Material(
child: Form( child: Form(
key: formKey, key: formKey,
autovalidate: true, autovalidateMode: AutovalidateMode.always,
child: ListView( child: ListView(
children: <Widget>[ children: <Widget>[
TextFormField( TextFormField(
...@@ -580,4 +580,273 @@ void main() { ...@@ -580,4 +580,273 @@ void main() {
formKey.currentState.save(); formKey.currentState.save();
expect(formKey.currentState.validate(), isTrue); expect(formKey.currentState.validate(), isTrue);
}); });
testWidgets('Does not auto-validate before value changes when autovalidateMode is set to onUserInteraction', (WidgetTester tester) async {
FormFieldState<String> formFieldState;
String errorText(String value) => '$value/error';
Widget builder() {
return MaterialApp(
home: MediaQuery(
data: const MediaQueryData(devicePixelRatio: 1.0),
child: Directionality(
textDirection: TextDirection.ltr,
child: Center(
child: Material(
child: FormField<String>(
initialValue: 'foo',
autovalidateMode: AutovalidateMode.onUserInteraction,
builder: (FormFieldState<String> state) {
formFieldState = state;
return Container();
},
validator: errorText,
),
),
),
),
),
);
}
await tester.pumpWidget(builder());
// The form field has no error.
expect(formFieldState.hasError, isFalse);
// No error widget is visible.
expect(find.text(errorText('foo')), findsNothing);
});
testWidgets('auto-validate before value changes if autovalidateMode was set to always', (WidgetTester tester) async {
FormFieldState<String> formFieldState;
String errorText(String value) => '$value/error';
Widget builder() {
return MaterialApp(
home: MediaQuery(
data: const MediaQueryData(devicePixelRatio: 1.0),
child: Directionality(
textDirection: TextDirection.ltr,
child: Center(
child: Material(
child: FormField<String>(
initialValue: 'foo',
autovalidateMode: AutovalidateMode.always,
builder: (FormFieldState<String> state) {
formFieldState = state;
return Container();
},
validator: errorText,
),
),
),
),
),
);
}
await tester.pumpWidget(builder());
expect(formFieldState.hasError, isTrue);
});
testWidgets('Form auto-validates form fields only after one of them changes if autovalidateMode is onUserInteraction', (WidgetTester tester) async {
const String initialValue = 'foo';
String errorText(String value) => 'error/$value';
Widget builder() {
return MaterialApp(
home: Directionality(
textDirection: TextDirection.ltr,
child: Center(
child: Material(
child: Form(
autovalidateMode: AutovalidateMode.onUserInteraction,
child: Column(
children: <Widget>[
TextFormField(
initialValue: initialValue,
validator: errorText,
),
TextFormField(
initialValue: initialValue,
validator: errorText,
),
TextFormField(
initialValue: initialValue,
validator: errorText,
)
],
),
),
),
),
),
);
}
// Makes sure the Form widget won't autovalidate the form fields
// after rebuilds if there is not user interaction.
await tester.pumpWidget(builder());
await tester.pumpWidget(builder());
// We expect no validation error text being shown.
expect(find.text(errorText(initialValue)), findsNothing);
// Set a empty string into the first form field to
// trigger the fields validators.
await tester.enterText(find.byType(TextFormField).first, '');
await tester.pump();
// Now we expect the errors to be shown for the first Text Field and
// for the next two form fields that have their contents unchanged.
expect(find.text(errorText('')), findsOneWidget);
expect(find.text(errorText(initialValue)), findsNWidgets(2));
});
testWidgets('Form auto-validates form fields even before any have changed if autovalidateMode is set to always', (WidgetTester tester) async {
String errorText(String value) => 'error/$value';
Widget builder() {
return MaterialApp(
home: Directionality(
textDirection: TextDirection.ltr,
child: Center(
child: Material(
child: Form(
autovalidateMode: AutovalidateMode.always,
child: TextFormField(
validator: errorText,
),
),
),
),
),
);
}
// The issue only happens on the second build so we
// need to rebuild the tree twice.
await tester.pumpWidget(builder());
await tester.pumpWidget(builder());
// We expect validation error text being shown.
expect(find.text(errorText('')), findsOneWidget);
});
testWidgets('autovalidate parameter is still used if true', (WidgetTester tester) async {
FormFieldState<String> formFieldState;
String errorText(String value) => '$value/error';
Widget builder() {
return MaterialApp(
home: MediaQuery(
data: const MediaQueryData(devicePixelRatio: 1.0),
child: Directionality(
textDirection: TextDirection.ltr,
child: Center(
child: Material(
child: FormField<String>(
initialValue: 'foo',
autovalidate: true,
builder: (FormFieldState<String> state) {
formFieldState = state;
return Container();
},
validator: errorText,
),
),
),
),
),
);
}
await tester.pumpWidget(builder());
expect(formFieldState.hasError, isTrue);
});
testWidgets('Form.reset() resets form fields, and auto validation will only happen on the next user interaction if autovalidateMode is onUserInteraction', (WidgetTester tester) async {
final GlobalKey<FormState> formState = GlobalKey<FormState>();
String errorText(String value) => '$value/error';
Widget builder() {
return MaterialApp(
theme: ThemeData(),
home: MediaQuery(
data: const MediaQueryData(devicePixelRatio: 1.0),
child: Directionality(
textDirection: TextDirection.ltr,
child: Center(
child: Form(
key: formState,
autovalidateMode: AutovalidateMode.onUserInteraction,
child: Material(
child: TextFormField(
initialValue: 'foo',
validator: errorText,
),
),
),
),
),
),
);
}
await tester.pumpWidget(builder());
// No error text is visible yet.
expect(find.text(errorText('foo')), findsNothing);
await tester.enterText(find.byType(TextFormField), 'bar');
await tester.pumpAndSettle();
await tester.pump();
expect(find.text(errorText('bar')), findsOneWidget);
// Resetting the form state should remove the error text.
formState.currentState.reset();
await tester.pump();
expect(find.text(errorText('bar')), findsNothing);
});
testWidgets('Form.autovalidateMode and Form.autovalidate should not be used at the same time', (WidgetTester tester) async {
Widget builder() {
return MaterialApp(
home: MediaQuery(
data: const MediaQueryData(devicePixelRatio: 1.0),
child: Directionality(
textDirection: TextDirection.ltr,
child: Form(
autovalidate: true,
autovalidateMode: AutovalidateMode.disabled,
child: Container(),
),
),
),
);
}
expect(() => builder(), throwsAssertionError);
});
testWidgets('FormField.autovalidateMode and FormField.autovalidate should not be used at the same time', (WidgetTester tester) async {
Widget builder() {
return MaterialApp(
home: MediaQuery(
data: const MediaQueryData(devicePixelRatio: 1.0),
child: Directionality(
textDirection: TextDirection.ltr,
child: FormField<String>(
autovalidate: true,
autovalidateMode: AutovalidateMode.disabled,
builder: (_) {
return Container();
},
),
),
),
);
}
expect(() => builder(), throwsAssertionError);
});
} }
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