mirror of
https://github.com/flutter/flutter.git
synced 2025-06-03 00:51:18 +00:00
add forceErrorText to FormField & TextFormField. (#132903)
Introducing the `forceErrorText` property to both `FormField` and `TextFormField`. With this addition, we gain the capability to trigger an error state and provide an error message without invoking the `validator` method. While the idea of making the `Validator` method work asynchronously may be appealing, it could introduce significant complexity to our current form field implementation. Additionally, this approach might not be suitable for all developers, as discussed by @justinmc in this [comment](https://github.com/flutter/flutter/issues/56414#issuecomment-624960263). This PR try to address this issue by adding `forceErrorText` property allowing us to force the error to the `FormField` or `TextFormField` at our own base making it possible to preform some async operations without the need for any hacks while keep the ability to check for errors if we call `formKey.currentState!.validate()`. Here is an example: <details> <summary>Code Example</summary> ```dart import 'package:flutter/material.dart'; void main() { runApp( const MaterialApp(home: MyHomePage()), ); } class MyHomePage extends StatefulWidget { const MyHomePage({ super.key, }); @override State<MyHomePage> createState() => _MyHomePageState(); } class _MyHomePageState extends State<MyHomePage> { final key = GlobalKey<FormState>(); String? forcedErrorText; Future<void> handleValidation() async { // simulate some async work.. await Future.delayed(const Duration(seconds: 3)); setState(() { forcedErrorText = 'this username is not available.'; }); // wait for build to run and then check. // // this is not required though, as the error would already be showing. WidgetsBinding.instance.addPostFrameCallback((_) { print(key.currentState!.validate()); }); } @override Widget build(BuildContext context) { print('build'); return Scaffold( floatingActionButton: FloatingActionButton(onPressed: handleValidation), body: Form( key: key, child: Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ Padding( padding: const EdgeInsets.symmetric(horizontal: 30), child: TextFormField( forceErrorText: forcedErrorText, ), ), ], ), ), ), ); } } ``` </details> Related to #9688 & #56414. Happy to hear your thoughts on this.
This commit is contained in:
parent
6c06abbb55
commit
f54dfcd27d
138
examples/api/lib/material/text_form_field/text_form_field.2.dart
Normal file
138
examples/api/lib/material/text_form_field/text_form_field.2.dart
Normal file
@ -0,0 +1,138 @@
|
||||
// Copyright 2014 The Flutter Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
/// Flutter code sample for [TextFormField].
|
||||
|
||||
const Duration kFakeHttpRequestDuration = Duration(seconds: 3);
|
||||
|
||||
void main() => runApp(const TextFormFieldExampleApp());
|
||||
|
||||
class TextFormFieldExampleApp extends StatelessWidget {
|
||||
const TextFormFieldExampleApp({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return const MaterialApp(
|
||||
home: TextFormFieldExample(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class TextFormFieldExample extends StatefulWidget {
|
||||
const TextFormFieldExample({super.key});
|
||||
|
||||
@override
|
||||
State<TextFormFieldExample> createState() => _TextFormFieldExampleState();
|
||||
}
|
||||
|
||||
class _TextFormFieldExampleState extends State<TextFormFieldExample> {
|
||||
final TextEditingController controller = TextEditingController();
|
||||
final GlobalKey<FormState> formKey = GlobalKey<FormState>();
|
||||
String? forceErrorText;
|
||||
bool isLoading = false;
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
controller.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
String? validator(String? value) {
|
||||
if (value == null || value.isEmpty) {
|
||||
return 'This field is required';
|
||||
}
|
||||
if (value.length != value.replaceAll(' ', '').length) {
|
||||
return 'Username must not contain any spaces';
|
||||
}
|
||||
if (int.tryParse(value[0]) != null) {
|
||||
return 'Username must not start with a number';
|
||||
}
|
||||
if (value.length <= 2) {
|
||||
return 'Username should be at least 3 characters long';
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
void onChanged(String value) {
|
||||
// Nullify forceErrorText if the input changed.
|
||||
if (forceErrorText != null) {
|
||||
setState(() {
|
||||
forceErrorText = null;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> onSave() async {
|
||||
// Providing a default value in case this was called on the
|
||||
// first frame, the [fromKey.currentState] will be null.
|
||||
final bool isValid = formKey.currentState?.validate() ?? false;
|
||||
if (!isValid) {
|
||||
return;
|
||||
}
|
||||
|
||||
setState(() => isLoading = true);
|
||||
final String? errorText = await validateUsernameFromServer(controller.text);
|
||||
|
||||
if (context.mounted) {
|
||||
setState(() => isLoading = false);
|
||||
|
||||
if (errorText != null) {
|
||||
setState(() {
|
||||
forceErrorText = errorText;
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Material(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 24.0),
|
||||
child: Center(
|
||||
child: Form(
|
||||
key: formKey,
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: <Widget>[
|
||||
TextFormField(
|
||||
forceErrorText: forceErrorText,
|
||||
controller: controller,
|
||||
decoration: const InputDecoration(
|
||||
hintText: 'Please write a username',
|
||||
),
|
||||
validator: validator,
|
||||
onChanged: onChanged,
|
||||
),
|
||||
const SizedBox(height: 40.0),
|
||||
if (isLoading)
|
||||
const CircularProgressIndicator()
|
||||
else
|
||||
TextButton(
|
||||
onPressed: onSave,
|
||||
child: const Text('Save'),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Future<String?> validateUsernameFromServer(String username) async {
|
||||
final Set<String> takenUsernames = <String>{'jack', 'alex'};
|
||||
|
||||
await Future<void>.delayed(kFakeHttpRequestDuration);
|
||||
|
||||
final bool isValid = !takenUsernames.contains(username);
|
||||
if (isValid) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return 'Username $username is already taken';
|
||||
}
|
@ -0,0 +1,82 @@
|
||||
// Copyright 2014 The Flutter Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_api_samples/material/text_form_field/text_form_field.2.dart' as example;
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
|
||||
void main() {
|
||||
group('TextFormFieldExample2 Widget Tests', () {
|
||||
testWidgets('Input validation handles empty, incorrect, and short usernames', (WidgetTester tester) async {
|
||||
await tester.pumpWidget(const example.TextFormFieldExampleApp());
|
||||
final Finder textFormField = find.byType(TextFormField);
|
||||
final Finder saveButton = find.byType(TextButton);
|
||||
|
||||
await tester.enterText(textFormField, '');
|
||||
await tester.pump();
|
||||
await tester.tap(saveButton);
|
||||
await tester.pump();
|
||||
expect(find.text('This field is required'), findsOneWidget);
|
||||
|
||||
await tester.enterText(textFormField, 'jo hn');
|
||||
await tester.tap(saveButton);
|
||||
await tester.pump();
|
||||
expect(find.text('Username must not contain any spaces'), findsOneWidget);
|
||||
|
||||
await tester.enterText(textFormField, 'jo');
|
||||
await tester.tap(saveButton);
|
||||
await tester.pump();
|
||||
expect(find.text('Username should be at least 3 characters long'), findsOneWidget);
|
||||
|
||||
await tester.enterText(textFormField, '1jo');
|
||||
await tester.tap(saveButton);
|
||||
await tester.pump();
|
||||
expect(find.text('Username must not start with a number'), findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets('Async validation feedback is handled correctly', (WidgetTester tester) async {
|
||||
await tester.pumpWidget(const example.TextFormFieldExampleApp());
|
||||
final Finder textFormField = find.byType(TextFormField);
|
||||
final Finder saveButton = find.byType(TextButton);
|
||||
|
||||
// Simulate entering a username already taken.
|
||||
await tester.enterText(textFormField, 'jack');
|
||||
await tester.pump();
|
||||
await tester.tap(saveButton);
|
||||
await tester.pump();
|
||||
expect(find.text('Username jack is already taken'), findsNothing);
|
||||
await tester.pump(example.kFakeHttpRequestDuration);
|
||||
expect(find.text('Username jack is already taken'), findsOneWidget);
|
||||
|
||||
await tester.enterText(textFormField, 'alex');
|
||||
await tester.pump();
|
||||
await tester.tap(saveButton);
|
||||
await tester.pump();
|
||||
expect(find.text('Username alex is already taken'), findsNothing);
|
||||
await tester.pump(example.kFakeHttpRequestDuration);
|
||||
expect(find.text('Username alex is already taken'), findsOneWidget);
|
||||
|
||||
await tester.enterText(textFormField, 'jack');
|
||||
await tester.pump();
|
||||
await tester.tap(saveButton);
|
||||
await tester.pump();
|
||||
expect(find.text('Username jack is already taken'), findsNothing);
|
||||
await tester.pump(example.kFakeHttpRequestDuration);
|
||||
expect(find.text('Username jack is already taken'), findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets('Loading spinner displays correctly when saving', (WidgetTester tester) async {
|
||||
await tester.pumpWidget(const example.TextFormFieldExampleApp());
|
||||
final Finder textFormField = find.byType(TextFormField);
|
||||
final Finder saveButton = find.byType(TextButton);
|
||||
await tester.enterText(textFormField, 'alexander');
|
||||
await tester.pump();
|
||||
await tester.tap(saveButton);
|
||||
await tester.pump();
|
||||
expect(find.byType(CircularProgressIndicator), findsOneWidget);
|
||||
await tester.pump(example.kFakeHttpRequestDuration);
|
||||
expect(find.byType(CircularProgressIndicator), findsNothing);
|
||||
});
|
||||
});
|
||||
}
|
@ -81,6 +81,13 @@ export 'package:flutter/services.dart' show SmartDashesType, SmartQuotesType;
|
||||
/// ** See code in examples/api/lib/material/text_form_field/text_form_field.1.dart **
|
||||
/// {@end-tool}
|
||||
///
|
||||
/// {@tool dartpad}
|
||||
/// This example shows how to force an error text to the field after making
|
||||
/// an asynchronous call.
|
||||
///
|
||||
/// ** See code in examples/api/lib/material/text_form_field/text_form_field.2.dart **
|
||||
/// {@end-tool}
|
||||
///
|
||||
/// See also:
|
||||
///
|
||||
/// * <https://material.io/design/components/text-fields.html>
|
||||
@ -105,6 +112,7 @@ class TextFormField extends FormField<String> {
|
||||
this.controller,
|
||||
String? initialValue,
|
||||
FocusNode? focusNode,
|
||||
super.forceErrorText,
|
||||
InputDecoration? decoration = const InputDecoration(),
|
||||
TextInputType? keyboardType,
|
||||
TextCapitalization textCapitalization = TextCapitalization.none,
|
||||
|
@ -234,8 +234,7 @@ class FormState extends State<Form> {
|
||||
void _fieldDidChange() {
|
||||
widget.onChanged?.call();
|
||||
|
||||
_hasInteractedByUser = _fields
|
||||
.any((FormFieldState<dynamic> field) => field._hasInteractedByUser.value);
|
||||
_hasInteractedByUser = _fields.any((FormFieldState<dynamic> field) => field._hasInteractedByUser.value);
|
||||
_forceRebuild();
|
||||
}
|
||||
|
||||
@ -337,7 +336,6 @@ class FormState extends State<Form> {
|
||||
return _validate();
|
||||
}
|
||||
|
||||
|
||||
/// Validates every [FormField] that is a descendant of this [Form], and
|
||||
/// returns a [Set] of [FormFieldState] of the invalid field(s) only, if any.
|
||||
///
|
||||
@ -393,8 +391,8 @@ class _FormScope extends InheritedWidget {
|
||||
required super.child,
|
||||
required FormState formState,
|
||||
required int generation,
|
||||
}) : _formState = formState,
|
||||
_generation = generation;
|
||||
}) : _formState = formState,
|
||||
_generation = generation;
|
||||
|
||||
final FormState _formState;
|
||||
|
||||
@ -454,6 +452,7 @@ class FormField<T> extends StatefulWidget {
|
||||
super.key,
|
||||
required this.builder,
|
||||
this.onSaved,
|
||||
this.forceErrorText,
|
||||
this.validator,
|
||||
this.initialValue,
|
||||
this.enabled = true,
|
||||
@ -465,6 +464,24 @@ class FormField<T> extends StatefulWidget {
|
||||
/// [FormState.save].
|
||||
final FormFieldSetter<T>? onSaved;
|
||||
|
||||
/// An optional property that forces the [FormFieldState] into an error state
|
||||
/// by directly setting the [FormFieldState.errorText] property without
|
||||
/// running the validator function.
|
||||
///
|
||||
/// When the [forceErrorText] property is provided, the [FormFieldState.errorText]
|
||||
/// will be set to the provided value, causing the form field to be considered
|
||||
/// invalid and to display the error message specified.
|
||||
///
|
||||
/// When [validator] is provided, [forceErrorText] will override any error that it
|
||||
/// returns. [validator] will not be called unless [forceErrorText] is null.
|
||||
///
|
||||
/// See also:
|
||||
///
|
||||
/// * [InputDecoration.errorText], which is used to display error messages in the text
|
||||
/// field's decoration without effecting the field's state. When [forceErrorText] is
|
||||
/// not null, it will override [InputDecoration.errorText] value.
|
||||
final String? forceErrorText;
|
||||
|
||||
/// An optional method that validates an input. Returns an error string to
|
||||
/// display if the input is invalid, or null otherwise.
|
||||
///
|
||||
@ -533,7 +550,9 @@ class FormField<T> extends StatefulWidget {
|
||||
/// for use in constructing the form field's widget.
|
||||
class FormFieldState<T> extends State<FormField<T>> with RestorationMixin {
|
||||
late T? _value = widget.initialValue;
|
||||
final RestorableStringN _errorText = RestorableStringN(null);
|
||||
// Marking it as late, so it can be registered
|
||||
// with the value provided by [forceErrorText].
|
||||
late final RestorableStringN _errorText;
|
||||
final RestorableBool _hasInteractedByUser = RestorableBool(false);
|
||||
final FocusNode _focusNode = FocusNode();
|
||||
|
||||
@ -541,8 +560,12 @@ class FormFieldState<T> extends State<FormField<T>> with RestorationMixin {
|
||||
T? get value => _value;
|
||||
|
||||
/// The current validation error returned by the [FormField.validator]
|
||||
/// callback, or null if no errors have been triggered. This only updates when
|
||||
/// [validate] is called.
|
||||
/// callback, or the manually provided error message using the
|
||||
/// [FormField.forceErrorText] property.
|
||||
///
|
||||
/// This property is automatically updated when [validate] is called and the
|
||||
/// [FormField.validator] callback is invoked, or If [FormField.forceErrorText] is set
|
||||
/// directly to a non-null value.
|
||||
String? get errorText => _errorText.value;
|
||||
|
||||
/// True if this field has any validation errors.
|
||||
@ -562,7 +585,9 @@ class FormFieldState<T> extends State<FormField<T>> with RestorationMixin {
|
||||
/// See also:
|
||||
///
|
||||
/// * [validate], which may update [errorText] and [hasError].
|
||||
bool get isValid => widget.validator?.call(_value) == null;
|
||||
///
|
||||
/// * [FormField.forceErrorText], which also may update [errorText] and [hasError].
|
||||
bool get isValid => widget.forceErrorText == null && widget.validator?.call(_value) == null;
|
||||
|
||||
/// Calls the [FormField]'s onSaved method with the current value.
|
||||
void save() {
|
||||
@ -579,9 +604,10 @@ class FormFieldState<T> extends State<FormField<T>> with RestorationMixin {
|
||||
Form.maybeOf(context)?._fieldDidChange();
|
||||
}
|
||||
|
||||
/// Calls [FormField.validator] to set the [errorText]. Returns true if there
|
||||
/// were no errors.
|
||||
/// Calls [FormField.validator] to set the [errorText] only if [FormField.forceErrorText] is null.
|
||||
/// When [FormField.forceErrorText] is not null, [FormField.validator] will not be called.
|
||||
///
|
||||
/// Returns true if there were no errors.
|
||||
/// See also:
|
||||
///
|
||||
/// * [isValid], which passively gets the validity without setting
|
||||
@ -594,6 +620,11 @@ class FormFieldState<T> extends State<FormField<T>> with RestorationMixin {
|
||||
}
|
||||
|
||||
void _validate() {
|
||||
if (widget.forceErrorText != null) {
|
||||
_errorText.value = widget.forceErrorText;
|
||||
// Skip validating if error is forced.
|
||||
return;
|
||||
}
|
||||
if (widget.validator != null) {
|
||||
_errorText.value = widget.validator!(_value);
|
||||
} else {
|
||||
@ -643,6 +674,20 @@ class FormFieldState<T> extends State<FormField<T>> with RestorationMixin {
|
||||
super.deactivate();
|
||||
}
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_errorText = RestorableStringN(widget.forceErrorText);
|
||||
}
|
||||
|
||||
@override
|
||||
void didUpdateWidget(FormField<T> oldWidget) {
|
||||
super.didUpdateWidget(oldWidget);
|
||||
if (widget.forceErrorText != oldWidget.forceErrorText) {
|
||||
_errorText.value = widget.forceErrorText;
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_errorText.dispose();
|
||||
|
@ -1714,4 +1714,84 @@ void main() {
|
||||
expect(stateKey.currentState!.value,'initialValue');
|
||||
expect(value, 'initialValue');
|
||||
});
|
||||
|
||||
testWidgets(
|
||||
'isValid returns false when forceErrorText is set and will change error display',
|
||||
(WidgetTester tester) async {
|
||||
final GlobalKey<FormFieldState<String>> fieldKey1 = GlobalKey<FormFieldState<String>>();
|
||||
final GlobalKey<FormFieldState<String>> fieldKey2 = GlobalKey<FormFieldState<String>>();
|
||||
const String forceErrorText = 'Forcing error.';
|
||||
const String validString = 'Valid string';
|
||||
String? validator(String? s) => s == validString ? null : 'Error text';
|
||||
|
||||
await tester.pumpWidget(
|
||||
MaterialApp(
|
||||
home: MediaQuery(
|
||||
data: const MediaQueryData(),
|
||||
child: Directionality(
|
||||
textDirection: TextDirection.ltr,
|
||||
child: Center(
|
||||
child: Material(
|
||||
child: Form(
|
||||
child: ListView(
|
||||
children: <Widget>[
|
||||
TextFormField(
|
||||
key: fieldKey1,
|
||||
initialValue: validString,
|
||||
validator: validator,
|
||||
autovalidateMode: AutovalidateMode.disabled,
|
||||
),
|
||||
TextFormField(
|
||||
key: fieldKey2,
|
||||
initialValue: '',
|
||||
forceErrorText: forceErrorText,
|
||||
validator: validator,
|
||||
autovalidateMode: AutovalidateMode.disabled,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
expect(fieldKey1.currentState!.isValid, isTrue);
|
||||
expect(fieldKey1.currentState!.hasError, isFalse);
|
||||
expect(fieldKey2.currentState!.isValid, isFalse);
|
||||
expect(fieldKey2.currentState!.hasError, isTrue);
|
||||
});
|
||||
|
||||
testWidgets('forceErrorText will override InputDecoration.error when both are provided', (WidgetTester tester) async {
|
||||
const String forceErrorText = 'Forcing error';
|
||||
const String decorationErrorText = 'Decoration';
|
||||
|
||||
await tester.pumpWidget(
|
||||
MaterialApp(
|
||||
home: MediaQuery(
|
||||
data: const MediaQueryData(),
|
||||
child: Directionality(
|
||||
textDirection: TextDirection.ltr,
|
||||
child: Center(
|
||||
child: Material(
|
||||
child: Form(
|
||||
child: TextFormField(
|
||||
forceErrorText: forceErrorText,
|
||||
decoration: const InputDecoration(
|
||||
errorText: decorationErrorText,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
expect(find.text(forceErrorText), findsOne);
|
||||
expect(find.text(decorationErrorText), findsNothing);
|
||||
});
|
||||
}
|
||||
|
@ -1060,8 +1060,162 @@ void main() {
|
||||
expect(fieldKey.currentState!.hasInteractedByUser, isFalse);
|
||||
});
|
||||
|
||||
testWidgets('Validator is nullified and error text behaves accordingly',
|
||||
(WidgetTester tester) async {
|
||||
testWidgets('forceErrorText forces an error state when first init', (WidgetTester tester) async {
|
||||
const String forceErrorText = 'Forcing error.';
|
||||
|
||||
Widget builder(AutovalidateMode autovalidateMode) {
|
||||
return MaterialApp(
|
||||
home: MediaQuery(
|
||||
data: const MediaQueryData(),
|
||||
child: Directionality(
|
||||
textDirection: TextDirection.ltr,
|
||||
child: Center(
|
||||
child: Material(
|
||||
child: Form(
|
||||
autovalidateMode: autovalidateMode,
|
||||
child: TextFormField(
|
||||
forceErrorText: forceErrorText,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
await tester.pumpWidget(builder(AutovalidateMode.disabled));
|
||||
expect(find.text(forceErrorText), findsOne);
|
||||
});
|
||||
|
||||
testWidgets(
|
||||
'Validate returns false when forceErrorText is non-null even when validator returns a null value',
|
||||
(WidgetTester tester) async {
|
||||
final GlobalKey<FormState> formKey = GlobalKey<FormState>();
|
||||
const String forceErrorText = 'Forcing error';
|
||||
|
||||
await tester.pumpWidget(
|
||||
MaterialApp(
|
||||
home: MediaQuery(
|
||||
data: const MediaQueryData(),
|
||||
child: Directionality(
|
||||
textDirection: TextDirection.ltr,
|
||||
child: Center(
|
||||
child: Material(
|
||||
child: Form(
|
||||
key: formKey,
|
||||
child: TextFormField(
|
||||
forceErrorText: forceErrorText,
|
||||
validator: (String? value) => null,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
expect(find.text(forceErrorText), findsOne);
|
||||
final bool isValid = formKey.currentState!.validate();
|
||||
expect(isValid, isFalse);
|
||||
|
||||
await tester.pump();
|
||||
expect(find.text(forceErrorText), findsOne);
|
||||
|
||||
});
|
||||
|
||||
testWidgets('forceErrorText forces an error state only after setting it to a non-null value', (WidgetTester tester) async {
|
||||
final GlobalKey<FormState> formKey = GlobalKey<FormState>();
|
||||
const String errorText = 'Forcing Error Text';
|
||||
Widget builder(AutovalidateMode autovalidateMode, String? forceErrorText) {
|
||||
return MaterialApp(
|
||||
home: MediaQuery(
|
||||
data: const MediaQueryData(),
|
||||
child: Directionality(
|
||||
textDirection: TextDirection.ltr,
|
||||
child: Center(
|
||||
child: Material(
|
||||
child: Form(
|
||||
key: formKey,
|
||||
autovalidateMode: autovalidateMode,
|
||||
child: TextFormField(
|
||||
forceErrorText: forceErrorText,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
await tester.pumpWidget(builder(AutovalidateMode.disabled, null));
|
||||
final bool isValid = formKey.currentState!.validate();
|
||||
expect(isValid, true);
|
||||
expect(find.text(errorText), findsNothing);
|
||||
await tester.pumpWidget(builder(AutovalidateMode.disabled, errorText));
|
||||
expect(find.text(errorText), findsOne);
|
||||
});
|
||||
|
||||
testWidgets('Validator will not be called if forceErrorText is provided', (WidgetTester tester) async {
|
||||
final GlobalKey<FormState> formKey = GlobalKey<FormState>();
|
||||
const String forceErrorText = 'Forcing error.';
|
||||
const String validatorErrorText = 'this error should not appear as we override it with forceErrorText';
|
||||
bool didCallValidator = false;
|
||||
|
||||
Widget builder(AutovalidateMode autovalidateMode) {
|
||||
return MaterialApp(
|
||||
home: MediaQuery(
|
||||
data: const MediaQueryData(),
|
||||
child: Directionality(
|
||||
textDirection: TextDirection.ltr,
|
||||
child: Center(
|
||||
child: Material(
|
||||
child: Form(
|
||||
key: formKey,
|
||||
autovalidateMode: autovalidateMode,
|
||||
child: TextFormField(
|
||||
forceErrorText: forceErrorText,
|
||||
validator: (String? value) {
|
||||
didCallValidator = true;
|
||||
return validatorErrorText;
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// Start off not autovalidating.
|
||||
await tester.pumpWidget(builder(AutovalidateMode.disabled));
|
||||
expect(find.text(forceErrorText), findsOne);
|
||||
expect(find.text(validatorErrorText), findsNothing);
|
||||
|
||||
formKey.currentState!.reset();
|
||||
await tester.pump();
|
||||
expect(find.text(forceErrorText), findsNothing);
|
||||
expect(find.text(validatorErrorText), findsNothing);
|
||||
|
||||
// We have to manually validate if we're not autovalidating.
|
||||
formKey.currentState!.validate();
|
||||
await tester.pump();
|
||||
|
||||
expect(didCallValidator, isFalse);
|
||||
expect(find.text(forceErrorText), findsOne);
|
||||
expect(find.text(validatorErrorText), findsNothing);
|
||||
|
||||
// Try again with autovalidation. Should validate immediately.
|
||||
await tester.pumpWidget(builder(AutovalidateMode.always));
|
||||
|
||||
expect(didCallValidator, isFalse);
|
||||
expect(find.text(forceErrorText), findsOne);
|
||||
expect(find.text(validatorErrorText), findsNothing);
|
||||
});
|
||||
|
||||
testWidgets('Validator is nullified and error text behaves accordingly', (WidgetTester tester) async {
|
||||
final GlobalKey<FormState> formKey = GlobalKey<FormState>();
|
||||
bool useValidator = false;
|
||||
late StateSetter setState;
|
||||
|
Loading…
Reference in New Issue
Block a user