form_test.dart 15.9 KB
Newer Older
1 2 3 4 5 6 7 8
// Copyright 2016 The Chromium 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_test/flutter_test.dart';
import 'package:flutter/material.dart';

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

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

32
    await tester.pumpWidget(builder());
33

34
    expect(fieldValue, isNull);
35

36
    Future<void> checkText(String testValue) async {
37
      await tester.enterText(find.byType(TextFormField), testValue);
Matt Perry's avatar
Matt Perry committed
38
      formKey.currentState.save();
39
      // pump'ing is unnecessary because callback happens regardless of frames
40 41
      expect(fieldValue, equals(testValue));
    }
42

43 44
    await checkText('Test');
    await checkText('');
45 46
  });

47 48 49 50
  testWidgets('onChanged callback is called', (WidgetTester tester) async {
    String fieldValue;

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

    await tester.pumpWidget(builder());

    expect(fieldValue, isNull);

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

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

82
  testWidgets('Validator sets the error text only when validate is called', (WidgetTester tester) async {
83
    final GlobalKey<FormState> formKey = GlobalKey<FormState>();
84
    String errorText(String value) => value + '/error';
85

86
    Widget builder(bool autovalidate) {
87 88 89 90 91 92 93 94 95 96 97 98
      return MediaQuery(
        data: const MediaQueryData(devicePixelRatio: 1.0),
        child: Directionality(
          textDirection: TextDirection.ltr,
          child: Center(
            child: Material(
              child: Form(
                key: formKey,
                autovalidate: autovalidate,
                child: TextFormField(
                  validator: errorText,
                ),
Ian Hickson's avatar
Ian Hickson committed
99
              ),
100
            ),
101 102
          ),
        ),
103 104
      );
    }
105

106 107
    // Start off not autovalidating.
    await tester.pumpWidget(builder(false));
108

109
    Future<void> checkErrorText(String testValue) async {
110 111
      formKey.currentState.reset();
      await tester.pumpWidget(builder(false));
112 113
      await tester.enterText(find.byType(TextFormField), testValue);
      await tester.pump();
114 115

      // We have to manually validate if we're not autovalidating.
116
      expect(find.text(errorText(testValue)), findsNothing);
117
      formKey.currentState.validate();
118
      await tester.pump();
119
      expect(find.text(errorText(testValue)), findsOneWidget);
120 121 122 123

      // Try again with autovalidation. Should validate immediately.
      formKey.currentState.reset();
      await tester.pumpWidget(builder(true));
124 125
      await tester.enterText(find.byType(TextFormField), testValue);
      await tester.pump();
126

127
      expect(find.text(errorText(testValue)), findsOneWidget);
128
    }
129

130 131
    await checkErrorText('Test');
    await checkErrorText('');
132 133
  });

134
  testWidgets('Multiple TextFormFields communicate', (WidgetTester tester) async {
135 136
    final GlobalKey<FormState> formKey = GlobalKey<FormState>();
    final GlobalKey<FormFieldState<String>> fieldKey = GlobalKey<FormFieldState<String>>();
137
    // Input 2's validator depends on a input 1's value.
138
    String errorText(String input) => '${fieldKey.currentState.value}/error';
139 140

    Widget builder() {
141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159
      return MediaQuery(
        data: const MediaQueryData(devicePixelRatio: 1.0),
        child: Directionality(
          textDirection: TextDirection.ltr,
          child: Center(
            child: Material(
              child: Form(
                key: formKey,
                autovalidate: true,
                child: ListView(
                  children: <Widget>[
                    TextFormField(
                      key: fieldKey,
                    ),
                    TextFormField(
                      validator: errorText,
                    ),
                  ],
                ),
160
              ),
161
            ),
162 163
          ),
        ),
164 165
      );
    }
166

167
    await tester.pumpWidget(builder());
168

169
    Future<void> checkErrorText(String testValue) async {
170
      await tester.enterText(find.byType(TextFormField).first, testValue);
171
      await tester.pump();
172

173
      // Check for a new Text widget with our error text.
Matt Perry's avatar
Matt Perry committed
174
      expect(find.text(testValue + '/error'), findsOneWidget);
175
      return;
176
    }
177

178 179
    await checkErrorText('Test');
    await checkErrorText('');
180
  });
181

182
  testWidgets('Provide initial value to input when no controller is specified', (WidgetTester tester) async {
183
    const String initialValue = 'hello';
184
    final GlobalKey<FormFieldState<String>> inputKey = GlobalKey<FormFieldState<String>>();
185 186

    Widget builder() {
187 188 189 190 191 192 193 194 195 196 197
      return MediaQuery(
        data: const MediaQueryData(devicePixelRatio: 1.0),
        child: Directionality(
          textDirection: TextDirection.ltr,
          child: Center(
            child: Material(
              child: Form(
                child: TextFormField(
                  key: inputKey,
                  initialValue: 'hello',
                ),
198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223
              ),
            ),
          ),
        ),
      );
    }

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

224
  testWidgets('Controller defines initial value', (WidgetTester tester) async {
225
    final TextEditingController controller = TextEditingController(text: 'hello');
226
    const String initialValue = 'hello';
227
    final GlobalKey<FormFieldState<String>> inputKey = GlobalKey<FormFieldState<String>>();
228 229

    Widget builder() {
230 231 232 233 234 235 236 237 238 239 240
      return MediaQuery(
        data: const MediaQueryData(devicePixelRatio: 1.0),
        child: Directionality(
          textDirection: TextDirection.ltr,
          child: Center(
            child: Material(
              child: Form(
                child: TextFormField(
                  key: inputKey,
                  controller: controller,
                ),
Ian Hickson's avatar
Ian Hickson committed
241
              ),
242
            ),
243 244
          ),
        ),
245 246 247 248
      );
    }

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

    // initial value should be loaded into keyboard editing state
252 253
    expect(tester.testTextInput.editingState, isNotNull);
    expect(tester.testTextInput.editingState['text'], equals(initialValue));
254 255

    // initial value should also be visible in the raw input line
256
    final EditableTextState editableText = tester.state(find.byType(EditableText));
257
    expect(editableText.widget.controller.text, equals(initialValue));
258
    expect(controller.text, equals(initialValue));
259 260

    // sanity check, make sure we can still edit the text and everything updates
261
    expect(inputKey.currentState.value, equals(initialValue));
262
    await tester.enterText(find.byType(TextFormField), 'world');
263
    await tester.pump();
264
    expect(inputKey.currentState.value, equals('world'));
265
    expect(editableText.widget.controller.text, equals('world'));
266 267 268 269
    expect(controller.text, equals('world'));
  });

  testWidgets('TextFormField resets to its initial value', (WidgetTester tester) async {
270 271 272
    final GlobalKey<FormState> formKey = GlobalKey<FormState>();
    final GlobalKey<FormFieldState<String>> inputKey = GlobalKey<FormFieldState<String>>();
    final TextEditingController controller = TextEditingController(text: 'Plover');
273 274

    Widget builder() {
275 276 277 278 279 280 281 282 283 284 285 286 287
      return MediaQuery(
        data: const MediaQueryData(devicePixelRatio: 1.0),
        child: Directionality(
          textDirection: TextDirection.ltr,
          child: Center(
            child: Material(
              child: Form(
                key: formKey,
                child: TextFormField(
                  key: inputKey,
                  controller: controller,
                  // initialValue is 'Plover'
                ),
288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307
              ),
            ),
          ),
        ),
      );
    }
    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();
308 309 310
    expect(inputKey.currentState.value, equals('Plover'));
    expect(editableText.widget.controller.text, equals('Plover'));
    expect(controller.text, equals('Plover'));
311 312 313
  });

  testWidgets('TextEditingController updates to/from form field value', (WidgetTester tester) async {
314 315 316
    final TextEditingController controller1 = TextEditingController(text: 'Foo');
    final TextEditingController controller2 = TextEditingController(text: 'Bar');
    final GlobalKey<FormFieldState<String>> inputKey = GlobalKey<FormFieldState<String>>();
317 318 319 320 321

    TextEditingController currentController;
    StateSetter setState;

    Widget builder() {
322
      return StatefulBuilder(
323 324
        builder: (BuildContext context, StateSetter setter) {
          setState = setter;
325 326 327 328 329 330 331 332 333 334 335
          return MediaQuery(
            data: const MediaQueryData(devicePixelRatio: 1.0),
            child: Directionality(
              textDirection: TextDirection.ltr,
              child: Center(
                child: Material(
                  child: Form(
                    child: TextFormField(
                      key: inputKey,
                      controller: currentController,
                    ),
336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415
                  ),
                ),
              ),
            ),
          );
        },
      );
    }

    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'));
Matt Perry's avatar
Matt Perry committed
416 417
  });

418
  testWidgets('No crash when a TextFormField is removed from the tree', (WidgetTester tester) async {
419
    final GlobalKey<FormState> formKey = GlobalKey<FormState>();
Matt Perry's avatar
Matt Perry committed
420 421 422
    String fieldValue;

    Widget builder(bool remove) {
423 424 425 426 427 428 429 430 431 432 433
      return MediaQuery(
        data: const MediaQueryData(devicePixelRatio: 1.0),
        child: Directionality(
          textDirection: TextDirection.ltr,
          child: Center(
            child: Material(
              child: Form(
                key: formKey,
                child: remove ? Container() : TextFormField(
                  autofocus: true,
                  onSaved: (String value) { fieldValue = value; },
434
                  validator: (String value) { return value.isEmpty ? null : 'yes'; },
435
                ),
Ian Hickson's avatar
Ian Hickson committed
436
              ),
437
            ),
438 439
          ),
        ),
Matt Perry's avatar
Matt Perry committed
440 441 442 443 444 445
      );
    }

    await tester.pumpWidget(builder(false));

    expect(fieldValue, isNull);
446
    expect(formKey.currentState.validate(), isTrue);
Matt Perry's avatar
Matt Perry committed
447

448
    await tester.enterText(find.byType(TextFormField), 'Test');
Matt Perry's avatar
Matt Perry committed
449 450
    await tester.pumpWidget(builder(false));

451
    // Form wasn't saved yet.
Matt Perry's avatar
Matt Perry committed
452
    expect(fieldValue, null);
453
    expect(formKey.currentState.validate(), isFalse);
Matt Perry's avatar
Matt Perry committed
454 455 456 457 458

    formKey.currentState.save();

    // Now fieldValue is saved.
    expect(fieldValue, 'Test');
459
    expect(formKey.currentState.validate(), isFalse);
Matt Perry's avatar
Matt Perry committed
460 461 462

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

Matt Perry's avatar
Matt Perry committed
464 465 466
    // Reset the form. Should not crash.
    formKey.currentState.reset();
    formKey.currentState.save();
467
    expect(formKey.currentState.validate(), isTrue);
468
  });
469
}