form_test.dart 28.9 KB
Newer Older
Ian Hickson's avatar
Ian Hickson committed
1
// Copyright 2014 The Flutter Authors. All rights reserved.
2 3 4 5
// 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';
6
import 'package:flutter/rendering.dart';
7
import 'package:flutter/services.dart';
8
import 'package:flutter_test/flutter_test.dart';
9 10

void main() {
11
  testWidgets('onSaved callback is called', (WidgetTester tester) async {
12
    final GlobalKey<FormState> formKey = GlobalKey<FormState>();
13
    String? fieldValue;
14 15

    Widget builder() {
16 17
      return MaterialApp(
        home: MediaQuery(
18
          data: const MediaQueryData(),
19 20 21 22 23 24 25
          child: Directionality(
            textDirection: TextDirection.ltr,
            child: Center(
              child: Material(
                child: Form(
                  key: formKey,
                  child: TextFormField(
26
                    onSaved: (String? value) { fieldValue = value; },
27
                  ),
28
                ),
Ian Hickson's avatar
Ian Hickson committed
29
              ),
30
            ),
31 32
          ),
        ),
33 34
      );
    }
35

36
    await tester.pumpWidget(builder());
37

38
    expect(fieldValue, isNull);
39

40
    Future<void> checkText(String testValue) async {
41
      await tester.enterText(find.byType(TextFormField), testValue);
42
      formKey.currentState!.save();
43
      // Pumping is unnecessary because callback happens regardless of frames.
44 45
      expect(fieldValue, equals(testValue));
    }
46

47 48
    await checkText('Test');
    await checkText('');
49 50
  });

51
  testWidgets('onChanged callback is called', (WidgetTester tester) async {
52
    String? fieldValue;
53 54

    Widget builder() {
55 56
      return MaterialApp(
        home: MediaQuery(
57
          data: const MediaQueryData(),
58 59 60 61 62 63 64 65
          child: Directionality(
            textDirection: TextDirection.ltr,
            child: Center(
              child: Material(
                child: Form(
                  child: TextField(
                    onChanged: (String value) { fieldValue = value; },
                  ),
66
                ),
Ian Hickson's avatar
Ian Hickson committed
67
              ),
68
            ),
69 70
          ),
        ),
71 72 73 74 75 76 77
      );
    }

    await tester.pumpWidget(builder());

    expect(fieldValue, isNull);

78
    Future<void> checkText(String testValue) async {
79
      await tester.enterText(find.byType(TextField), testValue);
80 81 82 83 84 85 86 87
      // pump'ing is unnecessary because callback happens regardless of frames
      expect(fieldValue, equals(testValue));
    }

    await checkText('Test');
    await checkText('');
  });

88
  testWidgets('Validator sets the error text only when validate is called', (WidgetTester tester) async {
89
    final GlobalKey<FormState> formKey = GlobalKey<FormState>();
90
    String? errorText(String? value) => '${value ?? ''}/error';
91

92
    Widget builder(AutovalidateMode autovalidateMode) {
93 94
      return MaterialApp(
        home: MediaQuery(
95
          data: const MediaQueryData(),
96 97 98 99 100 101
          child: Directionality(
            textDirection: TextDirection.ltr,
            child: Center(
              child: Material(
                child: Form(
                  key: formKey,
102
                  autovalidateMode: autovalidateMode,
103 104 105
                  child: TextFormField(
                    validator: errorText,
                  ),
106
                ),
Ian Hickson's avatar
Ian Hickson committed
107
              ),
108
            ),
109 110
          ),
        ),
111 112
      );
    }
113

114
    // Start off not autovalidating.
115
    await tester.pumpWidget(builder(AutovalidateMode.disabled));
116

117
    Future<void> checkErrorText(String testValue) async {
118
      formKey.currentState!.reset();
119
      await tester.pumpWidget(builder(AutovalidateMode.disabled));
120 121
      await tester.enterText(find.byType(TextFormField), testValue);
      await tester.pump();
122 123

      // We have to manually validate if we're not autovalidating.
124 125
      expect(find.text(errorText(testValue)!), findsNothing);
      formKey.currentState!.validate();
126
      await tester.pump();
127
      expect(find.text(errorText(testValue)!), findsOneWidget);
128 129

      // Try again with autovalidation. Should validate immediately.
130
      formKey.currentState!.reset();
131
      await tester.pumpWidget(builder(AutovalidateMode.always));
132 133
      await tester.enterText(find.byType(TextFormField), testValue);
      await tester.pump();
134

135
      expect(find.text(errorText(testValue)!), findsOneWidget);
136
    }
137

138 139
    await checkErrorText('Test');
    await checkErrorText('');
140 141
  });

142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180
  testWidgets('Should announce error text when validate returns error', (WidgetTester tester) async {
    final GlobalKey<FormState> formKey = GlobalKey<FormState>();
    await tester.pumpWidget(
      MaterialApp(
        home: MediaQuery(
          data: const MediaQueryData(),
          child: Directionality(
            textDirection: TextDirection.ltr,
            child: Center(
              child: Material(
                child: Form(
                  key: formKey,
                  child: TextFormField(
                    validator: (_)=> 'error',
                  ),
                ),
              ),
            ),
          ),
        ),
      ),
    );
    formKey.currentState!.reset();
    await tester.enterText(find.byType(TextFormField), '');
    await tester.pump();

    // Manually validate.
    expect(find.text('error'), findsNothing);
    formKey.currentState!.validate();
    await tester.pump();
    expect(find.text('error'), findsOneWidget);

    final CapturedAccessibilityAnnouncement announcement = tester.takeAnnouncements().single;
    expect(announcement.message, 'error');
    expect(announcement.textDirection, TextDirection.ltr);
    expect(announcement.assertiveness, Assertiveness.assertive);

  });

181 182 183 184
  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';
185
    String? validator(String? s) => s == validString ? null : 'Error text';
186 187 188 189

    Widget builder() {
      return MaterialApp(
        home: MediaQuery(
190
          data: const MediaQueryData(),
191 192 193 194 195 196 197 198 199 200 201
          child: Directionality(
            textDirection: TextDirection.ltr,
            child: Center(
              child: Material(
                child: Form(
                  child: ListView(
                    children: <Widget>[
                      TextFormField(
                        key: fieldKey1,
                        initialValue: validString,
                        validator: validator,
202
                        autovalidateMode: AutovalidateMode.always,
203 204 205 206 207
                      ),
                      TextFormField(
                        key: fieldKey2,
                        initialValue: validString,
                        validator: validator,
208
                        autovalidateMode: AutovalidateMode.always,
209 210 211 212 213 214 215 216 217 218 219 220 221
                      ),
                    ],
                  ),
                ),
              ),
            ),
          ),
        ),
      );
    }

    await tester.pumpWidget(builder());

222 223
    expect(fieldKey1.currentState!.isValid, isTrue);
    expect(fieldKey2.currentState!.isValid, isTrue);
224 225 226 227 228 229 230 231
  });

  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';
232
      String? validator(String? s) => s == validString ? null : 'Error text';
233 234 235 236

      Widget builder() {
        return MaterialApp(
          home: MediaQuery(
237
            data: const MediaQueryData(),
238 239 240 241 242 243 244 245 246 247 248
            child: Directionality(
              textDirection: TextDirection.ltr,
              child: Center(
                child: Material(
                  child: Form(
                    child: ListView(
                      children: <Widget>[
                        TextFormField(
                          key: fieldKey1,
                          initialValue: validString,
                          validator: validator,
249
                          autovalidateMode: AutovalidateMode.disabled,
250 251 252 253 254
                        ),
                        TextFormField(
                          key: fieldKey2,
                          initialValue: '',
                          validator: validator,
255
                          autovalidateMode: AutovalidateMode.disabled,
256 257 258 259 260 261 262 263 264 265 266 267 268
                        ),
                      ],
                    ),
                  ),
                ),
              ),
            ),
          ),
        );
      }

      await tester.pumpWidget(builder());

269 270 271
      expect(fieldKey1.currentState!.isValid, isTrue);
      expect(fieldKey2.currentState!.isValid, isFalse);
      expect(fieldKey2.currentState!.hasError, isFalse);
272 273 274
    },
  );

275
  testWidgets('Multiple TextFormFields communicate', (WidgetTester tester) async {
276 277
    final GlobalKey<FormState> formKey = GlobalKey<FormState>();
    final GlobalKey<FormFieldState<String>> fieldKey = GlobalKey<FormFieldState<String>>();
278
    // Input 2's validator depends on a input 1's value.
279
    String? errorText(String? input) => '${fieldKey.currentState!.value}/error';
280 281

    Widget builder() {
282 283
      return MaterialApp(
        home: MediaQuery(
284
          data: const MediaQueryData(),
285 286 287 288 289 290
          child: Directionality(
            textDirection: TextDirection.ltr,
            child: Center(
              child: Material(
                child: Form(
                  key: formKey,
291
                  autovalidateMode: AutovalidateMode.always,
292 293 294 295 296 297 298 299 300 301
                  child: ListView(
                    children: <Widget>[
                      TextFormField(
                        key: fieldKey,
                      ),
                      TextFormField(
                        validator: errorText,
                      ),
                    ],
                  ),
302
                ),
303
              ),
304
            ),
305 306
          ),
        ),
307 308
      );
    }
309

310
    await tester.pumpWidget(builder());
311

312
    Future<void> checkErrorText(String testValue) async {
313
      await tester.enterText(find.byType(TextFormField).first, testValue);
314
      await tester.pump();
315

316
      // Check for a new Text widget with our error text.
317
      expect(find.text('$testValue/error'), findsOneWidget);
318
      return;
319
    }
320

321 322
    await checkErrorText('Test');
    await checkErrorText('');
323
  });
324

325
  testWidgets('Provide initial value to input when no controller is specified', (WidgetTester tester) async {
326
    const String initialValue = 'hello';
327
    final GlobalKey<FormFieldState<String>> inputKey = GlobalKey<FormFieldState<String>>();
328 329

    Widget builder() {
330 331
      return MaterialApp(
        home: MediaQuery(
332
          data: const MediaQueryData(),
333 334 335 336 337 338 339 340 341
          child: Directionality(
            textDirection: TextDirection.ltr,
            child: Center(
              child: Material(
                child: Form(
                  child: TextFormField(
                    key: inputKey,
                    initialValue: 'hello',
                  ),
342
                ),
343 344 345 346 347 348 349 350 351 352 353 354
              ),
            ),
          ),
        ),
      );
    }

    await tester.pumpWidget(builder());
    await tester.showKeyboard(find.byType(TextFormField));

    // initial value should be loaded into keyboard editing state
    expect(tester.testTextInput.editingState, isNotNull);
355
    expect(tester.testTextInput.editingState!['text'], equals(initialValue));
356 357 358 359 360 361

    // 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
362
    expect(inputKey.currentState!.value, equals(initialValue));
363 364
    await tester.enterText(find.byType(TextFormField), 'world');
    await tester.pump();
365
    expect(inputKey.currentState!.value, equals('world'));
366 367 368
    expect(editableText.widget.controller.text, equals('world'));
  });

369
  testWidgets('Controller defines initial value', (WidgetTester tester) async {
370
    final TextEditingController controller = TextEditingController(text: 'hello');
371
    const String initialValue = 'hello';
372
    final GlobalKey<FormFieldState<String>> inputKey = GlobalKey<FormFieldState<String>>();
373 374

    Widget builder() {
375 376
      return MaterialApp(
        home: MediaQuery(
377
          data: const MediaQueryData(),
378 379 380 381 382 383 384 385 386
          child: Directionality(
            textDirection: TextDirection.ltr,
            child: Center(
              child: Material(
                child: Form(
                  child: TextFormField(
                    key: inputKey,
                    controller: controller,
                  ),
387
                ),
Ian Hickson's avatar
Ian Hickson committed
388
              ),
389
            ),
390 391
          ),
        ),
392 393 394 395
      );
    }

    await tester.pumpWidget(builder());
396
    await tester.showKeyboard(find.byType(TextFormField));
397 398

    // initial value should be loaded into keyboard editing state
399
    expect(tester.testTextInput.editingState, isNotNull);
400
    expect(tester.testTextInput.editingState!['text'], equals(initialValue));
401 402

    // initial value should also be visible in the raw input line
403
    final EditableTextState editableText = tester.state(find.byType(EditableText));
404
    expect(editableText.widget.controller.text, equals(initialValue));
405
    expect(controller.text, equals(initialValue));
406 407

    // sanity check, make sure we can still edit the text and everything updates
408
    expect(inputKey.currentState!.value, equals(initialValue));
409
    await tester.enterText(find.byType(TextFormField), 'world');
410
    await tester.pump();
411
    expect(inputKey.currentState!.value, equals('world'));
412
    expect(editableText.widget.controller.text, equals('world'));
413 414 415 416
    expect(controller.text, equals('world'));
  });

  testWidgets('TextFormField resets to its initial value', (WidgetTester tester) async {
417 418 419
    final GlobalKey<FormState> formKey = GlobalKey<FormState>();
    final GlobalKey<FormFieldState<String>> inputKey = GlobalKey<FormFieldState<String>>();
    final TextEditingController controller = TextEditingController(text: 'Plover');
420 421

    Widget builder() {
422 423
      return MaterialApp(
        home: MediaQuery(
424
          data: const MediaQueryData(),
425 426 427 428 429 430 431 432 433 434 435
          child: Directionality(
            textDirection: TextDirection.ltr,
            child: Center(
              child: Material(
                child: Form(
                  key: formKey,
                  child: TextFormField(
                    key: inputKey,
                    controller: controller,
                    // initialValue is 'Plover'
                  ),
436
                ),
437 438 439 440 441 442 443 444 445 446 447 448 449 450
              ),
            ),
          ),
        ),
      );
    }
    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'));
451
    expect(inputKey.currentState!.value, equals('Xyzzy'));
452 453 454
    expect(controller.text, equals('Xyzzy'));

    // verify value resets to initialValue on reset.
455
    formKey.currentState!.reset();
456
    await tester.idle();
457
    expect(inputKey.currentState!.value, equals('Plover'));
458 459
    expect(editableText.widget.controller.text, equals('Plover'));
    expect(controller.text, equals('Plover'));
460 461 462
  });

  testWidgets('TextEditingController updates to/from form field value', (WidgetTester tester) async {
463 464 465
    final TextEditingController controller1 = TextEditingController(text: 'Foo');
    final TextEditingController controller2 = TextEditingController(text: 'Bar');
    final GlobalKey<FormFieldState<String>> inputKey = GlobalKey<FormFieldState<String>>();
466

467 468
    TextEditingController? currentController;
    late StateSetter setState;
469 470

    Widget builder() {
471
      return StatefulBuilder(
472 473
        builder: (BuildContext context, StateSetter setter) {
          setState = setter;
474 475
          return MaterialApp(
            home: MediaQuery(
476
              data: const MediaQueryData(),
477 478 479 480 481 482 483 484 485
              child: Directionality(
                textDirection: TextDirection.ltr,
                child: Center(
                  child: Material(
                    child: Form(
                      child: TextFormField(
                        key: inputKey,
                        controller: currentController,
                      ),
486
                    ),
487 488 489 490 491 492 493 494 495 496 497 498 499 500
                  ),
                ),
              ),
            ),
          );
        },
      );
    }

    await tester.pumpWidget(builder());
    await tester.showKeyboard(find.byType(TextFormField));

    // verify initially empty.
    expect(tester.testTextInput.editingState, isNotNull);
501
    expect(tester.testTextInput.editingState!['text'], isEmpty);
502 503 504 505 506 507 508 509 510
    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'));
511
    expect(inputKey.currentState!.value, equals('Foo'));
512 513 514 515 516

    // 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'));
517
    expect(inputKey.currentState!.value, equals('Wobble'));
518 519 520 521

    // verify changes to the field text update the form value and controller1.
    await tester.enterText(find.byType(TextFormField), 'Wibble');
    await tester.pump();
522
    expect(inputKey.currentState!.value, equals('Wibble'));
523 524 525 526 527 528 529 530
    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();
531
    expect(inputKey.currentState!.value, equals('Bar'));
532 533 534 535 536 537 538 539
    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'));
540
    expect(inputKey.currentState!.value, equals('Xyzzy'));
541 542 543 544 545 546
    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'));
547
    expect(inputKey.currentState!.value, equals('Xyzzy'));
548 549 550 551 552 553 554
    expect(controller1.text, equals('Plugh'));

    // verify that switching from controller2 to null is handled.
    setState(() {
      currentController = null;
    });
    await tester.pump();
555
    expect(inputKey.currentState!.value, equals('Xyzzy'));
556 557 558 559 560 561 562
    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();
563
    expect(inputKey.currentState!.value, equals('Plover'));
564 565 566
    expect(editableText.widget.controller.text, equals('Plover'));
    expect(controller1.text, equals('Plugh'));
    expect(controller2.text, equals('Xyzzy'));
Matt Perry's avatar
Matt Perry committed
567 568
  });

569
  testWidgets('No crash when a TextFormField is removed from the tree', (WidgetTester tester) async {
570
    final GlobalKey<FormState> formKey = GlobalKey<FormState>();
571
    String? fieldValue;
Matt Perry's avatar
Matt Perry committed
572 573

    Widget builder(bool remove) {
574 575
      return MaterialApp(
        home: MediaQuery(
576
          data: const MediaQueryData(),
577 578 579 580 581 582 583 584
          child: Directionality(
            textDirection: TextDirection.ltr,
            child: Center(
              child: Material(
                child: Form(
                  key: formKey,
                  child: remove ? Container() : TextFormField(
                    autofocus: true,
585 586
                    onSaved: (String? value) { fieldValue = value; },
                    validator: (String? value) { return (value == null || value.isEmpty) ? null : 'yes'; },
587
                  ),
588
                ),
Ian Hickson's avatar
Ian Hickson committed
589
              ),
590
            ),
591 592
          ),
        ),
Matt Perry's avatar
Matt Perry committed
593 594 595 596 597 598
      );
    }

    await tester.pumpWidget(builder(false));

    expect(fieldValue, isNull);
599
    expect(formKey.currentState!.validate(), isTrue);
Matt Perry's avatar
Matt Perry committed
600

601
    await tester.enterText(find.byType(TextFormField), 'Test');
Matt Perry's avatar
Matt Perry committed
602 603
    await tester.pumpWidget(builder(false));

604
    // Form wasn't saved yet.
Matt Perry's avatar
Matt Perry committed
605
    expect(fieldValue, null);
606
    expect(formKey.currentState!.validate(), isFalse);
Matt Perry's avatar
Matt Perry committed
607

608
    formKey.currentState!.save();
Matt Perry's avatar
Matt Perry committed
609 610 611

    // Now fieldValue is saved.
    expect(fieldValue, 'Test');
612
    expect(formKey.currentState!.validate(), isFalse);
Matt Perry's avatar
Matt Perry committed
613 614 615

    // Now remove the field with an error.
    await tester.pumpWidget(builder(true));
616

Matt Perry's avatar
Matt Perry committed
617
    // Reset the form. Should not crash.
618 619 620
    formKey.currentState!.reset();
    formKey.currentState!.save();
    expect(formKey.currentState!.validate(), isTrue);
621
  });
622 623

  testWidgets('Does not auto-validate before value changes when autovalidateMode is set to onUserInteraction', (WidgetTester tester) async {
624
    late FormFieldState<String> formFieldState;
625

626
    String? errorText(String? value) => '$value/error';
627 628 629 630

    Widget builder() {
      return MaterialApp(
        home: MediaQuery(
631
          data: const MediaQueryData(),
632 633 634 635 636 637 638 639 640 641 642 643 644 645 646 647 648 649 650 651 652 653 654 655
          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.
656
    expect(find.text(errorText('foo')!), findsNothing);
657 658 659
  });

  testWidgets('auto-validate before value changes if autovalidateMode was set to always', (WidgetTester tester) async {
660
    late FormFieldState<String> formFieldState;
661

662
    String? errorText(String? value) => '$value/error';
663 664 665 666

    Widget builder() {
      return MaterialApp(
        home: MediaQuery(
667
          data: const MediaQueryData(),
668 669 670 671 672 673 674 675 676 677 678 679 680 681 682 683 684 685 686 687 688 689 690 691 692 693
          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';
694
    String? errorText(String? value) => 'error/$value';
695 696 697 698 699 700 701 702 703 704 705 706 707 708 709 710 711 712 713 714 715 716

    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,
717
                    ),
718 719 720 721 722 723 724 725 726
                  ],
                ),
              ),
            ),
          ),
        ),
      );
    }

727
    // Makes sure the Form widget won't auto-validate the form fields
728 729 730 731 732
    // after rebuilds if there is not user interaction.
    await tester.pumpWidget(builder());
    await tester.pumpWidget(builder());

    // We expect no validation error text being shown.
733
    expect(find.text(errorText(initialValue)!), findsNothing);
734 735 736 737 738 739 740 741

    // 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.
742 743
    expect(find.text(errorText('')!), findsOneWidget);
    expect(find.text(errorText(initialValue)!), findsNWidgets(2));
744 745 746
  });

  testWidgets('Form auto-validates form fields even before any have changed if autovalidateMode is set to always', (WidgetTester tester) async {
747
    String? errorText(String? value) => 'error/$value';
748 749 750 751 752 753 754 755 756 757 758 759 760 761 762 763 764 765 766 767 768 769 770 771 772

    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.
773
    expect(find.text(errorText('')!), findsOneWidget);
774 775 776 777
  });

  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>();
778
    String? errorText(String? value) => '$value/error';
779 780 781 782 783

    Widget builder() {
      return MaterialApp(
        theme: ThemeData(),
        home: MediaQuery(
784
          data: const MediaQueryData(),
785 786 787 788 789 790 791 792 793 794 795 796 797 798 799 800 801 802 803 804 805 806
          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.
807
    expect(find.text(errorText('foo')!), findsNothing);
808 809 810 811

    await tester.enterText(find.byType(TextFormField), 'bar');
    await tester.pumpAndSettle();
    await tester.pump();
812
    expect(find.text(errorText('bar')!), findsOneWidget);
813 814

    // Resetting the form state should remove the error text.
815
    formState.currentState!.reset();
816
    await tester.pump();
817
    expect(find.text(errorText('bar')!), findsNothing);
818 819
  });

820
  // Regression test for https://github.com/flutter/flutter/issues/63753.
821 822
  testWidgets('Validate form should return correct validation if the value is composing', (WidgetTester tester) async {
    final GlobalKey<FormState> formKey = GlobalKey<FormState>();
823
    String? fieldValue;
824 825 826

    final Widget widget = MaterialApp(
      home: MediaQuery(
827
        data: const MediaQueryData(),
828 829 830 831 832 833 834 835
        child: Directionality(
          textDirection: TextDirection.ltr,
          child: Center(
            child: Material(
              child: Form(
                key: formKey,
                child: TextFormField(
                  maxLength: 5,
836
                  maxLengthEnforcement: MaxLengthEnforcement.truncateAfterCompositionEnds,
837 838
                  onSaved: (String? value) { fieldValue = value; },
                  validator: (String? value) => (value != null && value.length > 5) ? 'Exceeded' : null,
839 840 841 842 843 844 845 846 847 848 849 850 851 852
                ),
              ),
            ),
          ),
        ),
      ),
    );

    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));

853
    formKey.currentState!.save();
854
    expect(fieldValue, '123456');
855
    expect(formKey.currentState!.validate(), isFalse);
856
  });
857
}