blob: f9b70f3f91385a274032b38bc7c970ff59701310 [file] [log] [blame]
// 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/services.dart';
import 'package:flutter_test/flutter_test.dart';
void main() {
testWidgets('onSaved callback is called', (WidgetTester tester) async {
final GlobalKey<FormState> 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);
// pump'ing is unnecessary because callback happens regardless of frames
expect(fieldValue, equals(testValue));
}
await checkText('Test');
await checkText('');
});
testWidgets('Validator sets the error text only when validate is called', (WidgetTester tester) async {
final GlobalKey<FormState> 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('');
});
testWidgets('isValid returns true when a field is valid', (WidgetTester tester) async {
final GlobalKey<FormFieldState<String>> fieldKey1 = GlobalKey<FormFieldState<String>>();
final GlobalKey<FormFieldState<String>> fieldKey2 = GlobalKey<FormFieldState<String>>();
const String 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 GlobalKey<FormFieldState<String>> fieldKey1 = GlobalKey<FormFieldState<String>>();
final GlobalKey<FormFieldState<String>> fieldKey2 = GlobalKey<FormFieldState<String>>();
const String 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('Multiple TextFormFields communicate', (WidgetTester tester) async {
final GlobalKey<FormState> formKey = GlobalKey<FormState>();
final GlobalKey<FormFieldState<String>> 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 String initialValue = 'hello';
final GlobalKey<FormFieldState<String>> 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 TextEditingController controller = TextEditingController(text: 'hello');
const String initialValue = 'hello';
final GlobalKey<FormFieldState<String>> 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 GlobalKey<FormState> formKey = GlobalKey<FormState>();
final GlobalKey<FormFieldState<String>> inputKey = GlobalKey<FormFieldState<String>>();
final TextEditingController controller = TextEditingController(text: 'Plover');
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 TextEditingController controller1 = TextEditingController(text: 'Foo');
final TextEditingController controller2 = TextEditingController(text: 'Bar');
final GlobalKey<FormFieldState<String>> 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 GlobalKey<FormState> 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('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 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 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 GlobalKey<FormState> 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);
});
// 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 GlobalKey<FormState> 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);
});
}