1825 lines
58 KiB
Dart
1825 lines
58 KiB
Dart
// 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/rendering.dart';
|
|
import 'package:flutter/services.dart';
|
|
import 'package:flutter_test/flutter_test.dart';
|
|
|
|
void main() {
|
|
testWidgets('onSaved callback is called', (WidgetTester tester) async {
|
|
final formKey = GlobalKey<FormState>();
|
|
String? fieldValue;
|
|
|
|
Widget builder() {
|
|
return MaterialApp(
|
|
home: MediaQuery(
|
|
data: const MediaQueryData(),
|
|
child: Directionality(
|
|
textDirection: TextDirection.ltr,
|
|
child: Center(
|
|
child: Material(
|
|
child: Form(
|
|
key: formKey,
|
|
child: TextFormField(
|
|
onSaved: (String? value) {
|
|
fieldValue = value;
|
|
},
|
|
),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
await tester.pumpWidget(builder());
|
|
|
|
expect(fieldValue, isNull);
|
|
|
|
Future<void> checkText(String testValue) async {
|
|
await tester.enterText(find.byType(TextFormField), testValue);
|
|
formKey.currentState!.save();
|
|
// Pumping is unnecessary because callback happens regardless of frames.
|
|
expect(fieldValue, equals(testValue));
|
|
}
|
|
|
|
await checkText('Test');
|
|
await checkText('');
|
|
});
|
|
|
|
testWidgets('onChanged callback is called', (WidgetTester tester) async {
|
|
String? fieldValue;
|
|
|
|
Widget builder() {
|
|
return MaterialApp(
|
|
home: MediaQuery(
|
|
data: const MediaQueryData(),
|
|
child: Directionality(
|
|
textDirection: TextDirection.ltr,
|
|
child: Center(
|
|
child: Material(
|
|
child: Form(
|
|
child: TextField(
|
|
onChanged: (String value) {
|
|
fieldValue = value;
|
|
},
|
|
),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
await tester.pumpWidget(builder());
|
|
|
|
expect(fieldValue, isNull);
|
|
|
|
Future<void> checkText(String testValue) async {
|
|
await tester.enterText(find.byType(TextField), testValue);
|
|
// Pumping is unnecessary because callback happens regardless of frames.
|
|
expect(fieldValue, equals(testValue));
|
|
}
|
|
|
|
await checkText('Test');
|
|
await checkText('');
|
|
});
|
|
|
|
testWidgets('onReset callback is called', (WidgetTester tester) async {
|
|
final formKey = GlobalKey<FormState>();
|
|
var resetCalled = false;
|
|
|
|
await tester.pumpWidget(
|
|
MaterialApp(
|
|
home: Form(
|
|
key: formKey,
|
|
child: FormField<String>(
|
|
builder: (_) => const SizedBox.shrink(),
|
|
onReset: () {
|
|
resetCalled = true;
|
|
},
|
|
),
|
|
),
|
|
),
|
|
);
|
|
|
|
expect(resetCalled, isFalse);
|
|
|
|
formKey.currentState!.reset();
|
|
await tester.pump();
|
|
|
|
expect(resetCalled, isTrue);
|
|
});
|
|
|
|
testWidgets('Validator sets the error text only when validate is called', (
|
|
WidgetTester tester,
|
|
) async {
|
|
final formKey = GlobalKey<FormState>();
|
|
String? errorText(String? value) => '${value ?? ''}/error';
|
|
|
|
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(validator: errorText),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
// Start off not autovalidating.
|
|
await tester.pumpWidget(builder(AutovalidateMode.disabled));
|
|
|
|
Future<void> checkErrorText(String testValue) async {
|
|
formKey.currentState!.reset();
|
|
await tester.pumpWidget(builder(AutovalidateMode.disabled));
|
|
await tester.enterText(find.byType(TextFormField), testValue);
|
|
await tester.pump();
|
|
|
|
// We have to manually validate if we're not autovalidating.
|
|
expect(find.text(errorText(testValue)!), findsNothing);
|
|
formKey.currentState!.validate();
|
|
await tester.pump();
|
|
expect(find.text(errorText(testValue)!), findsOneWidget);
|
|
|
|
// Try again with autovalidation. Should validate immediately.
|
|
formKey.currentState!.reset();
|
|
await tester.pumpWidget(builder(AutovalidateMode.always));
|
|
await tester.enterText(find.byType(TextFormField), testValue);
|
|
await tester.pump();
|
|
|
|
expect(find.text(errorText(testValue)!), findsOneWidget);
|
|
}
|
|
|
|
await checkErrorText('Test');
|
|
await checkErrorText('');
|
|
});
|
|
|
|
for (final test in <_PlatformAnnounceScenario>[
|
|
_PlatformAnnounceScenario(
|
|
supportsAnnounce: false,
|
|
testName:
|
|
'Should announce only the first error message when validate returns errors and announce = false',
|
|
),
|
|
_PlatformAnnounceScenario(
|
|
supportsAnnounce: true,
|
|
testName:
|
|
'Should not announce error message when validate returns errors and announce = true',
|
|
),
|
|
]) {
|
|
testWidgets(test.testName, (WidgetTester tester) async {
|
|
final formKey = GlobalKey<FormState>();
|
|
await tester.pumpWidget(
|
|
MaterialApp(
|
|
home: MediaQuery(
|
|
data: MediaQueryData(supportsAnnounce: test.supportsAnnounce),
|
|
child: Directionality(
|
|
textDirection: TextDirection.ltr,
|
|
child: Center(
|
|
child: Material(
|
|
child: Form(
|
|
key: formKey,
|
|
child: Column(
|
|
children: <Widget>[
|
|
TextFormField(validator: (_) => 'First error message'),
|
|
TextFormField(validator: (_) => 'Second error message'),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
);
|
|
formKey.currentState!.reset();
|
|
await tester.enterText(find.byType(TextFormField).first, '');
|
|
await tester.pump();
|
|
|
|
// Manually validate.
|
|
expect(find.text('First error message'), findsNothing);
|
|
expect(find.text('Second error message'), findsNothing);
|
|
formKey.currentState!.validate();
|
|
await tester.pump();
|
|
expect(find.text('First error message'), findsOneWidget);
|
|
expect(find.text('Second error message'), findsOneWidget);
|
|
|
|
if (test.supportsAnnounce) {
|
|
expect(tester.takeAnnouncements(), [
|
|
isAccessibilityAnnouncement(
|
|
'First error message',
|
|
textDirection: TextDirection.ltr,
|
|
assertiveness: Assertiveness.assertive,
|
|
),
|
|
]);
|
|
} else {
|
|
expect(tester.takeAnnouncements(), isEmpty);
|
|
}
|
|
});
|
|
}
|
|
|
|
testWidgets('isValid returns true when a field is valid', (WidgetTester tester) async {
|
|
final fieldKey1 = GlobalKey<FormFieldState<String>>();
|
|
final fieldKey2 = GlobalKey<FormFieldState<String>>();
|
|
const validString = 'Valid string';
|
|
String? validator(String? s) => s == validString ? null : 'Error text';
|
|
|
|
Widget builder() {
|
|
return 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.always,
|
|
),
|
|
TextFormField(
|
|
key: fieldKey2,
|
|
initialValue: validString,
|
|
validator: validator,
|
|
autovalidateMode: AutovalidateMode.always,
|
|
),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
await tester.pumpWidget(builder());
|
|
|
|
expect(fieldKey1.currentState!.isValid, isTrue);
|
|
expect(fieldKey2.currentState!.isValid, isTrue);
|
|
});
|
|
|
|
testWidgets('isValid returns false when the field is invalid and does not change error display', (
|
|
WidgetTester tester,
|
|
) async {
|
|
final fieldKey1 = GlobalKey<FormFieldState<String>>();
|
|
final fieldKey2 = GlobalKey<FormFieldState<String>>();
|
|
const validString = 'Valid string';
|
|
String? validator(String? s) => s == validString ? null : 'Error text';
|
|
|
|
Widget builder() {
|
|
return 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: '',
|
|
validator: validator,
|
|
autovalidateMode: AutovalidateMode.disabled,
|
|
),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
await tester.pumpWidget(builder());
|
|
|
|
expect(fieldKey1.currentState!.isValid, isTrue);
|
|
expect(fieldKey2.currentState!.isValid, isFalse);
|
|
expect(fieldKey2.currentState!.hasError, isFalse);
|
|
});
|
|
|
|
testWidgets('validateGranularly returns a set containing all, and only, invalid fields', (
|
|
WidgetTester tester,
|
|
) async {
|
|
final formKey = GlobalKey<FormState>();
|
|
final validFieldsKey = UniqueKey();
|
|
final invalidFieldsKey = UniqueKey();
|
|
|
|
const validString = 'Valid string';
|
|
const invalidString = 'Invalid string';
|
|
String? validator(String? s) => s == validString ? null : 'Error text';
|
|
|
|
Widget builder() {
|
|
return MaterialApp(
|
|
home: MediaQuery(
|
|
data: const MediaQueryData(),
|
|
child: Directionality(
|
|
textDirection: TextDirection.ltr,
|
|
child: Center(
|
|
child: Material(
|
|
child: Form(
|
|
key: formKey,
|
|
child: ListView(
|
|
children: <Widget>[
|
|
TextFormField(
|
|
key: validFieldsKey,
|
|
initialValue: validString,
|
|
validator: validator,
|
|
autovalidateMode: AutovalidateMode.disabled,
|
|
),
|
|
TextFormField(
|
|
key: invalidFieldsKey,
|
|
initialValue: invalidString,
|
|
validator: validator,
|
|
autovalidateMode: AutovalidateMode.disabled,
|
|
),
|
|
TextFormField(
|
|
key: invalidFieldsKey,
|
|
initialValue: invalidString,
|
|
validator: validator,
|
|
autovalidateMode: AutovalidateMode.disabled,
|
|
),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
await tester.pumpWidget(builder());
|
|
|
|
final Set<FormFieldState<dynamic>> validationResult = formKey.currentState!
|
|
.validateGranularly();
|
|
|
|
expect(validationResult.length, equals(2));
|
|
expect(
|
|
validationResult
|
|
.where((FormFieldState<dynamic> field) => field.widget.key == invalidFieldsKey)
|
|
.length,
|
|
equals(2),
|
|
);
|
|
expect(
|
|
validationResult
|
|
.where((FormFieldState<dynamic> field) => field.widget.key == validFieldsKey)
|
|
.length,
|
|
equals(0),
|
|
);
|
|
});
|
|
|
|
testWidgets('Should announce error text when validateGranularly is called', (
|
|
WidgetTester tester,
|
|
) async {
|
|
final formKey = GlobalKey<FormState>();
|
|
const validString = 'Valid string';
|
|
String? validator(String? s) => s == validString ? null : 'error';
|
|
|
|
Widget builder() {
|
|
return MaterialApp(
|
|
home: MediaQuery(
|
|
data: const MediaQueryData(supportsAnnounce: true),
|
|
child: Directionality(
|
|
textDirection: TextDirection.ltr,
|
|
child: Center(
|
|
child: Material(
|
|
child: Form(
|
|
key: formKey,
|
|
child: ListView(
|
|
children: <Widget>[
|
|
TextFormField(
|
|
initialValue: validString,
|
|
validator: validator,
|
|
autovalidateMode: AutovalidateMode.disabled,
|
|
),
|
|
TextFormField(
|
|
initialValue: '',
|
|
validator: validator,
|
|
autovalidateMode: AutovalidateMode.disabled,
|
|
),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
await tester.pumpWidget(builder());
|
|
expect(find.text('error'), findsNothing);
|
|
|
|
formKey.currentState!.validateGranularly();
|
|
|
|
await tester.pump();
|
|
expect(find.text('error'), findsOneWidget);
|
|
|
|
expect(tester.takeAnnouncements(), [
|
|
isAccessibilityAnnouncement(
|
|
'error',
|
|
textDirection: TextDirection.ltr,
|
|
assertiveness: Assertiveness.assertive,
|
|
),
|
|
]);
|
|
});
|
|
|
|
testWidgets('Multiple TextFormFields communicate', (WidgetTester tester) async {
|
|
final formKey = GlobalKey<FormState>();
|
|
final fieldKey = GlobalKey<FormFieldState<String>>();
|
|
// Input 2's validator depends on a input 1's value.
|
|
String? errorText(String? input) => '${fieldKey.currentState!.value}/error';
|
|
|
|
Widget builder() {
|
|
return MaterialApp(
|
|
home: MediaQuery(
|
|
data: const MediaQueryData(),
|
|
child: Directionality(
|
|
textDirection: TextDirection.ltr,
|
|
child: Center(
|
|
child: Material(
|
|
child: Form(
|
|
key: formKey,
|
|
autovalidateMode: AutovalidateMode.always,
|
|
child: ListView(
|
|
children: <Widget>[
|
|
TextFormField(key: fieldKey),
|
|
TextFormField(validator: errorText),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
await tester.pumpWidget(builder());
|
|
|
|
Future<void> checkErrorText(String testValue) async {
|
|
await tester.enterText(find.byType(TextFormField).first, testValue);
|
|
await tester.pump();
|
|
|
|
// Check for a new Text widget with our error text.
|
|
expect(find.text('$testValue/error'), findsOneWidget);
|
|
return;
|
|
}
|
|
|
|
await checkErrorText('Test');
|
|
await checkErrorText('');
|
|
});
|
|
|
|
testWidgets('Provide initial value to input when no controller is specified', (
|
|
WidgetTester tester,
|
|
) async {
|
|
const initialValue = 'hello';
|
|
final inputKey = GlobalKey<FormFieldState<String>>();
|
|
|
|
Widget builder() {
|
|
return MaterialApp(
|
|
home: MediaQuery(
|
|
data: const MediaQueryData(),
|
|
child: Directionality(
|
|
textDirection: TextDirection.ltr,
|
|
child: Center(
|
|
child: Material(
|
|
child: Form(
|
|
child: TextFormField(key: inputKey, initialValue: 'hello'),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
await tester.pumpWidget(builder());
|
|
await tester.showKeyboard(find.byType(TextFormField));
|
|
|
|
// initial value should be loaded into keyboard editing state
|
|
expect(tester.testTextInput.editingState, isNotNull);
|
|
expect(tester.testTextInput.editingState!['text'], equals(initialValue));
|
|
|
|
// initial value should also be visible in the raw input line
|
|
final EditableTextState editableText = tester.state(find.byType(EditableText));
|
|
expect(editableText.widget.controller.text, equals(initialValue));
|
|
|
|
// sanity check, make sure we can still edit the text and everything updates
|
|
expect(inputKey.currentState!.value, equals(initialValue));
|
|
await tester.enterText(find.byType(TextFormField), 'world');
|
|
await tester.pump();
|
|
expect(inputKey.currentState!.value, equals('world'));
|
|
expect(editableText.widget.controller.text, equals('world'));
|
|
});
|
|
|
|
testWidgets('Controller defines initial value', (WidgetTester tester) async {
|
|
final controller = TextEditingController(text: 'hello');
|
|
addTearDown(controller.dispose);
|
|
const initialValue = 'hello';
|
|
final inputKey = GlobalKey<FormFieldState<String>>();
|
|
|
|
Widget builder() {
|
|
return MaterialApp(
|
|
home: MediaQuery(
|
|
data: const MediaQueryData(),
|
|
child: Directionality(
|
|
textDirection: TextDirection.ltr,
|
|
child: Center(
|
|
child: Material(
|
|
child: Form(
|
|
child: TextFormField(key: inputKey, controller: controller),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
await tester.pumpWidget(builder());
|
|
await tester.showKeyboard(find.byType(TextFormField));
|
|
|
|
// initial value should be loaded into keyboard editing state
|
|
expect(tester.testTextInput.editingState, isNotNull);
|
|
expect(tester.testTextInput.editingState!['text'], equals(initialValue));
|
|
|
|
// initial value should also be visible in the raw input line
|
|
final EditableTextState editableText = tester.state(find.byType(EditableText));
|
|
expect(editableText.widget.controller.text, equals(initialValue));
|
|
expect(controller.text, equals(initialValue));
|
|
|
|
// sanity check, make sure we can still edit the text and everything updates
|
|
expect(inputKey.currentState!.value, equals(initialValue));
|
|
await tester.enterText(find.byType(TextFormField), 'world');
|
|
await tester.pump();
|
|
expect(inputKey.currentState!.value, equals('world'));
|
|
expect(editableText.widget.controller.text, equals('world'));
|
|
expect(controller.text, equals('world'));
|
|
});
|
|
|
|
testWidgets('TextFormField resets to its initial value', (WidgetTester tester) async {
|
|
final formKey = GlobalKey<FormState>();
|
|
final inputKey = GlobalKey<FormFieldState<String>>();
|
|
final controller = TextEditingController(text: 'Plover');
|
|
addTearDown(controller.dispose);
|
|
|
|
Widget builder() {
|
|
return MaterialApp(
|
|
home: MediaQuery(
|
|
data: const MediaQueryData(),
|
|
child: Directionality(
|
|
textDirection: TextDirection.ltr,
|
|
child: Center(
|
|
child: Material(
|
|
child: Form(
|
|
key: formKey,
|
|
child: TextFormField(
|
|
key: inputKey,
|
|
controller: controller,
|
|
// initialValue is 'Plover'
|
|
),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
await tester.pumpWidget(builder());
|
|
await tester.showKeyboard(find.byType(TextFormField));
|
|
final EditableTextState editableText = tester.state(find.byType(EditableText));
|
|
|
|
// overwrite initial value.
|
|
controller.text = 'Xyzzy';
|
|
await tester.idle();
|
|
expect(editableText.widget.controller.text, equals('Xyzzy'));
|
|
expect(inputKey.currentState!.value, equals('Xyzzy'));
|
|
expect(controller.text, equals('Xyzzy'));
|
|
|
|
// verify value resets to initialValue on reset.
|
|
formKey.currentState!.reset();
|
|
await tester.idle();
|
|
expect(inputKey.currentState!.value, equals('Plover'));
|
|
expect(editableText.widget.controller.text, equals('Plover'));
|
|
expect(controller.text, equals('Plover'));
|
|
});
|
|
|
|
testWidgets('TextEditingController updates to/from form field value', (
|
|
WidgetTester tester,
|
|
) async {
|
|
final controller1 = TextEditingController(text: 'Foo');
|
|
addTearDown(controller1.dispose);
|
|
final controller2 = TextEditingController(text: 'Bar');
|
|
addTearDown(controller2.dispose);
|
|
final inputKey = GlobalKey<FormFieldState<String>>();
|
|
|
|
TextEditingController? currentController;
|
|
late StateSetter setState;
|
|
|
|
Widget builder() {
|
|
return StatefulBuilder(
|
|
builder: (BuildContext context, StateSetter setter) {
|
|
setState = setter;
|
|
return MaterialApp(
|
|
home: MediaQuery(
|
|
data: const MediaQueryData(),
|
|
child: Directionality(
|
|
textDirection: TextDirection.ltr,
|
|
child: Center(
|
|
child: Material(
|
|
child: Form(
|
|
child: TextFormField(key: inputKey, controller: currentController),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
);
|
|
},
|
|
);
|
|
}
|
|
|
|
await tester.pumpWidget(builder());
|
|
await tester.showKeyboard(find.byType(TextFormField));
|
|
|
|
// verify initially empty.
|
|
expect(tester.testTextInput.editingState, isNotNull);
|
|
expect(tester.testTextInput.editingState!['text'], isEmpty);
|
|
final EditableTextState editableText = tester.state(find.byType(EditableText));
|
|
expect(editableText.widget.controller.text, isEmpty);
|
|
|
|
// verify changing the controller from null to controller1 sets the value.
|
|
setState(() {
|
|
currentController = controller1;
|
|
});
|
|
await tester.pump();
|
|
expect(editableText.widget.controller.text, equals('Foo'));
|
|
expect(inputKey.currentState!.value, equals('Foo'));
|
|
|
|
// verify changes to controller1 text are visible in text field and set in form value.
|
|
controller1.text = 'Wobble';
|
|
await tester.idle();
|
|
expect(editableText.widget.controller.text, equals('Wobble'));
|
|
expect(inputKey.currentState!.value, equals('Wobble'));
|
|
|
|
// verify changes to the field text update the form value and controller1.
|
|
await tester.enterText(find.byType(TextFormField), 'Wibble');
|
|
await tester.pump();
|
|
expect(inputKey.currentState!.value, equals('Wibble'));
|
|
expect(editableText.widget.controller.text, equals('Wibble'));
|
|
expect(controller1.text, equals('Wibble'));
|
|
|
|
// verify that switching from controller1 to controller2 is handled.
|
|
setState(() {
|
|
currentController = controller2;
|
|
});
|
|
await tester.pump();
|
|
expect(inputKey.currentState!.value, equals('Bar'));
|
|
expect(editableText.widget.controller.text, equals('Bar'));
|
|
expect(controller2.text, equals('Bar'));
|
|
expect(controller1.text, equals('Wibble'));
|
|
|
|
// verify changes to controller2 text are visible in text field and set in form value.
|
|
controller2.text = 'Xyzzy';
|
|
await tester.idle();
|
|
expect(editableText.widget.controller.text, equals('Xyzzy'));
|
|
expect(inputKey.currentState!.value, equals('Xyzzy'));
|
|
expect(controller1.text, equals('Wibble'));
|
|
|
|
// verify changes to controller1 text are not visible in text field or set in form value.
|
|
controller1.text = 'Plugh';
|
|
await tester.idle();
|
|
expect(editableText.widget.controller.text, equals('Xyzzy'));
|
|
expect(inputKey.currentState!.value, equals('Xyzzy'));
|
|
expect(controller1.text, equals('Plugh'));
|
|
|
|
// verify that switching from controller2 to null is handled.
|
|
setState(() {
|
|
currentController = null;
|
|
});
|
|
await tester.pump();
|
|
expect(inputKey.currentState!.value, equals('Xyzzy'));
|
|
expect(editableText.widget.controller.text, equals('Xyzzy'));
|
|
expect(controller2.text, equals('Xyzzy'));
|
|
expect(controller1.text, equals('Plugh'));
|
|
|
|
// verify that changes to the field text update the form value but not the previous controllers.
|
|
await tester.enterText(find.byType(TextFormField), 'Plover');
|
|
await tester.pump();
|
|
expect(inputKey.currentState!.value, equals('Plover'));
|
|
expect(editableText.widget.controller.text, equals('Plover'));
|
|
expect(controller1.text, equals('Plugh'));
|
|
expect(controller2.text, equals('Xyzzy'));
|
|
});
|
|
|
|
testWidgets('No crash when a TextFormField is removed from the tree', (
|
|
WidgetTester tester,
|
|
) async {
|
|
final formKey = GlobalKey<FormState>();
|
|
String? fieldValue;
|
|
|
|
Widget builder(bool remove) {
|
|
return MaterialApp(
|
|
home: MediaQuery(
|
|
data: const MediaQueryData(),
|
|
child: Directionality(
|
|
textDirection: TextDirection.ltr,
|
|
child: Center(
|
|
child: Material(
|
|
child: Form(
|
|
key: formKey,
|
|
child: remove
|
|
? Container()
|
|
: TextFormField(
|
|
autofocus: true,
|
|
onSaved: (String? value) {
|
|
fieldValue = value;
|
|
},
|
|
validator: (String? value) {
|
|
return (value == null || value.isEmpty) ? null : 'yes';
|
|
},
|
|
),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
await tester.pumpWidget(builder(false));
|
|
|
|
expect(fieldValue, isNull);
|
|
expect(formKey.currentState!.validate(), isTrue);
|
|
|
|
await tester.enterText(find.byType(TextFormField), 'Test');
|
|
await tester.pumpWidget(builder(false));
|
|
|
|
// Form wasn't saved yet.
|
|
expect(fieldValue, null);
|
|
expect(formKey.currentState!.validate(), isFalse);
|
|
|
|
formKey.currentState!.save();
|
|
|
|
// Now fieldValue is saved.
|
|
expect(fieldValue, 'Test');
|
|
expect(formKey.currentState!.validate(), isFalse);
|
|
|
|
// Now remove the field with an error.
|
|
await tester.pumpWidget(builder(true));
|
|
|
|
// Reset the form. Should not crash.
|
|
formKey.currentState!.reset();
|
|
formKey.currentState!.save();
|
|
expect(formKey.currentState!.validate(), isTrue);
|
|
});
|
|
|
|
testWidgets(
|
|
'Does not auto-validate before value changes when autovalidateMode is set to onUserInteraction',
|
|
(WidgetTester tester) async {
|
|
late FormFieldState<String> formFieldState;
|
|
|
|
String? errorText(String? value) => '$value/error';
|
|
|
|
Widget builder() {
|
|
return MaterialApp(
|
|
home: MediaQuery(
|
|
data: const MediaQueryData(),
|
|
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(
|
|
'Does not auto-validate before value changes when autovalidateMode is set to onUserInteractionIfError',
|
|
(WidgetTester tester) async {
|
|
late FormFieldState<String> formFieldState;
|
|
|
|
String? errorText(String? value) => (value == null || value.isEmpty) ? 'Required' : null;
|
|
|
|
Widget builder() {
|
|
return MaterialApp(
|
|
home: MediaQuery(
|
|
data: const MediaQueryData(),
|
|
child: Directionality(
|
|
textDirection: TextDirection.ltr,
|
|
child: Center(
|
|
child: Material(
|
|
child: FormField<String>(
|
|
initialValue: 'foo',
|
|
autovalidateMode: AutovalidateMode.onUserInteractionIfError,
|
|
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 "Required" error is visible.
|
|
expect(find.text('Required'), findsNothing);
|
|
},
|
|
);
|
|
|
|
testWidgets(
|
|
'Does not auto-validate before value changes even when initialValue is empty and autovalidateMode is set to onUserInteractionIfError',
|
|
(WidgetTester tester) async {
|
|
late FormFieldState<String> formFieldState;
|
|
|
|
String? errorText(String? value) => (value == null || value.isEmpty) ? 'Required' : null;
|
|
|
|
Widget builder() {
|
|
return MaterialApp(
|
|
home: MediaQuery(
|
|
data: const MediaQueryData(),
|
|
child: Directionality(
|
|
textDirection: TextDirection.ltr,
|
|
child: Center(
|
|
child: Material(
|
|
child: FormField<String>(
|
|
autovalidateMode: AutovalidateMode.onUserInteractionIfError,
|
|
builder: (FormFieldState<String> state) {
|
|
formFieldState = state;
|
|
return Column(
|
|
mainAxisSize: MainAxisSize.min,
|
|
children: <Widget>[
|
|
Container(),
|
|
if (state.errorText != null) Text(state.errorText!),
|
|
],
|
|
);
|
|
},
|
|
validator: errorText,
|
|
),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
await tester.pumpWidget(builder());
|
|
|
|
expect(formFieldState.hasError, isFalse);
|
|
|
|
expect(find.text('Required'), findsNothing);
|
|
|
|
expect(formFieldState.errorText, isNull);
|
|
|
|
formFieldState.validate();
|
|
await tester.pump();
|
|
|
|
expect(formFieldState.hasError, isTrue);
|
|
|
|
expect(find.text('Required'), findsOneWidget);
|
|
},
|
|
);
|
|
|
|
testWidgets('auto-validate before value changes if autovalidateMode was set to always', (
|
|
WidgetTester tester,
|
|
) async {
|
|
late FormFieldState<String> formFieldState;
|
|
|
|
String? errorText(String? value) => '$value/error';
|
|
|
|
Widget builder() {
|
|
return MaterialApp(
|
|
home: MediaQuery(
|
|
data: const MediaQueryData(),
|
|
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 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 auto-validate 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(
|
|
'Form.reset() resets form fields, and auto validation will only happen on the next user interaction if autovalidateMode is onUserInteraction',
|
|
(WidgetTester tester) async {
|
|
final formState = GlobalKey<FormState>();
|
|
String? errorText(String? value) => '$value/error';
|
|
|
|
Widget builder() {
|
|
return MaterialApp(
|
|
theme: ThemeData(),
|
|
home: MediaQuery(
|
|
data: const MediaQueryData(),
|
|
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 with AutovalidateMode.onUserInteractionIfError only revalidates when user interacts after an error exists',
|
|
(WidgetTester tester) async {
|
|
final formState = GlobalKey<FormState>();
|
|
String? errorText(String? value) => (value == null || value.isEmpty) ? 'Required' : null;
|
|
|
|
Widget builder() {
|
|
return MaterialApp(
|
|
theme: ThemeData(),
|
|
home: MediaQuery(
|
|
data: const MediaQueryData(),
|
|
child: Directionality(
|
|
textDirection: TextDirection.ltr,
|
|
child: Center(
|
|
child: Form(
|
|
key: formState,
|
|
autovalidateMode: AutovalidateMode.onUserInteractionIfError,
|
|
child: Material(
|
|
child: TextFormField(initialValue: 'foo', validator: errorText),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
await tester.pumpWidget(builder());
|
|
|
|
// No error text is visible yet. (Initial valid state).
|
|
expect(find.text('Required'), findsNothing);
|
|
|
|
// User types valid input 'bar' → autovalidate is disabled → still no error.
|
|
await tester.enterText(find.byType(TextFormField), 'bar');
|
|
await tester.pump();
|
|
expect(find.text('Required'), findsNothing);
|
|
|
|
// Clear the input (invalid).
|
|
await tester.enterText(find.byType(TextFormField), '');
|
|
await tester.pump();
|
|
|
|
// Manually submit form to show the initial error (AutovalidateMode is now active).
|
|
formState.currentState!.validate();
|
|
expect(find.text('Required'), findsNothing);
|
|
await tester.pump();
|
|
|
|
// Verify error is shown.
|
|
expect(find.text('Required'), findsOneWidget);
|
|
|
|
// Now user interacts again with valid text ('baz') → validation auto-runs and clears the error.
|
|
await tester.enterText(find.byType(TextFormField), 'baz');
|
|
await tester.pump();
|
|
expect(find.text('Required'), findsNothing);
|
|
|
|
// Check the behavior of a manual validate when the text is already valid.
|
|
// This should *confirm* the error is cleared, not re-introduce it.
|
|
formState.currentState!.validate();
|
|
await tester.pump();
|
|
expect(find.text('Required'), findsNothing);
|
|
|
|
// Resetting should clear form (already cleared, but a safety check).
|
|
await tester.enterText(find.byType(TextFormField), '');
|
|
formState.currentState!.reset();
|
|
await tester.pump();
|
|
expect(find.text('Required'), findsNothing);
|
|
},
|
|
);
|
|
|
|
// Regression test for https://github.com/flutter/flutter/issues/63753.
|
|
testWidgets('Validate form should return correct validation if the value is composing', (
|
|
WidgetTester tester,
|
|
) async {
|
|
final formKey = GlobalKey<FormState>();
|
|
String? fieldValue;
|
|
|
|
final Widget widget = MaterialApp(
|
|
home: MediaQuery(
|
|
data: const MediaQueryData(),
|
|
child: Directionality(
|
|
textDirection: TextDirection.ltr,
|
|
child: Center(
|
|
child: Material(
|
|
child: Form(
|
|
key: formKey,
|
|
child: TextFormField(
|
|
maxLength: 5,
|
|
maxLengthEnforcement: MaxLengthEnforcement.truncateAfterCompositionEnds,
|
|
onSaved: (String? value) {
|
|
fieldValue = value;
|
|
},
|
|
validator: (String? value) =>
|
|
(value != null && value.length > 5) ? 'Exceeded' : null,
|
|
),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
);
|
|
|
|
await tester.pumpWidget(widget);
|
|
|
|
final EditableTextState editableText = tester.state<EditableTextState>(
|
|
find.byType(EditableText),
|
|
);
|
|
editableText.updateEditingValue(
|
|
const TextEditingValue(text: '123456', composing: TextRange(start: 2, end: 5)),
|
|
);
|
|
expect(editableText.currentTextEditingValue.composing, const TextRange(start: 2, end: 5));
|
|
|
|
formKey.currentState!.save();
|
|
expect(fieldValue, '123456');
|
|
expect(formKey.currentState!.validate(), isFalse);
|
|
});
|
|
|
|
testWidgets('hasInteractedByUser returns false when the input has not changed', (
|
|
WidgetTester tester,
|
|
) async {
|
|
final fieldKey = GlobalKey<FormFieldState<String>>();
|
|
|
|
final Widget widget = MaterialApp(
|
|
home: MediaQuery(
|
|
data: const MediaQueryData(),
|
|
child: Directionality(
|
|
textDirection: TextDirection.ltr,
|
|
child: Center(
|
|
child: Material(child: TextFormField(key: fieldKey)),
|
|
),
|
|
),
|
|
),
|
|
);
|
|
|
|
await tester.pumpWidget(widget);
|
|
|
|
expect(fieldKey.currentState!.hasInteractedByUser, isFalse);
|
|
});
|
|
|
|
testWidgets('hasInteractedByUser returns true after the input has changed', (
|
|
WidgetTester tester,
|
|
) async {
|
|
final fieldKey = GlobalKey<FormFieldState<String>>();
|
|
|
|
final Widget widget = MaterialApp(
|
|
home: MediaQuery(
|
|
data: const MediaQueryData(),
|
|
child: Directionality(
|
|
textDirection: TextDirection.ltr,
|
|
child: Center(
|
|
child: Material(child: TextFormField(key: fieldKey)),
|
|
),
|
|
),
|
|
),
|
|
);
|
|
|
|
await tester.pumpWidget(widget);
|
|
|
|
// initially, the field has not been interacted with
|
|
expect(fieldKey.currentState!.hasInteractedByUser, isFalse);
|
|
|
|
// after entering text, the field has been interacted with
|
|
await tester.enterText(find.byType(TextFormField), 'foo');
|
|
expect(fieldKey.currentState!.hasInteractedByUser, isTrue);
|
|
});
|
|
|
|
testWidgets('hasInteractedByUser returns false after the field is reset', (
|
|
WidgetTester tester,
|
|
) async {
|
|
final fieldKey = GlobalKey<FormFieldState<String>>();
|
|
|
|
final Widget widget = MaterialApp(
|
|
home: MediaQuery(
|
|
data: const MediaQueryData(),
|
|
child: Directionality(
|
|
textDirection: TextDirection.ltr,
|
|
child: Center(
|
|
child: Material(child: TextFormField(key: fieldKey)),
|
|
),
|
|
),
|
|
),
|
|
);
|
|
|
|
await tester.pumpWidget(widget);
|
|
|
|
// initially, the field has not been interacted with
|
|
expect(fieldKey.currentState!.hasInteractedByUser, isFalse);
|
|
|
|
// after entering text, the field has been interacted with
|
|
await tester.enterText(find.byType(TextFormField), 'foo');
|
|
expect(fieldKey.currentState!.hasInteractedByUser, isTrue);
|
|
|
|
// after resetting the field, it has not been interacted with again
|
|
fieldKey.currentState!.reset();
|
|
expect(fieldKey.currentState!.hasInteractedByUser, isFalse);
|
|
});
|
|
|
|
testWidgets('forceErrorText forces an error state when first init', (WidgetTester tester) async {
|
|
const 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 formKey = GlobalKey<FormState>();
|
|
const 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 formKey = GlobalKey<FormState>();
|
|
const 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 formKey = GlobalKey<FormState>();
|
|
const forceErrorText = 'Forcing error.';
|
|
const validatorErrorText = 'this error should not appear as we override it with forceErrorText';
|
|
var 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 formKey = GlobalKey<FormState>();
|
|
var useValidator = false;
|
|
late StateSetter setState;
|
|
|
|
String? validator(String? value) {
|
|
if (value == null || value.isEmpty) {
|
|
return 'test_error';
|
|
}
|
|
return null;
|
|
}
|
|
|
|
Widget builder() {
|
|
return StatefulBuilder(
|
|
builder: (BuildContext context, StateSetter setter) {
|
|
setState = setter;
|
|
return MaterialApp(
|
|
home: MediaQuery(
|
|
data: const MediaQueryData(),
|
|
child: Directionality(
|
|
textDirection: TextDirection.ltr,
|
|
child: Center(
|
|
child: Material(
|
|
child: Form(
|
|
key: formKey,
|
|
child: TextFormField(validator: useValidator ? validator : null),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
);
|
|
},
|
|
);
|
|
}
|
|
|
|
await tester.pumpWidget(builder());
|
|
|
|
// Start with no validator.
|
|
await tester.enterText(find.byType(TextFormField), '');
|
|
await tester.pump();
|
|
formKey.currentState!.validate();
|
|
await tester.pump();
|
|
expect(find.text('test_error'), findsNothing);
|
|
|
|
// Now use the validator.
|
|
setState(() {
|
|
useValidator = true;
|
|
});
|
|
await tester.pump();
|
|
formKey.currentState!.validate();
|
|
await tester.pump();
|
|
expect(find.text('test_error'), findsOneWidget);
|
|
|
|
// Remove the validator again and expect the error to disappear.
|
|
setState(() {
|
|
useValidator = false;
|
|
});
|
|
await tester.pump();
|
|
formKey.currentState!.validate();
|
|
await tester.pump();
|
|
expect(find.text('test_error'), findsNothing);
|
|
});
|
|
|
|
testWidgets('AutovalidateMode.onUnfocus', (WidgetTester tester) async {
|
|
final formKey = GlobalKey<FormState>();
|
|
String? errorText(String? value) => '$value/error';
|
|
|
|
Widget builder() {
|
|
return MaterialApp(
|
|
theme: ThemeData(),
|
|
home: MediaQuery(
|
|
data: const MediaQueryData(),
|
|
child: Directionality(
|
|
textDirection: TextDirection.ltr,
|
|
child: Center(
|
|
child: Form(
|
|
key: formKey,
|
|
autovalidateMode: AutovalidateMode.onUnfocus,
|
|
child: Material(
|
|
child: Column(
|
|
children: <Widget>[
|
|
TextFormField(initialValue: 'bar', validator: errorText),
|
|
TextFormField(initialValue: 'bar', validator: errorText),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
await tester.pumpWidget(builder());
|
|
|
|
// No error text is visible yet.
|
|
expect(find.text(errorText('foo')!), findsNothing);
|
|
|
|
// Enter text in the first TextFormField.
|
|
await tester.enterText(find.byType(TextFormField).first, 'foo');
|
|
await tester.pumpAndSettle();
|
|
|
|
// No error text is visible yet.
|
|
expect(find.text(errorText('foo')!), findsNothing);
|
|
|
|
// Tap on the second TextFormField to trigger validation.
|
|
// This should trigger validation for the first TextFormField as well.
|
|
await tester.tap(find.byType(TextFormField).last);
|
|
await tester.pumpAndSettle();
|
|
|
|
// Verify that the error text is displayed for the first TextFormField.
|
|
expect(find.text(errorText('foo')!), findsOneWidget);
|
|
expect(find.text(errorText('bar')!), findsNothing);
|
|
|
|
// Tap on the first TextFormField to trigger validation.
|
|
await tester.tap(find.byType(TextFormField).first);
|
|
await tester.pumpAndSettle();
|
|
|
|
// Verify that the both error texts are displayed.
|
|
expect(find.text(errorText('foo')!), findsOneWidget);
|
|
expect(find.text(errorText('bar')!), findsOneWidget);
|
|
});
|
|
|
|
testWidgets('Validate conflicting AutovalidateModes', (WidgetTester tester) async {
|
|
final formKey = GlobalKey<FormState>();
|
|
String? errorText(String? value) => '$value/error';
|
|
|
|
Widget builder() {
|
|
return MaterialApp(
|
|
theme: ThemeData(),
|
|
home: MediaQuery(
|
|
data: const MediaQueryData(),
|
|
child: Directionality(
|
|
textDirection: TextDirection.ltr,
|
|
child: Center(
|
|
child: Form(
|
|
key: formKey,
|
|
autovalidateMode: AutovalidateMode.onUnfocus,
|
|
child: Material(
|
|
child: Column(
|
|
children: <Widget>[
|
|
TextFormField(
|
|
autovalidateMode: AutovalidateMode.always,
|
|
initialValue: 'foo',
|
|
validator: errorText,
|
|
),
|
|
TextFormField(
|
|
autovalidateMode: AutovalidateMode.disabled,
|
|
initialValue: 'bar',
|
|
validator: errorText,
|
|
),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
await tester.pumpWidget(builder());
|
|
|
|
// Verify that the error text is displayed for the first TextFormField.
|
|
expect(find.text(errorText('foo')!), findsOneWidget);
|
|
|
|
// Enter text in the TextFormField.
|
|
await tester.enterText(find.byType(TextFormField).first, 'foo');
|
|
await tester.pumpAndSettle();
|
|
|
|
// Click in the second TextFormField to trigger validation.
|
|
await tester.tap(find.byType(TextFormField).last);
|
|
await tester.pumpAndSettle();
|
|
|
|
// No error text is visible yet for the second TextFormField.
|
|
expect(find.text(errorText('bar')!), findsNothing);
|
|
|
|
// Now click in the first TextFormField to trigger validation for the second TextFormField.
|
|
await tester.tap(find.byType(TextFormField).first);
|
|
await tester.pumpAndSettle();
|
|
|
|
// Verify that the error text is displayed for the second TextFormField.
|
|
expect(find.text(errorText('bar')!), findsOneWidget);
|
|
});
|
|
|
|
testWidgets('FocusNode should move to next field when TextInputAction.next is received', (
|
|
WidgetTester tester,
|
|
) async {
|
|
final formKey = GlobalKey<FormState>();
|
|
final focusNode1 = FocusNode();
|
|
addTearDown(focusNode1.dispose);
|
|
final focusNode2 = FocusNode();
|
|
addTearDown(focusNode2.dispose);
|
|
final controller1 = TextEditingController();
|
|
addTearDown(controller1.dispose);
|
|
final controller2 = TextEditingController();
|
|
addTearDown(controller2.dispose);
|
|
|
|
final Widget widget = MaterialApp(
|
|
home: MediaQuery(
|
|
data: const MediaQueryData(),
|
|
child: Directionality(
|
|
textDirection: TextDirection.ltr,
|
|
child: Center(
|
|
child: Material(
|
|
child: Form(
|
|
key: formKey,
|
|
child: Column(
|
|
children: <Widget>[
|
|
TextFormField(
|
|
focusNode: focusNode1,
|
|
controller: controller1,
|
|
textInputAction: TextInputAction.next,
|
|
),
|
|
TextFormField(focusNode: focusNode2, controller: controller2),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
);
|
|
|
|
await tester.pumpWidget(widget);
|
|
|
|
await tester.showKeyboard(find.byType(TextFormField).first);
|
|
await tester.testTextInput.receiveAction(TextInputAction.next);
|
|
await tester.pumpAndSettle();
|
|
|
|
expect(focusNode2.hasFocus, isTrue);
|
|
});
|
|
|
|
testWidgets('AutovalidateMode.always should validate on second build', (
|
|
WidgetTester tester,
|
|
) async {
|
|
String errorText(String? value) => '$value/error';
|
|
|
|
await tester.pumpWidget(
|
|
MaterialApp(
|
|
theme: ThemeData(),
|
|
home: Center(
|
|
child: Form(
|
|
autovalidateMode: AutovalidateMode.always,
|
|
child: Material(
|
|
child: Column(
|
|
children: <Widget>[
|
|
TextFormField(initialValue: 'foo', validator: errorText),
|
|
TextFormField(initialValue: 'bar', validator: errorText),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
);
|
|
|
|
// The validation happens in a post frame callback, so the error
|
|
// doesn't show up until the second frame.
|
|
expect(find.text(errorText('foo')), findsNothing);
|
|
expect(find.text(errorText('bar')), findsNothing);
|
|
|
|
await tester.pump();
|
|
|
|
// The error shows up on the second frame.
|
|
expect(find.text(errorText('foo')), findsOneWidget);
|
|
expect(find.text(errorText('bar')), findsOneWidget);
|
|
});
|
|
|
|
testWidgets('AutovalidateMode.onUnfocus should validate all fields manually with FormState', (
|
|
WidgetTester tester,
|
|
) async {
|
|
final formKey = GlobalKey<FormState>();
|
|
const fieldKey = Key('form field');
|
|
String errorText(String? value) => '$value/error';
|
|
|
|
await tester.pumpWidget(
|
|
MaterialApp(
|
|
home: Center(
|
|
child: Form(
|
|
key: formKey,
|
|
autovalidateMode: AutovalidateMode.onUnfocus,
|
|
child: Material(
|
|
child: Column(
|
|
children: <Widget>[
|
|
TextFormField(key: fieldKey, initialValue: 'foo', validator: errorText),
|
|
TextFormField(initialValue: 'bar', validator: errorText),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
);
|
|
|
|
// Focus on the first field.
|
|
await tester.tap(find.byKey(fieldKey));
|
|
await tester.pump();
|
|
|
|
// Check no error messages are displayed initially.
|
|
expect(find.text('foo/error'), findsNothing);
|
|
expect(find.text('bar/error'), findsNothing);
|
|
|
|
// Validate all fields manually using FormState.
|
|
expect(formKey.currentState!.validate(), isFalse);
|
|
await tester.pump();
|
|
|
|
// Check error messages are displayed.
|
|
expect(find.text('foo/error'), findsOneWidget);
|
|
expect(find.text('bar/error'), findsOneWidget);
|
|
});
|
|
|
|
testWidgets('FormField adds validation result to the semantics of the child', (
|
|
WidgetTester tester,
|
|
) async {
|
|
final formKey = GlobalKey<FormState>();
|
|
|
|
String? errorText;
|
|
|
|
Future<void> pumpWidget() async {
|
|
formKey.currentState?.reset();
|
|
await tester.pumpWidget(
|
|
MaterialApp(
|
|
home: MediaQuery(
|
|
data: const MediaQueryData(),
|
|
child: Directionality(
|
|
textDirection: TextDirection.ltr,
|
|
child: Center(
|
|
child: Material(
|
|
child: Form(
|
|
key: formKey,
|
|
autovalidateMode: AutovalidateMode.always,
|
|
child: TextFormField(validator: (String? value) => errorText),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
);
|
|
|
|
await tester.enterText(find.byType(TextFormField), 'Hello');
|
|
await tester.pump();
|
|
}
|
|
|
|
// Test valid case
|
|
await pumpWidget();
|
|
expect(
|
|
tester.getSemantics(find.byType(TextFormField).last),
|
|
isSemantics(
|
|
isTextField: true,
|
|
isFocusable: true,
|
|
validationResult: SemanticsValidationResult.valid,
|
|
),
|
|
);
|
|
|
|
// Test invalid case
|
|
errorText = 'Error';
|
|
await pumpWidget();
|
|
expect(
|
|
tester.getSemantics(find.byType(TextFormField).last),
|
|
isSemantics(
|
|
isTextField: true,
|
|
isFocusable: true,
|
|
validationResult: SemanticsValidationResult.invalid,
|
|
),
|
|
);
|
|
});
|
|
}
|
|
|
|
class _PlatformAnnounceScenario {
|
|
_PlatformAnnounceScenario({required this.supportsAnnounce, required this.testName});
|
|
final bool supportsAnnounce;
|
|
final String testName;
|
|
}
|