form_test.dart 27.6 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/services.dart';
7
import 'package:flutter_test/flutter_test.dart';
8 9

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

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

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

37
    expect(fieldValue, isNull);
38

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

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

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

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

    await tester.pumpWidget(builder());

    expect(fieldValue, isNull);

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

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

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

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

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

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

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

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

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

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

141 142 143 144
  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';
145
    String? validator(String? s) => s == validString ? null : 'Error text';
146 147 148 149

    Widget builder() {
      return MaterialApp(
        home: MediaQuery(
150
          data: const MediaQueryData(),
151 152 153 154 155 156 157 158 159 160 161
          child: Directionality(
            textDirection: TextDirection.ltr,
            child: Center(
              child: Material(
                child: Form(
                  child: ListView(
                    children: <Widget>[
                      TextFormField(
                        key: fieldKey1,
                        initialValue: validString,
                        validator: validator,
162
                        autovalidateMode: AutovalidateMode.always,
163 164 165 166 167
                      ),
                      TextFormField(
                        key: fieldKey2,
                        initialValue: validString,
                        validator: validator,
168
                        autovalidateMode: AutovalidateMode.always,
169 170 171 172 173 174 175 176 177 178 179 180 181
                      ),
                    ],
                  ),
                ),
              ),
            ),
          ),
        ),
      );
    }

    await tester.pumpWidget(builder());

182 183
    expect(fieldKey1.currentState!.isValid, isTrue);
    expect(fieldKey2.currentState!.isValid, isTrue);
184 185 186 187 188 189 190 191
  });

  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';
192
      String? validator(String? s) => s == validString ? null : 'Error text';
193 194 195 196

      Widget builder() {
        return MaterialApp(
          home: MediaQuery(
197
            data: const MediaQueryData(),
198 199 200 201 202 203 204 205 206 207 208
            child: Directionality(
              textDirection: TextDirection.ltr,
              child: Center(
                child: Material(
                  child: Form(
                    child: ListView(
                      children: <Widget>[
                        TextFormField(
                          key: fieldKey1,
                          initialValue: validString,
                          validator: validator,
209
                          autovalidateMode: AutovalidateMode.disabled,
210 211 212 213 214
                        ),
                        TextFormField(
                          key: fieldKey2,
                          initialValue: '',
                          validator: validator,
215
                          autovalidateMode: AutovalidateMode.disabled,
216 217 218 219 220 221 222 223 224 225 226 227 228
                        ),
                      ],
                    ),
                  ),
                ),
              ),
            ),
          ),
        );
      }

      await tester.pumpWidget(builder());

229 230 231
      expect(fieldKey1.currentState!.isValid, isTrue);
      expect(fieldKey2.currentState!.isValid, isFalse);
      expect(fieldKey2.currentState!.hasError, isFalse);
232 233 234
    },
  );

235
  testWidgets('Multiple TextFormFields communicate', (WidgetTester tester) async {
236 237
    final GlobalKey<FormState> formKey = GlobalKey<FormState>();
    final GlobalKey<FormFieldState<String>> fieldKey = GlobalKey<FormFieldState<String>>();
238
    // Input 2's validator depends on a input 1's value.
239
    String? errorText(String? input) => '${fieldKey.currentState!.value}/error';
240 241

    Widget builder() {
242 243
      return MaterialApp(
        home: MediaQuery(
244
          data: const MediaQueryData(),
245 246 247 248 249 250
          child: Directionality(
            textDirection: TextDirection.ltr,
            child: Center(
              child: Material(
                child: Form(
                  key: formKey,
251
                  autovalidateMode: AutovalidateMode.always,
252 253 254 255 256 257 258 259 260 261
                  child: ListView(
                    children: <Widget>[
                      TextFormField(
                        key: fieldKey,
                      ),
                      TextFormField(
                        validator: errorText,
                      ),
                    ],
                  ),
262
                ),
263
              ),
264
            ),
265 266
          ),
        ),
267 268
      );
    }
269

270
    await tester.pumpWidget(builder());
271

272
    Future<void> checkErrorText(String testValue) async {
273
      await tester.enterText(find.byType(TextFormField).first, testValue);
274
      await tester.pump();
275

276
      // Check for a new Text widget with our error text.
277
      expect(find.text('$testValue/error'), findsOneWidget);
278
      return;
279
    }
280

281 282
    await checkErrorText('Test');
    await checkErrorText('');
283
  });
284

285
  testWidgets('Provide initial value to input when no controller is specified', (WidgetTester tester) async {
286
    const String initialValue = 'hello';
287
    final GlobalKey<FormFieldState<String>> inputKey = GlobalKey<FormFieldState<String>>();
288 289

    Widget builder() {
290 291
      return MaterialApp(
        home: MediaQuery(
292
          data: const MediaQueryData(),
293 294 295 296 297 298 299 300 301
          child: Directionality(
            textDirection: TextDirection.ltr,
            child: Center(
              child: Material(
                child: Form(
                  child: TextFormField(
                    key: inputKey,
                    initialValue: 'hello',
                  ),
302
                ),
303 304 305 306 307 308 309 310 311 312 313 314
              ),
            ),
          ),
        ),
      );
    }

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

    // initial value should be loaded into keyboard editing state
    expect(tester.testTextInput.editingState, isNotNull);
315
    expect(tester.testTextInput.editingState!['text'], equals(initialValue));
316 317 318 319 320 321

    // 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
322
    expect(inputKey.currentState!.value, equals(initialValue));
323 324
    await tester.enterText(find.byType(TextFormField), 'world');
    await tester.pump();
325
    expect(inputKey.currentState!.value, equals('world'));
326 327 328
    expect(editableText.widget.controller.text, equals('world'));
  });

329
  testWidgets('Controller defines initial value', (WidgetTester tester) async {
330
    final TextEditingController controller = TextEditingController(text: 'hello');
331
    const String initialValue = 'hello';
332
    final GlobalKey<FormFieldState<String>> inputKey = GlobalKey<FormFieldState<String>>();
333 334

    Widget builder() {
335 336
      return MaterialApp(
        home: MediaQuery(
337
          data: const MediaQueryData(),
338 339 340 341 342 343 344 345 346
          child: Directionality(
            textDirection: TextDirection.ltr,
            child: Center(
              child: Material(
                child: Form(
                  child: TextFormField(
                    key: inputKey,
                    controller: controller,
                  ),
347
                ),
Ian Hickson's avatar
Ian Hickson committed
348
              ),
349
            ),
350 351
          ),
        ),
352 353 354 355
      );
    }

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

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

    // initial value should also be visible in the raw input line
363
    final EditableTextState editableText = tester.state(find.byType(EditableText));
364
    expect(editableText.widget.controller.text, equals(initialValue));
365
    expect(controller.text, equals(initialValue));
366 367

    // sanity check, make sure we can still edit the text and everything updates
368
    expect(inputKey.currentState!.value, equals(initialValue));
369
    await tester.enterText(find.byType(TextFormField), 'world');
370
    await tester.pump();
371
    expect(inputKey.currentState!.value, equals('world'));
372
    expect(editableText.widget.controller.text, equals('world'));
373 374 375 376
    expect(controller.text, equals('world'));
  });

  testWidgets('TextFormField resets to its initial value', (WidgetTester tester) async {
377 378 379
    final GlobalKey<FormState> formKey = GlobalKey<FormState>();
    final GlobalKey<FormFieldState<String>> inputKey = GlobalKey<FormFieldState<String>>();
    final TextEditingController controller = TextEditingController(text: 'Plover');
380 381

    Widget builder() {
382 383
      return MaterialApp(
        home: MediaQuery(
384
          data: const MediaQueryData(),
385 386 387 388 389 390 391 392 393 394 395
          child: Directionality(
            textDirection: TextDirection.ltr,
            child: Center(
              child: Material(
                child: Form(
                  key: formKey,
                  child: TextFormField(
                    key: inputKey,
                    controller: controller,
                    // initialValue is 'Plover'
                  ),
396
                ),
397 398 399 400 401 402 403 404 405 406 407 408 409 410
              ),
            ),
          ),
        ),
      );
    }
    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'));
411
    expect(inputKey.currentState!.value, equals('Xyzzy'));
412 413 414
    expect(controller.text, equals('Xyzzy'));

    // verify value resets to initialValue on reset.
415
    formKey.currentState!.reset();
416
    await tester.idle();
417
    expect(inputKey.currentState!.value, equals('Plover'));
418 419
    expect(editableText.widget.controller.text, equals('Plover'));
    expect(controller.text, equals('Plover'));
420 421 422
  });

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

427 428
    TextEditingController? currentController;
    late StateSetter setState;
429 430

    Widget builder() {
431
      return StatefulBuilder(
432 433
        builder: (BuildContext context, StateSetter setter) {
          setState = setter;
434 435
          return MaterialApp(
            home: MediaQuery(
436
              data: const MediaQueryData(),
437 438 439 440 441 442 443 444 445
              child: Directionality(
                textDirection: TextDirection.ltr,
                child: Center(
                  child: Material(
                    child: Form(
                      child: TextFormField(
                        key: inputKey,
                        controller: currentController,
                      ),
446
                    ),
447 448 449 450 451 452 453 454 455 456 457 458 459 460
                  ),
                ),
              ),
            ),
          );
        },
      );
    }

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

    // verify initially empty.
    expect(tester.testTextInput.editingState, isNotNull);
461
    expect(tester.testTextInput.editingState!['text'], isEmpty);
462 463 464 465 466 467 468 469 470
    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'));
471
    expect(inputKey.currentState!.value, equals('Foo'));
472 473 474 475 476

    // 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'));
477
    expect(inputKey.currentState!.value, equals('Wobble'));
478 479 480 481

    // verify changes to the field text update the form value and controller1.
    await tester.enterText(find.byType(TextFormField), 'Wibble');
    await tester.pump();
482
    expect(inputKey.currentState!.value, equals('Wibble'));
483 484 485 486 487 488 489 490
    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();
491
    expect(inputKey.currentState!.value, equals('Bar'));
492 493 494 495 496 497 498 499
    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'));
500
    expect(inputKey.currentState!.value, equals('Xyzzy'));
501 502 503 504 505 506
    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'));
507
    expect(inputKey.currentState!.value, equals('Xyzzy'));
508 509 510 511 512 513 514
    expect(controller1.text, equals('Plugh'));

    // verify that switching from controller2 to null is handled.
    setState(() {
      currentController = null;
    });
    await tester.pump();
515
    expect(inputKey.currentState!.value, equals('Xyzzy'));
516 517 518 519 520 521 522
    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();
523
    expect(inputKey.currentState!.value, equals('Plover'));
524 525 526
    expect(editableText.widget.controller.text, equals('Plover'));
    expect(controller1.text, equals('Plugh'));
    expect(controller2.text, equals('Xyzzy'));
Matt Perry's avatar
Matt Perry committed
527 528
  });

529
  testWidgets('No crash when a TextFormField is removed from the tree', (WidgetTester tester) async {
530
    final GlobalKey<FormState> formKey = GlobalKey<FormState>();
531
    String? fieldValue;
Matt Perry's avatar
Matt Perry committed
532 533

    Widget builder(bool remove) {
534 535
      return MaterialApp(
        home: MediaQuery(
536
          data: const MediaQueryData(),
537 538 539 540 541 542 543 544
          child: Directionality(
            textDirection: TextDirection.ltr,
            child: Center(
              child: Material(
                child: Form(
                  key: formKey,
                  child: remove ? Container() : TextFormField(
                    autofocus: true,
545 546
                    onSaved: (String? value) { fieldValue = value; },
                    validator: (String? value) { return (value == null || value.isEmpty) ? null : 'yes'; },
547
                  ),
548
                ),
Ian Hickson's avatar
Ian Hickson committed
549
              ),
550
            ),
551 552
          ),
        ),
Matt Perry's avatar
Matt Perry committed
553 554 555 556 557 558
      );
    }

    await tester.pumpWidget(builder(false));

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

561
    await tester.enterText(find.byType(TextFormField), 'Test');
Matt Perry's avatar
Matt Perry committed
562 563
    await tester.pumpWidget(builder(false));

564
    // Form wasn't saved yet.
Matt Perry's avatar
Matt Perry committed
565
    expect(fieldValue, null);
566
    expect(formKey.currentState!.validate(), isFalse);
Matt Perry's avatar
Matt Perry committed
567

568
    formKey.currentState!.save();
Matt Perry's avatar
Matt Perry committed
569 570 571

    // Now fieldValue is saved.
    expect(fieldValue, 'Test');
572
    expect(formKey.currentState!.validate(), isFalse);
Matt Perry's avatar
Matt Perry committed
573 574 575

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

Matt Perry's avatar
Matt Perry committed
577
    // Reset the form. Should not crash.
578 579 580
    formKey.currentState!.reset();
    formKey.currentState!.save();
    expect(formKey.currentState!.validate(), isTrue);
581
  });
582 583

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

586
    String? errorText(String? value) => '$value/error';
587 588 589 590

    Widget builder() {
      return MaterialApp(
        home: MediaQuery(
591
          data: const MediaQueryData(),
592 593 594 595 596 597 598 599 600 601 602 603 604 605 606 607 608 609 610 611 612 613 614 615
          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.
616
    expect(find.text(errorText('foo')!), findsNothing);
617 618 619
  });

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

622
    String? errorText(String? value) => '$value/error';
623 624 625 626

    Widget builder() {
      return MaterialApp(
        home: MediaQuery(
627
          data: const MediaQueryData(),
628 629 630 631 632 633 634 635 636 637 638 639 640 641 642 643 644 645 646 647 648 649 650 651 652 653
          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';
654
    String? errorText(String? value) => 'error/$value';
655 656 657 658 659 660 661 662 663 664 665 666 667 668 669 670 671 672 673 674 675 676

    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,
677
                    ),
678 679 680 681 682 683 684 685 686
                  ],
                ),
              ),
            ),
          ),
        ),
      );
    }

687
    // Makes sure the Form widget won't auto-validate the form fields
688 689 690 691 692
    // after rebuilds if there is not user interaction.
    await tester.pumpWidget(builder());
    await tester.pumpWidget(builder());

    // We expect no validation error text being shown.
693
    expect(find.text(errorText(initialValue)!), findsNothing);
694 695 696 697 698 699 700 701

    // 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.
702 703
    expect(find.text(errorText('')!), findsOneWidget);
    expect(find.text(errorText(initialValue)!), findsNWidgets(2));
704 705 706
  });

  testWidgets('Form auto-validates form fields even before any have changed if autovalidateMode is set to always', (WidgetTester tester) async {
707
    String? errorText(String? value) => 'error/$value';
708 709 710 711 712 713 714 715 716 717 718 719 720 721 722 723 724 725 726 727 728 729 730 731 732

    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.
733
    expect(find.text(errorText('')!), findsOneWidget);
734 735 736 737
  });

  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>();
738
    String? errorText(String? value) => '$value/error';
739 740 741 742 743

    Widget builder() {
      return MaterialApp(
        theme: ThemeData(),
        home: MediaQuery(
744
          data: const MediaQueryData(),
745 746 747 748 749 750 751 752 753 754 755 756 757 758 759 760 761 762 763 764 765 766
          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.
767
    expect(find.text(errorText('foo')!), findsNothing);
768 769 770 771

    await tester.enterText(find.byType(TextFormField), 'bar');
    await tester.pumpAndSettle();
    await tester.pump();
772
    expect(find.text(errorText('bar')!), findsOneWidget);
773 774

    // Resetting the form state should remove the error text.
775
    formState.currentState!.reset();
776
    await tester.pump();
777
    expect(find.text(errorText('bar')!), findsNothing);
778 779
  });

780
  // Regression test for https://github.com/flutter/flutter/issues/63753.
781 782
  testWidgets('Validate form should return correct validation if the value is composing', (WidgetTester tester) async {
    final GlobalKey<FormState> formKey = GlobalKey<FormState>();
783
    String? fieldValue;
784 785 786

    final Widget widget = MaterialApp(
      home: MediaQuery(
787
        data: const MediaQueryData(),
788 789 790 791 792 793 794 795
        child: Directionality(
          textDirection: TextDirection.ltr,
          child: Center(
            child: Material(
              child: Form(
                key: formKey,
                child: TextFormField(
                  maxLength: 5,
796
                  maxLengthEnforcement: MaxLengthEnforcement.truncateAfterCompositionEnds,
797 798
                  onSaved: (String? value) { fieldValue = value; },
                  validator: (String? value) => (value != null && value.length > 5) ? 'Exceeded' : null,
799 800 801 802 803 804 805 806 807 808 809 810 811 812
                ),
              ),
            ),
          ),
        ),
      ),
    );

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

813
    formKey.currentState!.save();
814
    expect(fieldValue, '123456');
815
    expect(formKey.currentState!.validate(), isFalse);
816
  });
817
}