text_field_test.dart 148 KB
Newer Older
Hixie's avatar
Hixie committed
1 2 3 4
// Copyright 2015 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.

5
import 'dart:async';
6
import 'dart:math' as math;
7
import 'dart:ui' as ui show window;
8

9
import 'package:flutter/cupertino.dart';
Adam Barth's avatar
Adam Barth committed
10
import 'package:flutter/material.dart';
11
import 'package:flutter_test/flutter_test.dart';
12
import 'package:flutter/rendering.dart';
13
import 'package:flutter/services.dart';
14
import 'package:flutter/foundation.dart';
15
import 'package:flutter/gestures.dart' show DragStartBehavior;
16

17
import '../widgets/semantics_tester.dart';
18 19
import 'feedback_tester.dart';

20 21
class MockClipboard {
  Object _clipboardData = <String, dynamic>{
22
    'text': null,
23
  };
24

25 26
  Future<dynamic> handleMethodCall(MethodCall methodCall) async {
    switch (methodCall.method) {
27 28 29
      case 'Clipboard.getData':
        return _clipboardData;
      case 'Clipboard.setData':
30
        _clipboardData = methodCall.arguments;
31 32
        break;
    }
33 34 35
  }
}

36
class MaterialLocalizationsDelegate extends LocalizationsDelegate<MaterialLocalizations> {
37 38 39
  @override
  bool isSupported(Locale locale) => true;

40 41 42 43 44 45 46 47
  @override
  Future<MaterialLocalizations> load(Locale locale) => DefaultMaterialLocalizations.load(locale);

  @override
  bool shouldReload(MaterialLocalizationsDelegate old) => false;
}

class WidgetsLocalizationsDelegate extends LocalizationsDelegate<WidgetsLocalizations> {
48 49 50
  @override
  bool isSupported(Locale locale) => true;

51 52 53 54 55 56 57
  @override
  Future<WidgetsLocalizations> load(Locale locale) => DefaultWidgetsLocalizations.load(locale);

  @override
  bool shouldReload(WidgetsLocalizationsDelegate old) => false;
}

58
Widget overlay({ Widget child }) {
59
  return Localizations(
60 61
    locale: const Locale('en', 'US'),
    delegates: <LocalizationsDelegate<dynamic>>[
62 63
      WidgetsLocalizationsDelegate(),
      MaterialLocalizationsDelegate(),
64
    ],
65
    child: Directionality(
66
      textDirection: TextDirection.ltr,
67
      child: MediaQuery(
68
        data: const MediaQueryData(size: Size(800.0, 600.0)),
69
        child: Overlay(
70
          initialEntries: <OverlayEntry>[
71
            OverlayEntry(
72 73 74 75 76 77 78
              builder: (BuildContext context) {
                return Center(
                  child: Material(
                    child: child,
                  ),
                );
              },
79
            ),
80 81
          ],
        ),
82 83 84 85 86 87
      ),
    ),
  );
}

Widget boilerplate({ Widget child }) {
88
  return Localizations(
89 90
    locale: const Locale('en', 'US'),
    delegates: <LocalizationsDelegate<dynamic>>[
91 92
      WidgetsLocalizationsDelegate(),
      MaterialLocalizationsDelegate(),
93
    ],
94
    child: Directionality(
95
      textDirection: TextDirection.ltr,
96
      child: MediaQuery(
97
        data: const MediaQueryData(size: Size(800.0, 600.0)),
98 99
        child: Center(
          child: Material(
100 101
            child: child,
          ),
102
        ),
103
      ),
104
    ),
105 106 107
  );
}

108
Future<void> skipPastScrollingAnimation(WidgetTester tester) async {
109 110 111 112
  await tester.pump();
  await tester.pump(const Duration(milliseconds: 200));
}

113
double getOpacity(WidgetTester tester, Finder finder) {
114
  return tester.widget<FadeTransition>(
115 116
    find.ancestor(
      of: finder,
117
      matching: find.byType(FadeTransition),
118
    )
119
  ).opacity.value;
120 121
}

122
void main() {
123
  final MockClipboard mockClipboard = MockClipboard();
124
  SystemChannels.platform.setMockMethodCallHandler(mockClipboard.handleMethodCall);
125

126
  const String kThreeLines =
127 128 129
    'First line of text is\n'
    'Second line goes until\n'
    'Third line of stuff';
130
  const String kMoreThanFourLines =
131
    kThreeLines +
132
    '\nFourth line won\'t display and ends at';
133

134 135
  // Returns the first RenderEditable.
  RenderEditable findRenderEditable(WidgetTester tester) {
136
    final RenderObject root = tester.renderObject(find.byType(EditableText));
137 138
    expect(root, isNotNull);

139
    RenderEditable renderEditable;
140
    void recursiveFinder(RenderObject child) {
141 142
      if (child is RenderEditable) {
        renderEditable = child;
143 144 145 146 147
        return;
      }
      child.visitChildren(recursiveFinder);
    }
    root.visitChildren(recursiveFinder);
148 149
    expect(renderEditable, isNotNull);
    return renderEditable;
150 151
  }

152
  List<TextSelectionPoint> globalize(Iterable<TextSelectionPoint> points, RenderBox box) {
153
    return points.map<TextSelectionPoint>((TextSelectionPoint point) {
154
      return TextSelectionPoint(
155 156 157 158 159 160
        box.localToGlobal(point.point),
        point.direction,
      );
    }).toList();
  }

161
  Offset textOffsetToPosition(WidgetTester tester, int offset) {
162
    final RenderEditable renderEditable = findRenderEditable(tester);
163 164
    final List<TextSelectionPoint> endpoints = globalize(
      renderEditable.getEndpointsForSelection(
165
        TextSelection.collapsed(offset: offset),
166 167
      ),
      renderEditable,
168
    );
169
    expect(endpoints.length, 1);
170
    return endpoints[0].point + const Offset(0.0, -2.0);
171 172
  }

173 174 175 176
  setUp(() {
    debugResetSemanticsIdCounter();
  });

177 178 179 180
  testWidgets('TextField passes onEditingComplete to EditableText', (WidgetTester tester) async {
    final VoidCallback onEditingComplete = () {};

    await tester.pumpWidget(
181 182 183
      MaterialApp(
        home: Material(
          child: TextField(
184 185 186 187 188 189 190 191 192 193 194 195 196
            onEditingComplete: onEditingComplete,
          ),
        ),
      ),
    );

    final Finder editableTextFinder = find.byType(EditableText);
    expect(editableTextFinder, findsOneWidget);

    final EditableText editableTextWidget = tester.widget(editableTextFinder);
    expect(editableTextWidget.onEditingComplete, onEditingComplete);
  });

197
  testWidgets('TextField has consistent size', (WidgetTester tester) async {
198
    final Key textFieldKey = UniqueKey();
199
    String textFieldValue;
200

201 202
    await tester.pumpWidget(
      overlay(
203
        child: TextField(
204 205 206
          key: textFieldKey,
          decoration: const InputDecoration(
            hintText: 'Placeholder',
207
          ),
208 209 210
          onChanged: (String value) {
            textFieldValue = value;
          }
211
        ),
212 213
      )
    );
214

215
    RenderBox findTextFieldBox() => tester.renderObject(find.byKey(textFieldKey));
216

217
    final RenderBox inputBox = findTextFieldBox();
218
    final Size emptyInputSize = inputBox.size;
219

220
    Future<void> checkText(String testValue) async {
221
      return TestAsyncUtils.guard(() async {
222
        await tester.enterText(find.byType(TextField), testValue);
223 224
        // Check that the onChanged event handler fired.
        expect(textFieldValue, equals(testValue));
225
        await skipPastScrollingAnimation(tester);
226
      });
227
    }
228

229
    await checkText(' ');
230

231
    expect(findTextFieldBox(), equals(inputBox));
232
    expect(inputBox.size, equals(emptyInputSize));
233

234
    await checkText('Test');
235
    expect(findTextFieldBox(), equals(inputBox));
236
    expect(inputBox.size, equals(emptyInputSize));
237
  });
238

239
  testWidgets('Cursor blinks', (WidgetTester tester) async {
240 241 242
    await tester.pumpWidget(
      overlay(
        child: const TextField(
243
          decoration: InputDecoration(
244
            hintText: 'Placeholder',
245 246
          ),
        ),
247 248
      ),
    );
249
    await tester.showKeyboard(find.byType(TextField));
250

251
    final EditableTextState editableText = tester.state(find.byType(EditableText));
252 253

    // Check that the cursor visibility toggles after each blink interval.
254
    Future<void> checkCursorToggle() async {
255
      final bool initialShowCursor = editableText.cursorCurrentlyVisible;
256
      await tester.pump(editableText.cursorBlinkInterval);
257
      expect(editableText.cursorCurrentlyVisible, equals(!initialShowCursor));
258
      await tester.pump(editableText.cursorBlinkInterval);
259
      expect(editableText.cursorCurrentlyVisible, equals(initialShowCursor));
260
      await tester.pump(editableText.cursorBlinkInterval ~/ 10);
261
      expect(editableText.cursorCurrentlyVisible, equals(initialShowCursor));
262
      await tester.pump(editableText.cursorBlinkInterval);
263
      expect(editableText.cursorCurrentlyVisible, equals(!initialShowCursor));
264
      await tester.pump(editableText.cursorBlinkInterval);
265 266
      expect(editableText.cursorCurrentlyVisible, equals(initialShowCursor));
    }
267

268
    await checkCursorToggle();
269
    await tester.showKeyboard(find.byType(TextField));
270

271
    // Try the test again with a nonempty EditableText.
272
    tester.testTextInput.updateEditingValue(const TextEditingValue(
273
      text: 'X',
274
      selection: TextSelection.collapsed(offset: 1),
275
    ));
276
    await checkCursorToggle();
277
  });
278

279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337
  testWidgets('Cursor animates on iOS', (WidgetTester tester) async {
    debugDefaultTargetPlatformOverride = TargetPlatform.iOS;

    await tester.pumpWidget(
      const MaterialApp(
        home: Material(
          child: TextField(),
        ),
      ),
    );

    final Finder textFinder = find.byType(TextField);
    await tester.tap(textFinder);
    await tester.pump();

    final EditableTextState editableTextState = tester.firstState(find.byType(EditableText));
    final RenderEditable renderEditable = editableTextState.renderEditable;

    expect(renderEditable.cursorColor.alpha, 255);

    await tester.pump(const Duration(milliseconds: 100));
    await tester.pump(const Duration(milliseconds: 400));

    expect(renderEditable.cursorColor.alpha, 255);

    await tester.pump(const Duration(milliseconds: 200));
    await tester.pump(const Duration(milliseconds: 100));

    expect(renderEditable.cursorColor.alpha, 110);

    await tester.pump(const Duration(milliseconds: 100));

    expect(renderEditable.cursorColor.alpha, 16);
    await tester.pump(const Duration(milliseconds: 50));

    expect(renderEditable.cursorColor.alpha, 0);

    debugDefaultTargetPlatformOverride = null;
  });

  testWidgets('Cursor radius is 2.0 on iOS', (WidgetTester tester) async {
    debugDefaultTargetPlatformOverride = TargetPlatform.iOS;

    await tester.pumpWidget(
      const MaterialApp(
        home: Material(
          child: TextField(),
        ),
      ),
    );

    final EditableTextState editableTextState = tester.firstState(find.byType(EditableText));
    final RenderEditable renderEditable = editableTextState.renderEditable;

    expect(renderEditable.cursorRadius, const Radius.circular(2.0));

    debugDefaultTargetPlatformOverride = null;
  });

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
  testWidgets('cursor has expected defaults', (WidgetTester tester) async {
    await tester.pumpWidget(
        overlay(
          child: const TextField(
          ),
        )
    );

    final TextField textField = tester.firstWidget(find.byType(TextField));
    expect(textField.cursorWidth, 2.0);
    expect(textField.cursorRadius, null);
  });

  testWidgets('cursor has expected radius value', (WidgetTester tester) async {
    await tester.pumpWidget(
        overlay(
          child: const TextField(
            cursorRadius: Radius.circular(3.0),
          ),
        )
    );

    final TextField textField = tester.firstWidget(find.byType(TextField));
    expect(textField.cursorWidth, 2.0);
    expect(textField.cursorRadius, const Radius.circular(3.0));
  });

365 366
  // TODO(hansmuller): restore these tests after the fix for #24876 has landed.
  /*
367
  testWidgets('cursor layout has correct width', (WidgetTester tester) async {
368
    EditableText.debugDeterministicCursor = true;
369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384
    await tester.pumpWidget(
        overlay(
          child: const RepaintBoundary(
            child: TextField(
              cursorWidth: 15.0,
            ),
          ),
        )
    );
    await tester.enterText(find.byType(TextField), ' ');
    await skipPastScrollingAnimation(tester);

    await expectLater(
      find.byType(TextField),
      matchesGoldenFile('text_field_test.0.0.png'),
    );
385
    EditableText.debugDeterministicCursor = false;
386 387 388
  }, skip: !Platform.isLinux);

  testWidgets('cursor layout has correct radius', (WidgetTester tester) async {
389
    EditableText.debugDeterministicCursor = true;
390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406
    await tester.pumpWidget(
        overlay(
          child: const RepaintBoundary(
            child: TextField(
              cursorWidth: 15.0,
              cursorRadius: Radius.circular(3.0),
            ),
          ),
        )
    );
    await tester.enterText(find.byType(TextField), ' ');
    await skipPastScrollingAnimation(tester);

    await expectLater(
      find.byType(TextField),
      matchesGoldenFile('text_field_test.1.0.png'),
    );
407
    EditableText.debugDeterministicCursor = false;
408
  }, skip: !Platform.isLinux);
409
  */
410

411
  testWidgets('obscureText control test', (WidgetTester tester) async {
412 413 414 415
    await tester.pumpWidget(
      overlay(
        child: const TextField(
          obscureText: true,
416
          decoration: InputDecoration(
417
            hintText: 'Placeholder',
418 419
          ),
        ),
420 421
      ),
    );
422
    await tester.showKeyboard(find.byType(TextField));
Adam Barth's avatar
Adam Barth committed
423

424
    const String testValue = 'ABC';
425
    tester.testTextInput.updateEditingValue(const TextEditingValue(
426
      text: testValue,
427
      selection: TextSelection.collapsed(offset: testValue.length),
428
    ));
Adam Barth's avatar
Adam Barth committed
429

430
    await tester.pump();
431 432 433 434 435 436

    // Enter a character into the obscured field and verify that the character
    // is temporarily shown to the user and then changed to a bullet.
    const String newChar = 'X';
    tester.testTextInput.updateEditingValue(const TextEditingValue(
      text: testValue + newChar,
437
      selection: TextSelection.collapsed(offset: testValue.length + 1),
438 439 440 441 442 443 444 445 446 447 448
    ));

    await tester.pump();

    String editText = findRenderEditable(tester).text.text;
    expect(editText.substring(editText.length - 1), newChar);

    await tester.pump(const Duration(seconds: 2));

    editText = findRenderEditable(tester).text.text;
    expect(editText.substring(editText.length - 1), '\u2022');
Adam Barth's avatar
Adam Barth committed
449
  });
450

451
  testWidgets('Caret position is updated on tap', (WidgetTester tester) async {
452
    final TextEditingController controller = TextEditingController();
453

454 455
    await tester.pumpWidget(
      overlay(
456
        child: TextField(
457
          controller: controller,
458
        ),
459 460
      )
    );
461 462 463
    expect(controller.selection.baseOffset, -1);
    expect(controller.selection.extentOffset, -1);

464
    const String testValue = 'abc def ghi';
465
    await tester.enterText(find.byType(TextField), testValue);
466
    await skipPastScrollingAnimation(tester);
467 468 469

    // Tap to reposition the caret.
    final int tapIndex = testValue.indexOf('e');
470
    final Offset ePos = textOffsetToPosition(tester, tapIndex);
471
    await tester.tapAt(ePos);
472
    await tester.pump();
473 474 475 476 477

    expect(controller.selection.baseOffset, tapIndex);
    expect(controller.selection.extentOffset, tapIndex);
  });

478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505
  testWidgets('enableInteractiveSelection = false, tap', (WidgetTester tester) async {
    final TextEditingController controller = TextEditingController();

    await tester.pumpWidget(
      overlay(
        child: TextField(
          controller: controller,
          enableInteractiveSelection: false,
        ),
      )
    );
    expect(controller.selection.baseOffset, -1);
    expect(controller.selection.extentOffset, -1);

    const String testValue = 'abc def ghi';
    await tester.enterText(find.byType(TextField), testValue);
    await skipPastScrollingAnimation(tester);

    // Tap would ordinarily reposition the caret.
    final int tapIndex = testValue.indexOf('e');
    final Offset ePos = textOffsetToPosition(tester, tapIndex);
    await tester.tapAt(ePos);
    await tester.pump();

    expect(controller.selection.baseOffset, -1);
    expect(controller.selection.extentOffset, -1);
  });

506
  testWidgets('Can long press to select', (WidgetTester tester) async {
507
    final TextEditingController controller = TextEditingController();
508

509 510
    await tester.pumpWidget(
      overlay(
511
        child: TextField(
512
          controller: controller,
513
        ),
514 515
      )
    );
516

517
    const String testValue = 'abc def ghi';
518
    await tester.enterText(find.byType(TextField), testValue);
519
    expect(controller.value.text, testValue);
520
    await skipPastScrollingAnimation(tester);
521

522
    expect(controller.selection.isCollapsed, true);
523

524
    // Long press the 'e' to select 'def'.
525
    final Offset ePos = textOffsetToPosition(tester, testValue.indexOf('e'));
526
    final TestGesture gesture = await tester.startGesture(ePos, pointer: 7);
527 528 529
    await tester.pump(const Duration(seconds: 2));
    await gesture.up();
    await tester.pump();
530

531
    // 'def' is selected.
532 533
    expect(controller.selection.baseOffset, testValue.indexOf('d'));
    expect(controller.selection.extentOffset, testValue.indexOf('f')+1);
534 535
  });

536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566
  testWidgets('enableInteractiveSelection = false, long-press', (WidgetTester tester) async {
    final TextEditingController controller = TextEditingController();

    await tester.pumpWidget(
      overlay(
        child: TextField(
          controller: controller,
          enableInteractiveSelection: false,
        ),
      )
    );

    const String testValue = 'abc def ghi';
    await tester.enterText(find.byType(TextField), testValue);
    expect(controller.value.text, testValue);
    await skipPastScrollingAnimation(tester);

    expect(controller.selection.isCollapsed, true);

    // Long press the 'e' to select 'def'.
    final Offset ePos = textOffsetToPosition(tester, testValue.indexOf('e'));
    final TestGesture gesture = await tester.startGesture(ePos, pointer: 7);
    await tester.pump(const Duration(seconds: 2));
    await gesture.up();
    await tester.pump();

    expect(controller.selection.isCollapsed, true);
    expect(controller.selection.baseOffset, -1);
    expect(controller.selection.extentOffset, -1);
  });

567
  testWidgets('Can drag handles to change selection', (WidgetTester tester) async {
568
    final TextEditingController controller = TextEditingController();
569

570
    await tester.pumpWidget(
571 572 573 574 575 576
      MaterialApp(
        home: Material(
          child: TextField(
            dragStartBehavior: DragStartBehavior.down,
            controller: controller,
          ),
577
        ),
578 579
      ),
    );
580

581
    const String testValue = 'abc def ghi';
582
    await tester.enterText(find.byType(TextField), testValue);
583
    await skipPastScrollingAnimation(tester);
584

585
    // Long press the 'e' to select 'def'.
586
    final Offset ePos = textOffsetToPosition(tester, testValue.indexOf('e'));
587 588 589 590
    TestGesture gesture = await tester.startGesture(ePos, pointer: 7);
    await tester.pump(const Duration(seconds: 2));
    await gesture.up();
    await tester.pump();
591
    await tester.pump(const Duration(milliseconds: 200)); // skip past the frame where the opacity is zero
592

593
    final TextSelection selection = controller.selection;
594

595
    final RenderEditable renderEditable = findRenderEditable(tester);
596 597 598 599
    final List<TextSelectionPoint> endpoints = globalize(
      renderEditable.getEndpointsForSelection(selection),
      renderEditable,
    );
600 601 602
    expect(endpoints.length, 2);

    // Drag the right handle 2 letters to the right.
603
    // We use a small offset because the endpoint is on the very corner
604
    // of the handle.
605 606
    Offset handlePos = endpoints[1].point + const Offset(1.0, 1.0);
    Offset newHandlePos = textOffsetToPosition(tester, selection.extentOffset+2);
607 608 609 610 611
    gesture = await tester.startGesture(handlePos, pointer: 7);
    await tester.pump();
    await gesture.moveTo(newHandlePos);
    await tester.pump();
    await gesture.up();
612
    await tester.pump();
613

614
    expect(controller.selection.baseOffset, selection.baseOffset);
615
    expect(controller.selection.extentOffset, selection.extentOffset);
616 617

    // Drag the left handle 2 letters to the left.
618
    handlePos = endpoints[0].point + const Offset(-1.0, 1.0);
619
    newHandlePos = textOffsetToPosition(tester, selection.baseOffset-2);
620 621 622 623 624
    gesture = await tester.startGesture(handlePos, pointer: 7);
    await tester.pump();
    await gesture.moveTo(newHandlePos);
    await tester.pump();
    await gesture.up();
625
    await tester.pump();
626

627 628
    expect(controller.selection.baseOffset, selection.baseOffset);
    expect(controller.selection.extentOffset, selection.extentOffset);
629 630
  });

631
  testWidgets('Can use selection toolbar', (WidgetTester tester) async {
632
    final TextEditingController controller = TextEditingController();
633

634 635
    await tester.pumpWidget(
      overlay(
636
        child: TextField(
637
          controller: controller,
638
        ),
639 640
      ),
    );
641

642
    const String testValue = 'abc def ghi';
643
    await tester.enterText(find.byType(TextField), testValue);
644
    await skipPastScrollingAnimation(tester);
645

646
    // Tap the selection handle to bring up the "paste / select all" menu.
647
    await tester.tapAt(textOffsetToPosition(tester, testValue.indexOf('e')));
648
    await tester.pump();
649
    await tester.pump(const Duration(milliseconds: 200)); // skip past the frame where the opacity is zero
650
    RenderEditable renderEditable = findRenderEditable(tester);
651 652 653 654
    List<TextSelectionPoint> endpoints = globalize(
      renderEditable.getEndpointsForSelection(controller.selection),
      renderEditable,
    );
655
    await tester.tapAt(endpoints[0].point + const Offset(1.0, 1.0));
656
    await tester.pump();
657
    await tester.pump(const Duration(milliseconds: 200)); // skip past the frame where the opacity is zero
658 659

    // SELECT ALL should select all the text.
660
    await tester.tap(find.text('SELECT ALL'));
661
    await tester.pump();
662 663
    expect(controller.selection.baseOffset, 0);
    expect(controller.selection.extentOffset, testValue.length);
664 665

    // COPY should reset the selection.
666
    await tester.tap(find.text('COPY'));
667
    await skipPastScrollingAnimation(tester);
668
    expect(controller.selection.isCollapsed, true);
669 670

    // Tap again to bring back the menu.
671
    await tester.tapAt(textOffsetToPosition(tester, testValue.indexOf('e')));
672
    await tester.pump();
673
    await tester.pump(const Duration(milliseconds: 200)); // skip past the frame where the opacity is zero
674
    renderEditable = findRenderEditable(tester);
675 676 677 678
    endpoints = globalize(
      renderEditable.getEndpointsForSelection(controller.selection),
      renderEditable,
    );
679
    await tester.tapAt(endpoints[0].point + const Offset(1.0, 1.0));
680
    await tester.pump();
681
    await tester.pump(const Duration(milliseconds: 200)); // skip past the frame where the opacity is zero
682 683

    // PASTE right before the 'e'.
684
    await tester.tap(find.text('PASTE'));
685
    await tester.pump();
686
    expect(controller.text, 'abc d${testValue}ef ghi');
687
  });
688 689

  testWidgets('Selection toolbar fades in', (WidgetTester tester) async {
690
    final TextEditingController controller = TextEditingController();
691

692 693
    await tester.pumpWidget(
      overlay(
694
        child: TextField(
695
          controller: controller,
696
        ),
697 698
      ),
    );
699

700
    const String testValue = 'abc def ghi';
701
    await tester.enterText(find.byType(TextField), testValue);
702
    await skipPastScrollingAnimation(tester);
703 704 705

    // Tap the selection handle to bring up the "paste / select all" menu.
    await tester.tapAt(textOffsetToPosition(tester, testValue.indexOf('e')));
706
    await tester.pump();
707
    await tester.pump(const Duration(milliseconds: 200)); // skip past the frame where the opacity is zero
708
    final RenderEditable renderEditable = findRenderEditable(tester);
709 710 711 712
    final List<TextSelectionPoint> endpoints = globalize(
      renderEditable.getEndpointsForSelection(controller.selection),
      renderEditable,
    );
713
    await tester.tapAt(endpoints[0].point + const Offset(1.0, 1.0));
714
    await tester.pump();
715 716

    // Toolbar should fade in. Starting at 0% opacity.
717
    final Element target = tester.element(find.text('SELECT ALL'));
718
    final FadeTransition opacity = target.ancestorWidgetOfExactType(FadeTransition);
719
    expect(opacity, isNotNull);
720
    expect(opacity.opacity.value, equals(0.0));
721 722 723

    // Still fading in.
    await tester.pump(const Duration(milliseconds: 50));
724 725 726 727
    final FadeTransition opacity2 = target.ancestorWidgetOfExactType(FadeTransition);
    expect(opacity, same(opacity2));
    expect(opacity.opacity.value, greaterThan(0.0));
    expect(opacity.opacity.value, lessThan(1.0));
728 729 730

    // End the test here to ensure the animation is properly disposed of.
  });
731

732 733 734 735 736 737 738 739 740 741 742 743 744 745 746 747 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 773 774 775 776 777 778 779 780 781 782 783 784 785 786 787 788 789 790 791
  testWidgets('An obscured TextField is not selectable by default', (WidgetTester tester) async {
    // This is a regression test for
    // https://github.com/flutter/flutter/issues/24100

    final TextEditingController controller = TextEditingController();
    Widget buildFrame(bool obscureText, bool enableInteractiveSelection) {
      return overlay(
        child: TextField(
          controller: controller,
          obscureText: obscureText,
          enableInteractiveSelection: enableInteractiveSelection,
        ),
      );
    }

    // Obscure text and don't enable or disable selection
    await tester.pumpWidget(buildFrame(true, null));
    await tester.enterText(find.byType(TextField), 'abcdefghi');
    await skipPastScrollingAnimation(tester);
    expect(controller.selection.isCollapsed, true);

    // Long press doesn't select anything
    final Offset ePos = textOffsetToPosition(tester, 1);
    final TestGesture gesture = await tester.startGesture(ePos, pointer: 7);
    await tester.pump(const Duration(seconds: 2));
    await gesture.up();
    await tester.pump();
    expect(controller.selection.isCollapsed, true);
  });

  testWidgets('An obscured TextField is selectable when enabled', (WidgetTester tester) async {
    // This is a regression test for
    // https://github.com/flutter/flutter/issues/24100

    final TextEditingController controller = TextEditingController();
    Widget buildFrame(bool obscureText, bool enableInteractiveSelection) {
      return overlay(
        child: TextField(
          controller: controller,
          obscureText: obscureText,
          enableInteractiveSelection: enableInteractiveSelection,
        ),
      );
    }

    // Explicitly allow selection on obscured text
    await tester.pumpWidget(buildFrame(true, true));
    await tester.enterText(find.byType(TextField), 'abcdefghi');
    await skipPastScrollingAnimation(tester);
    expect(controller.selection.isCollapsed, true);

    // Long press does select text
    final Offset ePos2 = textOffsetToPosition(tester, 1);
    final TestGesture gesture2 = await tester.startGesture(ePos2, pointer: 7);
    await tester.pump(const Duration(seconds: 2));
    await gesture2.up();
    await tester.pump();
    expect(controller.selection.isCollapsed, false);
  });

792
  testWidgets('Multiline text will wrap up to maxLines', (WidgetTester tester) async {
793
    final Key textFieldKey = UniqueKey();
794

795
    Widget builder(int maxLines) {
796
      return boilerplate(
797
        child: TextField(
798 799 800 801 802
          key: textFieldKey,
          style: const TextStyle(color: Colors.black, fontSize: 34.0),
          maxLines: maxLines,
          decoration: const InputDecoration(
            hintText: 'Placeholder',
803 804
          ),
        ),
805 806 807
      );
    }

808
    await tester.pumpWidget(builder(null));
809

810
    RenderBox findInputBox() => tester.renderObject(find.byKey(textFieldKey));
811

812 813
    final RenderBox inputBox = findInputBox();
    final Size emptyInputSize = inputBox.size;
814

815
    await tester.enterText(find.byType(TextField), 'No wrapping here.');
816
    await tester.pumpWidget(builder(null));
817 818 819 820
    expect(findInputBox(), equals(inputBox));
    expect(inputBox.size, equals(emptyInputSize));

    await tester.pumpWidget(builder(3));
821 822 823
    expect(findInputBox(), equals(inputBox));
    expect(inputBox.size, greaterThan(emptyInputSize));

824
    final Size threeLineInputSize = inputBox.size;
825

826 827 828 829 830 831 832 833 834 835
    await tester.enterText(find.byType(TextField), kThreeLines);
    await tester.pumpWidget(builder(null));
    expect(findInputBox(), equals(inputBox));
    expect(inputBox.size, greaterThan(emptyInputSize));

    await tester.enterText(find.byType(TextField), kThreeLines);
    await tester.pumpWidget(builder(null));
    expect(findInputBox(), equals(inputBox));
    expect(inputBox.size, threeLineInputSize);

836
    // An extra line won't increase the size because we max at 3.
837
    await tester.enterText(find.byType(TextField), kMoreThanFourLines);
838
    await tester.pumpWidget(builder(3));
839
    expect(findInputBox(), equals(inputBox));
840 841
    expect(inputBox.size, threeLineInputSize);

842 843
    // But now it will... but it will max at four
    await tester.enterText(find.byType(TextField), kMoreThanFourLines);
844 845 846
    await tester.pumpWidget(builder(4));
    expect(findInputBox(), equals(inputBox));
    expect(inputBox.size, greaterThan(threeLineInputSize));
847 848 849 850 851 852 853

    final Size fourLineInputSize = inputBox.size;

    // Now it won't max out until the end
    await tester.pumpWidget(builder(null));
    expect(findInputBox(), equals(inputBox));
    expect(inputBox.size, greaterThan(fourLineInputSize));
854 855
  });

856 857 858 859 860 861 862 863 864 865 866 867 868 869 870 871 872 873 874 875 876 877 878 879 880 881 882 883 884 885 886 887 888 889 890 891 892

  testWidgets('Multiline hint text will wrap up to maxLines', (WidgetTester tester) async {
    final Key textFieldKey = UniqueKey();

    Widget builder(int maxLines, final String hintMsg) {
      return boilerplate(
        child: TextField(
          key: textFieldKey,
          style: const TextStyle(color: Colors.black, fontSize: 34.0),
          maxLines: maxLines,
          decoration: InputDecoration(
            hintText: hintMsg,
          ),
        ),
      );
    }

    const String hintPlaceholder = 'Placeholder';
    const String multipleLineText = 'Here\'s a text, which is more than one line, to demostrate the multiple line hint text';
    await tester.pumpWidget(builder(null, hintPlaceholder));

    RenderBox findHintText(String hint) => tester.renderObject(find.text(hint));

    final RenderBox hintTextBox = findHintText(hintPlaceholder);
    final Size oneLineHintSize = hintTextBox.size;

    await tester.pumpWidget(builder(null, hintPlaceholder));
    expect(findHintText(hintPlaceholder), equals(hintTextBox));
    expect(hintTextBox.size, equals(oneLineHintSize));

    const int maxLines = 3;
    await tester.pumpWidget(builder(maxLines, multipleLineText));
    final Text hintTextWidget = tester.widget(find.text(multipleLineText));
    expect(hintTextWidget.maxLines, equals(maxLines));
    expect(findHintText(multipleLineText).size, greaterThan(oneLineHintSize));
  });

893
  testWidgets('Can drag handles to change selection in multiline', (WidgetTester tester) async {
894
    final TextEditingController controller = TextEditingController();
895

896 897
    await tester.pumpWidget(
      overlay(
898
        child: TextField(
899
          dragStartBehavior: DragStartBehavior.down,
900 901 902
          controller: controller,
          style: const TextStyle(color: Colors.black, fontSize: 34.0),
          maxLines: 3,
903
        ),
904 905
      ),
    );
906

907
    const String testValue = kThreeLines;
908
    const String cutValue = 'First line of stuff';
909
    await tester.enterText(find.byType(TextField), testValue);
910
    await skipPastScrollingAnimation(tester);
911 912

    // Check that the text spans multiple lines.
913 914 915
    final Offset firstPos = textOffsetToPosition(tester, testValue.indexOf('First'));
    final Offset secondPos = textOffsetToPosition(tester, testValue.indexOf('Second'));
    final Offset thirdPos = textOffsetToPosition(tester, testValue.indexOf('Third'));
916 917 918 919 920
    final Offset middleStringPos = textOffsetToPosition(tester, testValue.indexOf('irst'));
    expect(firstPos.dx, 0);
    expect(secondPos.dx, 0);
    expect(thirdPos.dx, 0);
    expect(middleStringPos.dx, 34);
921 922 923 924
    expect(firstPos.dx, secondPos.dx);
    expect(firstPos.dx, thirdPos.dx);
    expect(firstPos.dy, lessThan(secondPos.dy));
    expect(secondPos.dy, lessThan(thirdPos.dy));
925 926

    // Long press the 'n' in 'until' to select the word.
927
    final Offset untilPos = textOffsetToPosition(tester, testValue.indexOf('until')+1);
928 929 930 931
    TestGesture gesture = await tester.startGesture(untilPos, pointer: 7);
    await tester.pump(const Duration(seconds: 2));
    await gesture.up();
    await tester.pump();
932
    await tester.pump(const Duration(milliseconds: 200)); // skip past the frame where the opacity is zero
933

934 935
    expect(controller.selection.baseOffset, 39);
    expect(controller.selection.extentOffset, 44);
936

937
    final RenderEditable renderEditable = findRenderEditable(tester);
938 939 940 941
    final List<TextSelectionPoint> endpoints = globalize(
      renderEditable.getEndpointsForSelection(controller.selection),
      renderEditable,
    );
942 943 944
    expect(endpoints.length, 2);

    // Drag the right handle to the third line, just after 'Third'.
945 946
    Offset handlePos = endpoints[1].point + const Offset(1.0, 1.0);
    Offset newHandlePos = textOffsetToPosition(tester, testValue.indexOf('Third') + 5);
947 948 949 950 951
    gesture = await tester.startGesture(handlePos, pointer: 7);
    await tester.pump();
    await gesture.moveTo(newHandlePos);
    await tester.pump();
    await gesture.up();
952
    await tester.pump();
953

954 955
    expect(controller.selection.baseOffset, 39);
    expect(controller.selection.extentOffset, 50);
956 957

    // Drag the left handle to the first line, just after 'First'.
958
    handlePos = endpoints[0].point + const Offset(-1.0, 1.0);
959 960 961 962 963 964
    newHandlePos = textOffsetToPosition(tester, testValue.indexOf('First') + 5);
    gesture = await tester.startGesture(handlePos, pointer: 7);
    await tester.pump();
    await gesture.moveTo(newHandlePos);
    await tester.pump();
    await gesture.up();
965
    await tester.pump();
966

967
    expect(controller.selection.baseOffset, 5);
968
    expect(controller.selection.extentOffset, 50);
969 970

    await tester.tap(find.text('CUT'));
971
    await tester.pump();
972 973
    expect(controller.selection.isCollapsed, true);
    expect(controller.text, cutValue);
974
  });
975

976
  testWidgets('Can scroll multiline input', (WidgetTester tester) async {
977
    final Key textFieldKey = UniqueKey();
978 979 980
    final TextEditingController controller = TextEditingController(
      text: kMoreThanFourLines,
    );
981

982 983
    await tester.pumpWidget(
      overlay(
984
        child: TextField(
985
          dragStartBehavior: DragStartBehavior.down,
986 987 988 989
          key: textFieldKey,
          controller: controller,
          style: const TextStyle(color: Colors.black, fontSize: 34.0),
          maxLines: 2,
990
        ),
991 992
      ),
    );
993

994
    RenderBox findInputBox() => tester.renderObject(find.byKey(textFieldKey));
995
    final RenderBox inputBox = findInputBox();
996 997

    // Check that the last line of text is not displayed.
998 999
    final Offset firstPos = textOffsetToPosition(tester, kMoreThanFourLines.indexOf('First'));
    final Offset fourthPos = textOffsetToPosition(tester, kMoreThanFourLines.indexOf('Fourth'));
1000 1001
    expect(firstPos.dx, 0);
    expect(fourthPos.dx, 0);
1002 1003
    expect(firstPos.dx, fourthPos.dx);
    expect(firstPos.dy, lessThan(fourthPos.dy));
1004 1005
    expect(inputBox.hitTest(HitTestResult(), position: inputBox.globalToLocal(firstPos)), isTrue);
    expect(inputBox.hitTest(HitTestResult(), position: inputBox.globalToLocal(fourthPos)), isFalse);
1006 1007 1008

    TestGesture gesture = await tester.startGesture(firstPos, pointer: 7);
    await tester.pump();
1009
    await gesture.moveBy(const Offset(0.0, -1000.0));
1010
    await tester.pump(const Duration(seconds: 1));
1011 1012
    // Wait and drag again to trigger https://github.com/flutter/flutter/issues/6329
    // (No idea why this is necessary, but the bug wouldn't repro without it.)
1013
    await gesture.moveBy(const Offset(0.0, -1000.0));
1014
    await tester.pump(const Duration(seconds: 1));
1015 1016
    await gesture.up();
    await tester.pump();
1017
    await tester.pump(const Duration(seconds: 1));
1018 1019

    // Now the first line is scrolled up, and the fourth line is visible.
1020 1021
    Offset newFirstPos = textOffsetToPosition(tester, kMoreThanFourLines.indexOf('First'));
    Offset newFourthPos = textOffsetToPosition(tester, kMoreThanFourLines.indexOf('Fourth'));
1022

1023
    expect(newFirstPos.dy, lessThan(firstPos.dy));
1024 1025
    expect(inputBox.hitTest(HitTestResult(), position: inputBox.globalToLocal(newFirstPos)), isFalse);
    expect(inputBox.hitTest(HitTestResult(), position: inputBox.globalToLocal(newFourthPos)), isTrue);
1026 1027 1028

    // Now try scrolling by dragging the selection handle.
    // Long press the 'i' in 'Fourth line' to select the word.
1029 1030 1031 1032 1033 1034
    final Offset selectedWordPos = textOffsetToPosition(
      tester,
      kMoreThanFourLines.indexOf('Fourth line') + 8,
    );

    gesture = await tester.startGesture(selectedWordPos, pointer: 7);
1035
    await tester.pump(const Duration(seconds: 1));
1036
    await gesture.up();
1037
    await tester.pump();
1038
    await tester.pump(const Duration(seconds: 1));
1039

1040 1041 1042
    expect(controller.selection.base.offset, 91);
    expect(controller.selection.extent.offset, 94);

1043
    final RenderEditable renderEditable = findRenderEditable(tester);
1044 1045 1046 1047
    final List<TextSelectionPoint> endpoints = globalize(
      renderEditable.getEndpointsForSelection(controller.selection),
      renderEditable,
    );
1048 1049 1050
    expect(endpoints.length, 2);

    // Drag the left handle to the first line, just after 'First'.
1051
    final Offset handlePos = endpoints[0].point + const Offset(-1, 1);
1052
    final Offset newHandlePos = textOffsetToPosition(tester, kMoreThanFourLines.indexOf('First') + 5);
1053
    gesture = await tester.startGesture(handlePos, pointer: 7);
1054
    await tester.pump(const Duration(seconds: 1));
1055
    await gesture.moveTo(newHandlePos + const Offset(0.0, -10.0));
1056
    await tester.pump(const Duration(seconds: 1));
1057
    await gesture.up();
1058
    await tester.pump(const Duration(seconds: 1));
1059 1060 1061

    // The text should have scrolled up with the handle to keep the active
    // cursor visible, back to its original position.
1062 1063
    newFirstPos = textOffsetToPosition(tester, kMoreThanFourLines.indexOf('First'));
    newFourthPos = textOffsetToPosition(tester, kMoreThanFourLines.indexOf('Fourth'));
1064
    expect(newFirstPos.dy, firstPos.dy);
1065 1066
    expect(inputBox.hitTest(HitTestResult(), position: inputBox.globalToLocal(newFirstPos)), isTrue);
    expect(inputBox.hitTest(HitTestResult(), position: inputBox.globalToLocal(newFourthPos)), isFalse);
1067
  });
1068

xster's avatar
xster committed
1069
  testWidgets('TextField smoke test', (WidgetTester tester) async {
1070
    String textFieldValue;
1071

1072 1073
    await tester.pumpWidget(
      overlay(
1074
        child: TextField(
1075 1076 1077 1078
          decoration: null,
          onChanged: (String value) {
            textFieldValue = value;
          },
1079
        ),
1080 1081
      ),
    );
1082

1083
    Future<void> checkText(String testValue) {
1084
      return TestAsyncUtils.guard(() async {
1085
        await tester.enterText(find.byType(TextField), testValue);
1086

1087 1088
        // Check that the onChanged event handler fired.
        expect(textFieldValue, equals(testValue));
1089

1090
        await tester.pump();
1091
      });
1092 1093
    }

1094
    await checkText('Hello World');
1095 1096
  });

xster's avatar
xster committed
1097
  testWidgets('TextField with global key', (WidgetTester tester) async {
1098
    final GlobalKey textFieldKey = GlobalKey(debugLabel: 'textFieldKey');
1099
    String textFieldValue;
1100

1101 1102
    await tester.pumpWidget(
      overlay(
1103
        child: TextField(
1104 1105 1106
          key: textFieldKey,
          decoration: const InputDecoration(
            hintText: 'Placeholder',
1107
          ),
1108
          onChanged: (String value) { textFieldValue = value; },
1109
        ),
1110 1111
      ),
    );
1112

1113
    Future<void> checkText(String testValue) async {
1114
      return TestAsyncUtils.guard(() async {
1115
        await tester.enterText(find.byType(TextField), testValue);
1116

1117 1118
        // Check that the onChanged event handler fired.
        expect(textFieldValue, equals(testValue));
1119

1120
        await tester.pump();
1121
      });
1122 1123
    }

1124
    await checkText('Hello World');
1125
  });
1126

1127
  testWidgets('TextField errorText trumps helperText', (WidgetTester tester) async {
1128 1129 1130
    await tester.pumpWidget(
      overlay(
        child: const TextField(
1131
          decoration: InputDecoration(
1132 1133
            errorText: 'error text',
            helperText: 'helper text',
1134 1135
          ),
        ),
1136 1137
      ),
    );
1138 1139 1140 1141 1142
    expect(find.text('helper text'), findsNothing);
    expect(find.text('error text'), findsOneWidget);
  });

  testWidgets('TextField with default helperStyle', (WidgetTester tester) async {
1143
    final ThemeData themeData = ThemeData(hintColor: Colors.blue[500]);
1144 1145
    await tester.pumpWidget(
      overlay(
1146
        child: Theme(
1147
          data: themeData,
1148
          child: const TextField(
1149
            decoration: InputDecoration(
1150
              helperText: 'helper text',
1151 1152 1153
            ),
          ),
        ),
1154 1155
      ),
    );
1156 1157
    final Text helperText = tester.widget(find.text('helper text'));
    expect(helperText.style.color, themeData.hintColor);
1158
    expect(helperText.style.fontSize, Typography.englishLike2014.caption.fontSize);
1159 1160 1161
  });

  testWidgets('TextField with specified helperStyle', (WidgetTester tester) async {
1162
    final TextStyle style = TextStyle(
1163
      inherit: false,
1164 1165 1166 1167
      color: Colors.pink[500],
      fontSize: 10.0,
    );

1168 1169
    await tester.pumpWidget(
      overlay(
1170 1171
        child: TextField(
          decoration: InputDecoration(
1172 1173
            helperText: 'helper text',
            helperStyle: style,
1174 1175
          ),
        ),
1176 1177
      ),
    );
1178 1179 1180 1181
    final Text helperText = tester.widget(find.text('helper text'));
    expect(helperText.style, style);
  });

1182
  testWidgets('TextField with default hintStyle', (WidgetTester tester) async {
1183
    final TextStyle style = TextStyle(
1184 1185 1186
      color: Colors.pink[500],
      fontSize: 10.0,
    );
1187
    final ThemeData themeData = ThemeData(
1188 1189 1190
      hintColor: Colors.blue[500],
    );

1191 1192
    await tester.pumpWidget(
      overlay(
1193
        child: Theme(
1194
          data: themeData,
1195
          child: TextField(
1196 1197
            decoration: const InputDecoration(
              hintText: 'Placeholder',
1198
            ),
1199
            style: style,
1200 1201
          ),
        ),
1202 1203
      ),
    );
1204

1205
    final Text hintText = tester.widget(find.text('Placeholder'));
1206
    expect(hintText.style.color, themeData.hintColor);
1207
    expect(hintText.style.fontSize, style.fontSize);
1208 1209
  });

1210
  testWidgets('TextField with specified hintStyle', (WidgetTester tester) async {
1211
    final TextStyle hintStyle = TextStyle(
1212
      inherit: false,
1213 1214 1215 1216
      color: Colors.pink[500],
      fontSize: 10.0,
    );

1217 1218
    await tester.pumpWidget(
      overlay(
1219 1220
        child: TextField(
          decoration: InputDecoration(
1221 1222
            hintText: 'Placeholder',
            hintStyle: hintStyle,
1223 1224
          ),
        ),
1225 1226
      ),
    );
1227

1228
    final Text hintText = tester.widget(find.text('Placeholder'));
1229 1230 1231
    expect(hintText.style, hintStyle);
  });

1232
  testWidgets('TextField with specified prefixStyle', (WidgetTester tester) async {
1233
    final TextStyle prefixStyle = TextStyle(
1234
      inherit: false,
1235 1236 1237 1238
      color: Colors.pink[500],
      fontSize: 10.0,
    );

1239 1240
    await tester.pumpWidget(
      overlay(
1241 1242
        child: TextField(
          decoration: InputDecoration(
1243 1244
            prefixText: 'Prefix:',
            prefixStyle: prefixStyle,
1245 1246
          ),
        ),
1247 1248
      ),
    );
1249 1250 1251 1252 1253 1254

    final Text prefixText = tester.widget(find.text('Prefix:'));
    expect(prefixText.style, prefixStyle);
  });

  testWidgets('TextField with specified suffixStyle', (WidgetTester tester) async {
1255
    final TextStyle suffixStyle = TextStyle(
1256 1257 1258 1259
      color: Colors.pink[500],
      fontSize: 10.0,
    );

1260 1261
    await tester.pumpWidget(
      overlay(
1262 1263
        child: TextField(
          decoration: InputDecoration(
1264 1265
            suffixText: '.com',
            suffixStyle: suffixStyle,
1266 1267
          ),
        ),
1268 1269
      ),
    );
1270 1271 1272 1273 1274 1275 1276

    final Text suffixText = tester.widget(find.text('.com'));
    expect(suffixText.style, suffixStyle);
  });

  testWidgets('TextField prefix and suffix appear correctly with no hint or label',
          (WidgetTester tester) async {
1277
    final Key secondKey = UniqueKey();
1278

1279 1280
    await tester.pumpWidget(
      overlay(
1281
        child: Column(
1282 1283
          children: <Widget>[
            const TextField(
1284
              decoration: InputDecoration(
1285
                labelText: 'First',
1286
              ),
1287
            ),
1288
            TextField(
1289 1290 1291 1292
              key: secondKey,
              decoration: const InputDecoration(
                prefixText: 'Prefix',
                suffixText: 'Suffix',
1293
              ),
1294 1295
            ),
          ],
1296
        ),
1297 1298
      ),
    );
1299 1300 1301 1302 1303 1304

    expect(find.text('Prefix'), findsOneWidget);
    expect(find.text('Suffix'), findsOneWidget);

    // Focus the Input. The prefix should still display.
    await tester.tap(find.byKey(secondKey));
1305
    await tester.pump();
1306 1307 1308 1309 1310

    expect(find.text('Prefix'), findsOneWidget);
    expect(find.text('Suffix'), findsOneWidget);

    // Enter some text, and the prefix should still display.
Ian Hickson's avatar
Ian Hickson committed
1311
    await tester.enterText(find.byKey(secondKey), 'Hi');
1312 1313 1314 1315 1316 1317 1318 1319 1320
    await tester.pump();
    await tester.pump(const Duration(seconds: 1));

    expect(find.text('Prefix'), findsOneWidget);
    expect(find.text('Suffix'), findsOneWidget);
  });

  testWidgets('TextField prefix and suffix appear correctly with hint text',
          (WidgetTester tester) async {
1321
    final TextStyle hintStyle = TextStyle(
1322
      inherit: false,
1323 1324 1325
      color: Colors.pink[500],
      fontSize: 10.0,
    );
1326
    final Key secondKey = UniqueKey();
1327

1328 1329
    await tester.pumpWidget(
      overlay(
1330
        child: Column(
1331 1332
          children: <Widget>[
            const TextField(
1333
              decoration: InputDecoration(
1334
                labelText: 'First',
1335
              ),
1336
            ),
1337
            TextField(
1338
              key: secondKey,
1339
              decoration: InputDecoration(
1340 1341 1342 1343
                hintText: 'Hint',
                hintStyle: hintStyle,
                prefixText: 'Prefix',
                suffixText: 'Suffix',
1344
              ),
1345 1346
            ),
          ],
1347
        ),
1348 1349
      ),
    );
1350 1351

    // Neither the prefix or the suffix should initially be visible, only the hint.
1352 1353 1354
    expect(getOpacity(tester, find.text('Prefix')), 0.0);
    expect(getOpacity(tester, find.text('Suffix')), 0.0);
    expect(getOpacity(tester, find.text('Hint')), 1.0);
1355 1356

    await tester.tap(find.byKey(secondKey));
1357
    await tester.pumpAndSettle();
1358

1359 1360 1361 1362
    // Focus the Input. The hint, prefix, and suffix should appear
    expect(getOpacity(tester, find.text('Prefix')), 1.0);
    expect(getOpacity(tester, find.text('Suffix')), 1.0);
    expect(getOpacity(tester, find.text('Hint')), 1.0);
1363 1364

    // Enter some text, and the hint should disappear and the prefix and suffix
1365
    // should continue to be visible
Ian Hickson's avatar
Ian Hickson committed
1366
    await tester.enterText(find.byKey(secondKey), 'Hi');
1367
    await tester.pumpAndSettle();
1368

1369 1370 1371
    expect(getOpacity(tester, find.text('Prefix')), 1.0);
    expect(getOpacity(tester, find.text('Suffix')), 1.0);
    expect(getOpacity(tester, find.text('Hint')), 0.0);
1372 1373 1374 1375 1376 1377 1378 1379 1380 1381

    // Check and make sure that the right styles were applied.
    final Text prefixText = tester.widget(find.text('Prefix'));
    expect(prefixText.style, hintStyle);
    final Text suffixText = tester.widget(find.text('Suffix'));
    expect(suffixText.style, hintStyle);
  });

  testWidgets('TextField prefix and suffix appear correctly with label text',
          (WidgetTester tester) async {
1382
    final TextStyle prefixStyle = TextStyle(
1383 1384 1385
      color: Colors.pink[500],
      fontSize: 10.0,
    );
1386
    final TextStyle suffixStyle = TextStyle(
1387 1388 1389
      color: Colors.green[500],
      fontSize: 12.0,
    );
1390
    final Key secondKey = UniqueKey();
1391

1392 1393
    await tester.pumpWidget(
      overlay(
1394
        child: Column(
1395 1396
          children: <Widget>[
            const TextField(
1397
              decoration: InputDecoration(
1398
                labelText: 'First',
1399
              ),
1400
            ),
1401
            TextField(
1402
              key: secondKey,
1403
              decoration: InputDecoration(
1404 1405 1406 1407 1408
                labelText: 'Label',
                prefixText: 'Prefix',
                prefixStyle: prefixStyle,
                suffixText: 'Suffix',
                suffixStyle: suffixStyle,
1409
              ),
1410 1411
            ),
          ],
1412
        ),
1413 1414
      ),
    );
1415

1416
    // Not focused. The prefix and suffix should not appear, but the label should.
1417 1418
    expect(getOpacity(tester, find.text('Prefix')), 0.0);
    expect(getOpacity(tester, find.text('Suffix')), 0.0);
1419 1420
    expect(find.text('Label'), findsOneWidget);

1421
    // Focus the input. The label, prefix, and suffix should appear.
1422
    await tester.tap(find.byKey(secondKey));
1423
    await tester.pumpAndSettle();
1424

1425 1426
    expect(getOpacity(tester, find.text('Prefix')), 1.0);
    expect(getOpacity(tester, find.text('Suffix')), 1.0);
1427 1428
    expect(find.text('Label'), findsOneWidget);

1429
    // Enter some text. The label, prefix, and suffix should remain visible.
Ian Hickson's avatar
Ian Hickson committed
1430
    await tester.enterText(find.byKey(secondKey), 'Hi');
1431
    await tester.pumpAndSettle();
1432

1433 1434
    expect(getOpacity(tester, find.text('Prefix')), 1.0);
    expect(getOpacity(tester, find.text('Suffix')), 1.0);
1435 1436 1437 1438 1439 1440 1441 1442 1443
    expect(find.text('Label'), findsOneWidget);

    // Check and make sure that the right styles were applied.
    final Text prefixText = tester.widget(find.text('Prefix'));
    expect(prefixText.style, prefixStyle);
    final Text suffixText = tester.widget(find.text('Suffix'));
    expect(suffixText.style, suffixStyle);
  });

1444
  testWidgets('TextField label text animates', (WidgetTester tester) async {
1445
    final Key secondKey = UniqueKey();
1446

1447 1448
    await tester.pumpWidget(
      overlay(
1449
        child: Column(
1450 1451
          children: <Widget>[
            const TextField(
1452
              decoration: InputDecoration(
1453
                labelText: 'First',
1454
              ),
1455
            ),
1456
            TextField(
1457 1458 1459
              key: secondKey,
              decoration: const InputDecoration(
                labelText: 'Second',
1460
              ),
1461 1462
            ),
          ],
1463
        ),
1464 1465
      ),
    );
1466

1467
    Offset pos = tester.getTopLeft(find.text('Second'));
1468 1469

    // Focus the Input. The label should start animating upwards.
1470
    await tester.tap(find.byKey(secondKey));
1471
    await tester.idle();
1472
    await tester.pump();
1473 1474
    await tester.pump(const Duration(milliseconds: 50));

1475 1476
    Offset newPos = tester.getTopLeft(find.text('Second'));
    expect(newPos.dy, lessThan(pos.dy));
1477 1478 1479 1480 1481

    // Label should still be sliding upward.
    await tester.pump(const Duration(milliseconds: 50));
    pos = newPos;
    newPos = tester.getTopLeft(find.text('Second'));
1482
    expect(newPos.dy, lessThan(pos.dy));
1483
  });
1484

1485
  testWidgets('Icon is separated from input/label by 16+12', (WidgetTester tester) async {
1486
    await tester.pumpWidget(
1487 1488
      overlay(
        child: const TextField(
1489 1490
          decoration: InputDecoration(
            icon: Icon(Icons.phone),
1491
            labelText: 'label',
1492
            filled: true,
1493 1494 1495 1496
          ),
        ),
      ),
    );
1497
    final double iconRight = tester.getTopRight(find.byType(Icon)).dx;
1498
    // Per https://material.io/go/design-text-fields#text-fields-layout
1499 1500 1501 1502 1503
    // There's a 16 dps gap between the right edge of the icon and the text field's
    // container, and the 12dps more padding between the left edge of the container
    // and the left edge of the input and label.
    expect(iconRight + 28.0, equals(tester.getTopLeft(find.text('label')).dx));
    expect(iconRight + 28.0, equals(tester.getTopLeft(find.byType(EditableText)).dx));
1504
  });
1505 1506 1507

  testWidgets('Collapsed hint text placement', (WidgetTester tester) async {
    await tester.pumpWidget(
1508 1509
      overlay(
        child: const TextField(
1510
          decoration: InputDecoration.collapsed(
1511
            hintText: 'hint',
1512 1513
          ),
        ),
1514
      ),
1515 1516 1517 1518 1519 1520 1521
    );

    expect(tester.getTopLeft(find.text('hint')), equals(tester.getTopLeft(find.byType(TextField))));
  });

  testWidgets('Can align to center', (WidgetTester tester) async {
    await tester.pumpWidget(
1522
      overlay(
1523
        child: Container(
1524 1525 1526 1527
          width: 300.0,
          child: const TextField(
            textAlign: TextAlign.center,
            decoration: null,
1528 1529
          ),
        ),
1530
      ),
1531 1532 1533
    );

    final RenderEditable editable = findRenderEditable(tester);
1534 1535
    Offset topLeft = editable.localToGlobal(
      editable.getLocalRectForCaret(const TextPosition(offset: 0)).topLeft,
1536 1537
    );

1538 1539 1540 1541
    // The overlay() function centers its child within a 800x600 window.
    // Default cursorWidth is 2.0, test windowWidth is 800
    // Centered cursor topLeft.dx: 399 == windowWidth/2 - cursorWidth/2
    expect(topLeft.dx, equals(399.0));
1542

1543
    await tester.enterText(find.byType(TextField), 'abcd');
1544 1545 1546
    await tester.pump();

    topLeft = editable.localToGlobal(
1547
      editable.getLocalRectForCaret(const TextPosition(offset: 2)).topLeft,
1548 1549
    );

1550 1551
    // TextPosition(offset: 2) - center of 'abcd'
    expect(topLeft.dx, equals(399.0));
1552
  });
1553 1554 1555

  testWidgets('Can align to center within center', (WidgetTester tester) async {
    await tester.pumpWidget(
1556
      overlay(
1557
        child: Container(
1558 1559
          width: 300.0,
          child: const Center(
1560
            child: TextField(
1561 1562
              textAlign: TextAlign.center,
              decoration: null,
1563 1564 1565
            ),
          ),
        ),
1566
      ),
1567 1568 1569
    );

    final RenderEditable editable = findRenderEditable(tester);
1570 1571
    Offset topLeft = editable.localToGlobal(
      editable.getLocalRectForCaret(const TextPosition(offset: 0)).topLeft,
1572 1573
    );

1574 1575 1576 1577
    // The overlay() function centers its child within a 800x600 window.
    // Default cursorWidth is 2.0, test windowWidth is 800
    // Centered cursor topLeft.dx: 399 == windowWidth/2 - cursorWidth/2
    expect(topLeft.dx, equals(399.0));
1578

1579
    await tester.enterText(find.byType(TextField), 'abcd');
1580 1581 1582
    await tester.pump();

    topLeft = editable.localToGlobal(
1583
      editable.getLocalRectForCaret(const TextPosition(offset: 2)).topLeft,
1584 1585
    );

1586 1587
    // TextPosition(offset: 2) - center of 'abcd'
    expect(topLeft.dx, equals(399.0));
1588
  });
1589 1590

  testWidgets('Controller can update server', (WidgetTester tester) async {
1591
    final TextEditingController controller1 = TextEditingController(
1592 1593
      text: 'Initial Text',
    );
1594
    final TextEditingController controller2 = TextEditingController(
1595 1596 1597
      text: 'More Text',
    );

1598
    TextEditingController currentController;
1599 1600 1601
    StateSetter setState;

    await tester.pumpWidget(
1602
      overlay(
1603
        child: StatefulBuilder(
1604 1605
          builder: (BuildContext context, StateSetter setter) {
            setState = setter;
1606
            return TextField(controller: currentController);
1607
          }
1608 1609
        ),
      ),
1610
    );
1611 1612
    expect(tester.testTextInput.editingState['text'], isEmpty);

1613
    // Initial state with null controller.
1614
    await tester.tap(find.byType(TextField));
1615
    await tester.pump();
1616 1617 1618 1619 1620 1621 1622
    expect(tester.testTextInput.editingState['text'], isEmpty);

    // Update the controller from null to controller1.
    setState(() {
      currentController = controller1;
    });
    await tester.pump();
1623 1624
    expect(tester.testTextInput.editingState['text'], equals('Initial Text'));

1625 1626
    // Verify that updates to controller1 are handled.
    controller1.text = 'Updated Text';
1627 1628 1629
    await tester.idle();
    expect(tester.testTextInput.editingState['text'], equals('Updated Text'));

1630
    // Verify that switching from controller1 to controller2 is handled.
1631 1632 1633 1634 1635 1636
    setState(() {
      currentController = controller2;
    });
    await tester.pump();
    expect(tester.testTextInput.editingState['text'], equals('More Text'));

1637 1638
    // Verify that updates to controller1 are ignored.
    controller1.text = 'Ignored Text';
1639 1640 1641
    await tester.idle();
    expect(tester.testTextInput.editingState['text'], equals('More Text'));

1642
    // Verify that updates to controller text are handled.
1643
    controller2.text = 'Additional Text';
1644
    await tester.idle();
1645 1646
    expect(tester.testTextInput.editingState['text'], equals('Additional Text'));

1647
    // Verify that updates to controller selection are handled.
1648 1649 1650 1651 1652
    controller2.selection = const TextSelection(baseOffset: 0, extentOffset: 5);
    await tester.idle();
    expect(tester.testTextInput.editingState['selectionBase'], equals(0));
    expect(tester.testTextInput.editingState['selectionExtent'], equals(5));

1653
    // Verify that calling clear() clears the text.
1654 1655 1656
    controller2.clear();
    await tester.idle();
    expect(tester.testTextInput.editingState['text'], equals(''));
1657 1658 1659 1660 1661 1662 1663 1664 1665 1666 1667 1668 1669 1670

    // Verify that switching from controller2 to null preserves current text.
    controller2.text = 'The Final Cut';
    await tester.idle();
    expect(tester.testTextInput.editingState['text'], equals('The Final Cut'));
    setState(() {
      currentController = null;
    });
    await tester.pump();
    expect(tester.testTextInput.editingState['text'], equals('The Final Cut'));

    // Verify that changes to controller2 are ignored.
    controller2.text = 'Goodbye Cruel World';
    expect(tester.testTextInput.editingState['text'], equals('The Final Cut'));
1671
  });
1672

1673
  testWidgets('Cannot enter new lines onto single line TextField', (WidgetTester tester) async {
1674
    final TextEditingController textController = TextEditingController();
1675

Ian Hickson's avatar
Ian Hickson committed
1676
    await tester.pumpWidget(boilerplate(
1677
      child: TextField(controller: textController, decoration: null),
1678
    ));
1679

1680
    await tester.enterText(find.byType(TextField), 'abc\ndef');
1681

1682 1683
    expect(textController.text, 'abcdef');
  });
1684

1685
  testWidgets('Injected formatters are chained', (WidgetTester tester) async {
1686
    final TextEditingController textController = TextEditingController();
1687

Ian Hickson's avatar
Ian Hickson committed
1688
    await tester.pumpWidget(boilerplate(
1689
      child: TextField(
1690 1691 1692
        controller: textController,
        decoration: null,
        inputFormatters: <TextInputFormatter> [
1693 1694
          BlacklistingTextInputFormatter(
            RegExp(r'[a-z]'),
1695 1696 1697 1698 1699
            replacementString: '#',
          ),
        ],
      ),
    ));
1700

1701 1702 1703 1704
    await tester.enterText(find.byType(TextField), 'a一b二c三\nd四e五f六');
    // The default single line formatter replaces \n with empty string.
    expect(textController.text, '#一#二#三#四#五#六');
  });
1705

1706
  testWidgets('Chained formatters are in sequence', (WidgetTester tester) async {
1707
    final TextEditingController textController = TextEditingController();
1708

Ian Hickson's avatar
Ian Hickson committed
1709
    await tester.pumpWidget(boilerplate(
1710
      child: TextField(
1711 1712 1713 1714
        controller: textController,
        decoration: null,
        maxLines: 2,
        inputFormatters: <TextInputFormatter> [
1715 1716
          BlacklistingTextInputFormatter(
            RegExp(r'[a-z]'),
1717 1718
            replacementString: '12\n',
          ),
1719
          WhitelistingTextInputFormatter(RegExp(r'\n[0-9]')),
1720 1721 1722 1723 1724 1725 1726 1727 1728 1729 1730 1731
        ],
      ),
    ));

    await tester.enterText(find.byType(TextField), 'a1b2c3');
    // The first formatter turns it into
    // 12\n112\n212\n3
    // The second formatter turns it into
    // \n1\n2\n3
    // Multiline is allowed since maxLine != 1.
    expect(textController.text, '\n1\n2\n3');
  });
1732

1733
  testWidgets('Pasted values are formatted', (WidgetTester tester) async {
1734
    final TextEditingController textController = TextEditingController();
1735

1736 1737
    await tester.pumpWidget(
      overlay(
1738
        child: TextField(
1739
          controller: textController,
1740 1741
          decoration: null,
          inputFormatters: <TextInputFormatter> [
1742
            WhitelistingTextInputFormatter.digitsOnly,
1743 1744
          ],
        ),
1745 1746
      ),
    );
1747

1748 1749 1750
    await tester.enterText(find.byType(TextField), 'a1b\n2c3');
    expect(textController.text, '123');
    await skipPastScrollingAnimation(tester);
1751

1752
    await tester.tapAt(textOffsetToPosition(tester, '123'.indexOf('2')));
1753
    await tester.pump();
1754 1755 1756 1757 1758 1759 1760
    await tester.pump(const Duration(milliseconds: 200)); // skip past the frame where the opacity is zero
    final RenderEditable renderEditable = findRenderEditable(tester);
    final List<TextSelectionPoint> endpoints = globalize(
      renderEditable.getEndpointsForSelection(textController.selection),
      renderEditable,
    );
    await tester.tapAt(endpoints[0].point + const Offset(1.0, 1.0));
1761
    await tester.pump();
1762
    await tester.pump(const Duration(milliseconds: 200)); // skip past the frame where the opacity is zero
1763

1764 1765 1766 1767 1768 1769
    Clipboard.setData(const ClipboardData(text: '一4二\n5三6'));
    await tester.tap(find.text('PASTE'));
    await tester.pump();
    // Puts 456 before the 2 in 123.
    expect(textController.text, '145623');
  });
1770

1771
  testWidgets('Text field scrolls the caret into view', (WidgetTester tester) async {
1772
    final TextEditingController controller = TextEditingController();
1773

1774 1775
    await tester.pumpWidget(
      overlay(
1776
        child: Container(
1777
          width: 100.0,
1778
          child: TextField(
1779
            controller: controller,
1780
          ),
1781 1782 1783
        ),
      ),
    );
1784

1785 1786 1787
    final String longText = 'a' * 20;
    await tester.enterText(find.byType(TextField), longText);
    await skipPastScrollingAnimation(tester);
1788

1789 1790
    ScrollableState scrollableState = tester.firstState(find.byType(Scrollable));
    expect(scrollableState.position.pixels, equals(0.0));
1791

1792 1793
    // Move the caret to the end of the text and check that the text field
    // scrolls to make the caret visible.
1794
    controller.selection = TextSelection.collapsed(offset: longText.length);
1795 1796
    await tester.pump(); // TODO(ianh): Figure out why this extra pump is needed.
    await skipPastScrollingAnimation(tester);
1797

1798
    scrollableState = tester.firstState(find.byType(Scrollable));
1799
    // For a horizontal input, scrolls to the exact position of the caret.
1800
    expect(scrollableState.position.pixels, equals(222.0));
1801 1802 1803 1804 1805 1806 1807 1808 1809 1810 1811 1812 1813 1814 1815 1816 1817 1818 1819 1820 1821 1822 1823 1824 1825 1826 1827 1828 1829 1830 1831 1832 1833
  });

  testWidgets('Multiline text field scrolls the caret into view', (WidgetTester tester) async {
    final TextEditingController controller = TextEditingController();

    await tester.pumpWidget(
      overlay(
        child: Container(
          child: TextField(
            controller: controller,
            maxLines: 6,
          ),
        ),
      ),
    );

    const String tallText = 'a\nb\nc\nd\ne\nf\ng'; // One line over max
    await tester.enterText(find.byType(TextField), tallText);
    await skipPastScrollingAnimation(tester);

    ScrollableState scrollableState = tester.firstState(find.byType(Scrollable));
    expect(scrollableState.position.pixels, equals(0.0));

    // Move the caret to the end of the text and check that the text field
    // scrolls to make the caret visible.
    controller.selection = const TextSelection.collapsed(offset: tallText.length);
    await tester.pump();
    await skipPastScrollingAnimation(tester);

    // Should have scrolled down exactly one line height (7 lines of text in 6
    // line text field).
    final double lineHeight = findRenderEditable(tester).preferredLineHeight;
    scrollableState = tester.firstState(find.byType(Scrollable));
1834
    expect(scrollableState.position.pixels, closeTo(lineHeight, 0.1));
1835
  });
1836

1837
  testWidgets('haptic feedback', (WidgetTester tester) async {
1838 1839
    final FeedbackTester feedback = FeedbackTester();
    final TextEditingController controller = TextEditingController();
1840

1841 1842
    await tester.pumpWidget(
      overlay(
1843
        child: Container(
1844
          width: 100.0,
1845
          child: TextField(
1846
            controller: controller,
1847 1848
          ),
        ),
1849 1850
      ),
    );
1851 1852 1853 1854 1855 1856 1857 1858 1859 1860 1861 1862 1863 1864

    await tester.tap(find.byType(TextField));
    await tester.pumpAndSettle(const Duration(seconds: 1));
    expect(feedback.clickSoundCount, 0);
    expect(feedback.hapticCount, 0);

    await tester.longPress(find.byType(TextField));
    await tester.pumpAndSettle(const Duration(seconds: 1));
    expect(feedback.clickSoundCount, 0);
    expect(feedback.hapticCount, 1);

    feedback.dispose();
  });

1865
  testWidgets('Text field drops selection when losing focus', (WidgetTester tester) async {
1866 1867 1868
    final Key key1 = UniqueKey();
    final TextEditingController controller1 = TextEditingController();
    final Key key2 = UniqueKey();
1869 1870 1871

    await tester.pumpWidget(
      overlay(
1872
        child: Column(
1873
          children: <Widget>[
1874
            TextField(
1875 1876
              key: key1,
              controller: controller1
1877
            ),
1878
            TextField(key: key2),
1879 1880 1881 1882
          ],
        ),
      ),
    );
1883

1884 1885 1886 1887 1888 1889 1890 1891
    await tester.tap(find.byKey(key1));
    await tester.enterText(find.byKey(key1), 'abcd');
    await tester.pump();
    controller1.selection = const TextSelection(baseOffset: 0, extentOffset: 3);
    await tester.pump();
    expect(controller1.selection, isNot(equals(TextRange.empty)));

    await tester.tap(find.byKey(key2));
1892
    await tester.pump();
1893 1894
    expect(controller1.selection, equals(TextRange.empty));
  });
1895

1896
  testWidgets('Selection is consistent with text length', (WidgetTester tester) async {
1897
    final TextEditingController controller = TextEditingController();
1898

1899 1900
    controller.text = 'abcde';
    controller.selection = const TextSelection.collapsed(offset: 5);
1901

1902 1903 1904
    controller.text = '';
    expect(controller.selection.start, lessThanOrEqualTo(0));
    expect(controller.selection.end, lessThanOrEqualTo(0));
1905

1906 1907 1908 1909
    expect(() {
      controller.selection = const TextSelection.collapsed(offset: 10);
    }, throwsFlutterError);
  });
1910 1911

  testWidgets('maxLength limits input.', (WidgetTester tester) async {
1912
    final TextEditingController textController = TextEditingController();
1913 1914

    await tester.pumpWidget(boilerplate(
1915
      child: TextField(
1916 1917 1918 1919 1920 1921 1922 1923 1924 1925
        controller: textController,
        maxLength: 10,
      ),
    ));

    await tester.enterText(find.byType(TextField), '0123456789101112');
    expect(textController.text, '0123456789');
  });

  testWidgets('maxLength limits input length even if decoration is null.', (WidgetTester tester) async {
1926
    final TextEditingController textController = TextEditingController();
1927 1928

    await tester.pumpWidget(boilerplate(
1929
      child: TextField(
1930 1931 1932 1933 1934 1935 1936 1937 1938 1939 1940
        controller: textController,
        decoration: null,
        maxLength: 10,
      ),
    ));

    await tester.enterText(find.byType(TextField), '0123456789101112');
    expect(textController.text, '0123456789');
  });

  testWidgets('maxLength still works with other formatters.', (WidgetTester tester) async {
1941
    final TextEditingController textController = TextEditingController();
1942 1943

    await tester.pumpWidget(boilerplate(
1944
      child: TextField(
1945 1946 1947
        controller: textController,
        maxLength: 10,
        inputFormatters: <TextInputFormatter> [
1948 1949
          BlacklistingTextInputFormatter(
            RegExp(r'[a-z]'),
1950 1951 1952 1953 1954 1955 1956 1957 1958 1959 1960 1961
            replacementString: '#',
          ),
        ],
      ),
    ));

    await tester.enterText(find.byType(TextField), 'a一b二c三\nd四e五f六');
    // The default single line formatter replaces \n with empty string.
    expect(textController.text, '#一#二#三#四#五');
  });

  testWidgets("maxLength isn't enforced when maxLengthEnforced is false.", (WidgetTester tester) async {
1962
    final TextEditingController textController = TextEditingController();
1963 1964

    await tester.pumpWidget(boilerplate(
1965
      child: TextField(
1966 1967 1968 1969 1970 1971 1972 1973 1974 1975 1976
        controller: textController,
        maxLength: 10,
        maxLengthEnforced: false,
      ),
    ));

    await tester.enterText(find.byType(TextField), '0123456789101112');
    expect(textController.text, '0123456789101112');
  });

  testWidgets('maxLength shows warning when maxLengthEnforced is false.', (WidgetTester tester) async {
1977
    final TextEditingController textController = TextEditingController();
1978
    const TextStyle testStyle = TextStyle(color: Colors.deepPurpleAccent);
1979 1980

    await tester.pumpWidget(boilerplate(
1981
      child: TextField(
1982
        decoration: const InputDecoration(errorStyle: testStyle),
1983 1984 1985 1986 1987 1988 1989 1990 1991 1992
        controller: textController,
        maxLength: 10,
        maxLengthEnforced: false,
      ),
    ));

    await tester.enterText(find.byType(TextField), '0123456789101112');
    await tester.pump();

    expect(textController.text, '0123456789101112');
1993 1994
    expect(find.text('16/10'), findsOneWidget);
    Text counterTextWidget = tester.widget(find.text('16/10'));
1995 1996 1997 1998 1999 2000
    expect(counterTextWidget.style.color, equals(Colors.deepPurpleAccent));

    await tester.enterText(find.byType(TextField), '0123456789');
    await tester.pump();

    expect(textController.text, '0123456789');
2001 2002
    expect(find.text('10/10'), findsOneWidget);
    counterTextWidget = tester.widget(find.text('10/10'));
2003 2004 2005 2006
    expect(counterTextWidget.style.color, isNot(equals(Colors.deepPurpleAccent)));
  });

  testWidgets('setting maxLength shows counter', (WidgetTester tester) async {
2007 2008
    await tester.pumpWidget(const MaterialApp(
      home: Material(
2009
        child: Center(
2010
            child: TextField(
2011 2012 2013 2014 2015
              maxLength: 10,
            ),
          ),
        ),
      ),
2016
    );
2017

2018
    expect(find.text('0/10'), findsOneWidget);
2019 2020 2021 2022

    await tester.enterText(find.byType(TextField), '01234');
    await tester.pump();

2023
    expect(find.text('5/10'), findsOneWidget);
2024
  });
2025

2026 2027 2028 2029 2030 2031 2032 2033 2034 2035 2036 2037 2038 2039 2040 2041 2042 2043 2044 2045 2046 2047
  testWidgets(
      'setting maxLength to TextField.noMaxLength shows only entered length',
      (WidgetTester tester) async {
    await tester.pumpWidget(const MaterialApp(
      home: Material(
        child: Center(
            child: TextField(
              maxLength: TextField.noMaxLength,
            ),
          ),
        ),
      ),
    );

    expect(find.text('0'), findsOneWidget);

    await tester.enterText(find.byType(TextField), '01234');
    await tester.pump();

    expect(find.text('5'), findsOneWidget);
  });

2048 2049 2050 2051 2052 2053 2054 2055 2056 2057 2058 2059 2060 2061 2062 2063 2064 2065 2066 2067 2068 2069 2070
  testWidgets('passing a buildCounter shows returned widget', (WidgetTester tester) async {
    await tester.pumpWidget(MaterialApp(
      home: Material(
        child: Center(
            child: TextField(
              buildCounter: (BuildContext context, {int currentLength, int maxLength, bool isFocused}) {
                return Text('${currentLength.toString()} of ${maxLength.toString()}');
              },
              maxLength: 10,
            ),
          ),
        ),
      ),
    );

    expect(find.text('0 of 10'), findsOneWidget);

    await tester.enterText(find.byType(TextField), '01234');
    await tester.pump();

    expect(find.text('5 of 10'), findsOneWidget);
  });

2071
  testWidgets('TextField identifies as text field in semantics', (WidgetTester tester) async {
2072
    final SemanticsTester semantics = SemanticsTester(tester);
2073 2074

    await tester.pumpWidget(
2075 2076
      const MaterialApp(
        home: Material(
2077
          child: Center(
2078
              child: TextField(
2079 2080 2081 2082 2083 2084 2085
                maxLength: 10,
              ),
            ),
          ),
        ),
    );

2086
    expect(semantics, includesNodeWith(flags: <SemanticsFlag>[SemanticsFlag.isTextField]));
2087 2088

    semantics.dispose();
2089
  });
2090

2091 2092 2093 2094 2095 2096 2097 2098 2099 2100 2101 2102 2103 2104 2105 2106 2107 2108 2109 2110 2111 2112 2113 2114 2115 2116 2117 2118
  void sendFakeKeyEvent(Map<String, dynamic> data) {
    BinaryMessages.handlePlatformMessage(
      SystemChannels.keyEvent.name,
      SystemChannels.keyEvent.codec.encodeMessage(data),
          (ByteData data) { },
    );
  }

  void sendKeyEventWithCode(int code, bool down, bool shiftDown, bool ctrlDown) {

    int metaState = shiftDown ? 1 : 0;
    if (ctrlDown)
      metaState |= 1 << 12;

    sendFakeKeyEvent(<String, dynamic>{
      'type': down ? 'keydown' : 'keyup',
      'keymap': 'android',
      'keyCode' : code,
      'hidUsage': 0x04,
      'codePoint': 0x64,
      'metaState': metaState,
    });
  }

  group('Keyboard Tests', (){
    TextEditingController controller;

    setUp( () {
2119
      controller = TextEditingController();
2120 2121 2122 2123
    });

    MaterialApp setupWidget() {

2124 2125
      final FocusNode focusNode = FocusNode();
      controller = TextEditingController();
2126

2127
      return MaterialApp(
2128
        home:  Material(
2129
          child: RawKeyboardListener(
2130 2131 2132 2133 2134 2135 2136 2137 2138 2139 2140 2141 2142 2143 2144 2145 2146 2147 2148
            focusNode: focusNode,
            onKey: null,
            child: TextField(
              controller: controller,
              maxLines: 3,
            ),
          ) ,
        ),
      );
    }

    testWidgets('Shift test 1', (WidgetTester tester) async{

      await tester.pumpWidget(setupWidget());
      const String testValue = 'a big house';
      await tester.enterText(find.byType(TextField), testValue);

      await tester.idle();
      await tester.tap(find.byType(TextField));
2149
      await tester.pumpAndSettle();
2150 2151 2152 2153 2154 2155 2156 2157 2158 2159 2160 2161

      sendKeyEventWithCode(22, true, true, false);     // RIGHT_ARROW keydown, SHIFT_ON
      expect(controller.selection.extentOffset - controller.selection.baseOffset, 1);
    });

    testWidgets('Control Shift test', (WidgetTester tester) async{
      await tester.pumpWidget(setupWidget());
      const String testValue = 'their big house';
      await tester.enterText(find.byType(TextField), testValue);

      await tester.idle();
      await tester.tap(find.byType(TextField));
2162
      await tester.pumpAndSettle();
2163 2164 2165 2166 2167 2168 2169 2170 2171 2172 2173 2174 2175 2176 2177
      await tester.pumpAndSettle();
      sendKeyEventWithCode(22, true, true, true);         // RIGHT_ARROW keydown SHIFT_ON, CONTROL_ON

      await tester.pumpAndSettle();

      expect(controller.selection.extentOffset - controller.selection.baseOffset, 5);
    });

    testWidgets('Down and up test', (WidgetTester tester) async{
      await tester.pumpWidget(setupWidget());
      const String testValue = 'a big house';
      await tester.enterText(find.byType(TextField), testValue);

      await tester.idle();
      await tester.tap(find.byType(TextField));
2178
      await tester.pumpAndSettle();
2179 2180 2181 2182 2183 2184 2185 2186 2187 2188 2189 2190 2191 2192 2193 2194 2195 2196 2197 2198 2199 2200

      sendKeyEventWithCode(20, true, true, false);         // DOWN_ARROW keydown
      await tester.pumpAndSettle();

      expect(controller.selection.extentOffset - controller.selection.baseOffset, 11);

      sendKeyEventWithCode(20, false, true, false);          // DOWN_ARROW keyup
      await tester.pumpAndSettle();
      sendKeyEventWithCode(19, true, true, false);           // UP_ARROW keydown
      await tester.pumpAndSettle();

      expect(controller.selection.extentOffset - controller.selection.baseOffset, 0);
    });


    testWidgets('Down and up test 2', (WidgetTester tester) async{
      await tester.pumpWidget(setupWidget());
      const String testValue = 'a big house\njumped over a mouse\nOne more line yay'; // 11 \n 19
      await tester.enterText(find.byType(TextField), testValue);

      await tester.idle();
      await tester.tap(find.byType(TextField));
2201
      await tester.pumpAndSettle();
2202 2203 2204 2205 2206 2207 2208 2209 2210 2211 2212 2213 2214 2215 2216 2217 2218 2219 2220 2221 2222 2223 2224 2225 2226 2227 2228 2229 2230 2231 2232 2233 2234 2235 2236 2237 2238 2239 2240 2241 2242 2243 2244 2245

      for (int i = 0; i < 5; i += 1) {
        sendKeyEventWithCode(22, true, false, false);             // RIGHT_ARROW keydown
        await tester.pumpAndSettle();
        sendKeyEventWithCode(22, false, false, false);            // RIGHT_ARROW keyup
        await tester.pumpAndSettle();
      }
      sendKeyEventWithCode(20, true, true, false);               // DOWN_ARROW keydown
      await tester.pumpAndSettle();
      sendKeyEventWithCode(20, false, true, false);              // DOWN_ARROW keyup
      await tester.pumpAndSettle();

      expect(controller.selection.extentOffset - controller.selection.baseOffset, 12);

      sendKeyEventWithCode(20, true, true, false);                 // DOWN_ARROW keydown
      await tester.pumpAndSettle();
      sendKeyEventWithCode(20, false, true, false);                // DOWN_ARROW keyup
      await tester.pumpAndSettle();

      expect(controller.selection.extentOffset - controller.selection.baseOffset, 32);

      sendKeyEventWithCode(19, true, true, false);               // UP_ARROW keydown
      await tester.pumpAndSettle();
      sendKeyEventWithCode(19, false, true, false);              // UP_ARROW keyup
      await tester.pumpAndSettle();

      expect(controller.selection.extentOffset - controller.selection.baseOffset, 12);

      sendKeyEventWithCode(19, true, true, false);               // UP_ARROW keydown
      await tester.pumpAndSettle();
      sendKeyEventWithCode(19, false, true, false);              // UP_ARROW keyup
      await tester.pumpAndSettle();

      expect(controller.selection.extentOffset - controller.selection.baseOffset, 0);

      sendKeyEventWithCode(19, true, true, false);               // UP_ARROW keydown
      await tester.pumpAndSettle();
      sendKeyEventWithCode(19, false, true, false);              // UP_ARROW keyup
      await tester.pumpAndSettle();

      expect(controller.selection.extentOffset - controller.selection.baseOffset, 5);
    });
  });

jslavitz's avatar
jslavitz committed
2246 2247 2248 2249 2250 2251 2252
  const int _kXKeyCode = 52;
  const int _kCKeyCode = 31;
  const int _kVKeyCode = 50;
  const int _kAKeyCode = 29;
  const int _kDelKeyCode = 112;

  testWidgets('Copy paste test', (WidgetTester tester) async{
2253 2254
    final FocusNode focusNode = FocusNode();
    final TextEditingController controller = TextEditingController();
jslavitz's avatar
jslavitz committed
2255
    final TextField textField =
2256
      TextField(
jslavitz's avatar
jslavitz committed
2257 2258 2259 2260 2261 2262 2263 2264 2265 2266 2267 2268 2269 2270 2271
        controller: controller,
        maxLines: 3,
      );

    String clipboardContent = '';
    SystemChannels.platform
        .setMockMethodCallHandler((MethodCall methodCall) async {
      if (methodCall.method == 'Clipboard.setData')
        clipboardContent = methodCall.arguments['text'];
      else if (methodCall.method == 'Clipboard.getData')
        return <String, dynamic>{'text': clipboardContent};
      return null;
    });

    await tester.pumpWidget(
2272 2273 2274
      MaterialApp(
        home: Material(
          child: RawKeyboardListener(
jslavitz's avatar
jslavitz committed
2275 2276 2277 2278 2279 2280 2281 2282 2283 2284 2285 2286 2287
            focusNode: focusNode,
            onKey: null,
            child: textField,
          ),
        ),
      ),
    );

    const String testValue = 'a big house\njumped over a mouse'; // 11 \n 19
    await tester.enterText(find.byType(TextField), testValue);

    await tester.idle();
    await tester.tap(find.byType(TextField));
2288
    await tester.pumpAndSettle();
jslavitz's avatar
jslavitz committed
2289 2290 2291 2292 2293 2294 2295 2296 2297 2298 2299 2300 2301 2302 2303 2304 2305 2306 2307 2308 2309 2310 2311 2312 2313 2314 2315 2316 2317 2318 2319 2320 2321 2322 2323

    // Select the first 5 characters
    for (int i = 0; i < 5; i += 1) {
      sendKeyEventWithCode(22, true, true, false);             // RIGHT_ARROW keydown shift
      await tester.pumpAndSettle();
      sendKeyEventWithCode(22, false, false, false);           // RIGHT_ARROW keyup
      await tester.pumpAndSettle();
    }

    // Copy them
    sendKeyEventWithCode(_kCKeyCode, true, false, true);    // keydown control
    await tester.pumpAndSettle();
    sendKeyEventWithCode(_kCKeyCode, false, false, false);  // keyup control
    await tester.pumpAndSettle();

    expect(clipboardContent, 'a big');

    sendKeyEventWithCode(22, true, false, false);              // RIGHT_ARROW keydown
    await tester.pumpAndSettle();
    sendKeyEventWithCode(22, false, false, false);             // RIGHT_ARROW keyup
    await tester.pumpAndSettle();

    // Paste them
    sendKeyEventWithCode(_kVKeyCode, true, false, true);     // Control V keydown
    await tester.pumpAndSettle();
    await tester.pump(const Duration(milliseconds: 200));

    sendKeyEventWithCode(_kVKeyCode, false, false, false);   // Control V keyup
    await tester.pumpAndSettle();

    const String expected = 'a biga big house\njumped over a mouse';
    expect(find.text(expected), findsOneWidget);
  });

  testWidgets('Cut test', (WidgetTester tester) async{
2324 2325
    final FocusNode focusNode = FocusNode();
    final TextEditingController controller = TextEditingController();
jslavitz's avatar
jslavitz committed
2326
    final TextField textField =
2327
      TextField(
jslavitz's avatar
jslavitz committed
2328 2329 2330 2331 2332 2333 2334 2335 2336 2337 2338 2339 2340 2341
        controller: controller,
        maxLines: 3,
      );
    String clipboardContent = '';
    SystemChannels.platform
        .setMockMethodCallHandler((MethodCall methodCall) async {
      if (methodCall.method == 'Clipboard.setData')
        clipboardContent = methodCall.arguments['text'];
      else if (methodCall.method == 'Clipboard.getData')
        return <String, dynamic>{'text': clipboardContent};
      return null;
    });

    await tester.pumpWidget(
2342 2343 2344
      MaterialApp(
        home: Material(
          child: RawKeyboardListener(
jslavitz's avatar
jslavitz committed
2345 2346 2347 2348 2349 2350 2351 2352 2353 2354 2355 2356 2357
            focusNode: focusNode,
            onKey: null,
            child: textField,
          ),
        ),
      ),
    );

    const String testValue = 'a big house\njumped over a mouse'; // 11 \n 19
    await tester.enterText(find.byType(TextField), testValue);

    await tester.idle();
    await tester.tap(find.byType(TextField));
2358
    await tester.pumpAndSettle();
jslavitz's avatar
jslavitz committed
2359 2360 2361 2362 2363 2364 2365 2366 2367 2368 2369 2370 2371 2372 2373 2374 2375 2376 2377 2378 2379 2380 2381 2382 2383 2384 2385 2386 2387 2388 2389 2390 2391 2392 2393 2394 2395

    // Select the first 5 characters
    for (int i = 0; i < 5; i += 1) {
      sendKeyEventWithCode(22, true, true, false);             // RIGHT_ARROW keydown shift
      await tester.pumpAndSettle();
      sendKeyEventWithCode(22, false, false, false);           // RIGHT_ARROW keyup
      await tester.pumpAndSettle();
    }

    // Cut them
    sendKeyEventWithCode(_kXKeyCode, true, false, true);    // keydown control X
    await tester.pumpAndSettle();
    sendKeyEventWithCode(_kXKeyCode, false, false, false);  // keyup control X
    await tester.pumpAndSettle();

    expect(clipboardContent, 'a big');

    for (int i = 0; i < 5; i += 1) {
      sendKeyEventWithCode(22, true, false, false);  // RIGHT_ARROW keydown
      await tester.pumpAndSettle();
      sendKeyEventWithCode(22, false, false, false); // RIGHT_ARROW keyup
      await tester.pumpAndSettle();
    }

    // Paste them
    sendKeyEventWithCode(_kVKeyCode, true, false, true);     // Control V keydown
    await tester.pumpAndSettle();
    await tester.pump(const Duration(milliseconds: 200));

    sendKeyEventWithCode(_kVKeyCode, false, false, false);    // Control V keyup
    await tester.pumpAndSettle();

    const String expected = ' housa bige\njumped over a mouse';
    expect(find.text(expected), findsOneWidget);
  });

  testWidgets('Select all test', (WidgetTester tester) async{
2396 2397
    final FocusNode focusNode = FocusNode();
    final TextEditingController controller = TextEditingController();
jslavitz's avatar
jslavitz committed
2398
    final TextField textField =
2399
      TextField(
jslavitz's avatar
jslavitz committed
2400 2401 2402 2403 2404
        controller: controller,
        maxLines: 3,
      );

    await tester.pumpWidget(
2405 2406 2407
      MaterialApp(
        home: Material(
          child: RawKeyboardListener(
jslavitz's avatar
jslavitz committed
2408 2409 2410 2411 2412 2413 2414 2415 2416 2417 2418 2419 2420
            focusNode: focusNode,
            onKey: null,
            child: textField,
          ),
        ),
      ),
    );

    const String testValue = 'a big house\njumped over a mouse'; // 11 \n 19
    await tester.enterText(find.byType(TextField), testValue);

    await tester.idle();
    await tester.tap(find.byType(TextField));
2421
    await tester.pumpAndSettle();
jslavitz's avatar
jslavitz committed
2422 2423 2424 2425 2426 2427 2428 2429 2430 2431 2432 2433 2434 2435 2436 2437 2438 2439 2440 2441

    // Select All
    sendKeyEventWithCode(_kAKeyCode, true, false, true);    // keydown control A
    await tester.pumpAndSettle();
    sendKeyEventWithCode(_kAKeyCode, false, false, true);   // keyup control A
    await tester.pumpAndSettle();

    // Delete them
    sendKeyEventWithCode(_kDelKeyCode, true, false, false);     // DEL keydown
    await tester.pumpAndSettle();
    await tester.pump(const Duration(milliseconds: 200));

    sendKeyEventWithCode(_kDelKeyCode, false, false, false);     // DEL keyup
    await tester.pumpAndSettle();

    const String expected = '';
    expect(find.text(expected), findsOneWidget);
  });

  testWidgets('Delete test', (WidgetTester tester) async{
2442 2443
    final FocusNode focusNode = FocusNode();
    final TextEditingController controller = TextEditingController();
jslavitz's avatar
jslavitz committed
2444
    final TextField textField =
2445
      TextField(
jslavitz's avatar
jslavitz committed
2446 2447 2448 2449 2450
        controller: controller,
        maxLines: 3,
      );

    await tester.pumpWidget(
2451 2452 2453
      MaterialApp(
        home: Material(
          child: RawKeyboardListener(
jslavitz's avatar
jslavitz committed
2454 2455 2456 2457 2458 2459 2460 2461 2462 2463 2464 2465 2466
            focusNode: focusNode,
            onKey: null,
            child: textField,
          ),
        ),
      ),
    );

    const String testValue = 'a big house\njumped over a mouse'; // 11 \n 19
    await tester.enterText(find.byType(TextField), testValue);

    await tester.idle();
    await tester.tap(find.byType(TextField));
2467
    await tester.pumpAndSettle();
jslavitz's avatar
jslavitz committed
2468 2469 2470 2471 2472 2473 2474 2475 2476 2477 2478 2479 2480 2481 2482 2483 2484 2485 2486 2487 2488 2489 2490 2491 2492 2493 2494

    // Delete
    for (int i = 0; i < 6; i += 1) {
      sendKeyEventWithCode(_kDelKeyCode, true, false, false); // keydown DEL
      await tester.pumpAndSettle();
      sendKeyEventWithCode(_kDelKeyCode, false, false, false); // keyup DEL
      await tester.pumpAndSettle();
    }

    const String expected = 'house\njumped over a mouse';
    expect(find.text(expected), findsOneWidget);

    sendKeyEventWithCode(_kAKeyCode, true, false, true);    // keydown control A
    await tester.pumpAndSettle();
    sendKeyEventWithCode(_kAKeyCode, false, false, true);   // keyup control A
    await tester.pumpAndSettle();


    sendKeyEventWithCode(_kDelKeyCode, true, false, false); // keydown DEL
    await tester.pumpAndSettle();
    sendKeyEventWithCode(_kDelKeyCode, false, false, false); // keyup DEL
    await tester.pumpAndSettle();

    const String expected2 = '';
    expect(find.text(expected2), findsOneWidget);
  });

2495 2496
  testWidgets('Changing positions of text fields', (WidgetTester tester) async{

2497
    final FocusNode focusNode = FocusNode();
2498 2499
    final List<RawKeyEvent> events = <RawKeyEvent>[];

2500 2501 2502 2503
    final TextEditingController c1 = TextEditingController();
    final TextEditingController c2 = TextEditingController();
    final Key key1 = UniqueKey();
    final Key key2 = UniqueKey();
2504 2505

   await tester.pumpWidget(
2506
      MaterialApp(
2507 2508
        home:
        Material(
2509
          child: RawKeyboardListener(
2510 2511
            focusNode: focusNode,
            onKey: events.add,
2512
            child: Column(
2513 2514 2515 2516 2517 2518 2519 2520 2521 2522 2523 2524 2525 2526 2527 2528 2529 2530 2531 2532 2533 2534 2535 2536
              crossAxisAlignment: CrossAxisAlignment.stretch,
              children: <Widget>[
                TextField(
                  key: key1,
                  controller: c1,
                  maxLines: 3,
                ),
                TextField(
                  key: key2,
                  controller: c2,
                  maxLines: 3,
                ),
              ],
            ),
          ),
        ),
      ),
    );

    const String testValue = 'a big house';
    await tester.enterText(find.byType(TextField).first, testValue);

    await tester.idle();
    await tester.tap(find.byType(TextField).first);
2537
    await tester.pumpAndSettle();
2538 2539 2540 2541 2542 2543 2544 2545 2546

    for (int i = 0; i < 5; i += 1) {
      sendKeyEventWithCode(22, true, true, false); // RIGHT_ARROW keydown
      await tester.pumpAndSettle();
    }

    expect(c1.selection.extentOffset - c1.selection.baseOffset, 5);

    await tester.pumpWidget(
2547
      MaterialApp(
2548 2549
        home:
        Material(
2550
          child: RawKeyboardListener(
2551 2552
            focusNode: focusNode,
            onKey: events.add,
2553
            child: Column(
2554 2555 2556 2557 2558 2559 2560 2561 2562 2563 2564 2565 2566 2567 2568 2569 2570 2571 2572 2573 2574 2575 2576 2577 2578 2579 2580 2581 2582
              crossAxisAlignment: CrossAxisAlignment.stretch,
              children: <Widget>[
                TextField(
                  key: key2,
                  controller: c2,
                  maxLines: 3,
                ),
                TextField(
                  key: key1,
                  controller: c1,
                  maxLines: 3,
                ),
              ],
            ),
          ),
        ),
      ),
    );

    for (int i = 0; i < 5; i += 1) {
      sendKeyEventWithCode(22, true, true, false); // RIGHT_ARROW keydown
      await tester.pumpAndSettle();
    }

    expect(c1.selection.extentOffset - c1.selection.baseOffset, 10);
  });


  testWidgets('Changing focus test', (WidgetTester tester) async {
2583
    final FocusNode focusNode = FocusNode();
2584 2585
    final List<RawKeyEvent> events = <RawKeyEvent>[];

2586 2587 2588 2589
    final TextEditingController c1 = TextEditingController();
    final TextEditingController c2 = TextEditingController();
    final Key key1 = UniqueKey();
    final Key key2 = UniqueKey();
2590 2591

    await tester.pumpWidget(
2592
      MaterialApp(
2593 2594
        home:
        Material(
2595
          child: RawKeyboardListener(
2596 2597
            focusNode: focusNode,
            onKey: events.add,
2598
            child: Column(
2599 2600 2601 2602 2603 2604 2605 2606 2607 2608 2609 2610 2611 2612 2613 2614 2615 2616 2617 2618 2619 2620 2621 2622 2623
              crossAxisAlignment: CrossAxisAlignment.stretch,
              children: <Widget>[
                TextField(
                  key: key1,
                  controller: c1,
                  maxLines: 3,
                ),
                TextField(
                  key: key2,
                  controller: c2,
                  maxLines: 3,
                ),
              ],
            ),
          ),
        ),
      ),
    );

    await tester.idle();
    await tester.tap(find.byType(TextField).first);

    const String testValue = 'a big house';
    await tester.enterText(find.byType(TextField).first, testValue);

2624
    await tester.pumpAndSettle();
2625 2626 2627 2628 2629 2630 2631 2632 2633 2634 2635 2636 2637 2638

    for (int i = 0; i < 5; i += 1) {
      sendKeyEventWithCode(22, true, true, false); // RIGHT_ARROW keydown
      await tester.pumpAndSettle();
    }

    expect(c1.selection.extentOffset - c1.selection.baseOffset, 5);
    expect(c2.selection.extentOffset - c2.selection.baseOffset, 0);

    await tester.idle();
    await tester.tap(find.byType(TextField).last);

    await tester.enterText(find.byType(TextField).last, testValue);

2639
    await tester.pumpAndSettle();
2640 2641 2642 2643 2644 2645 2646 2647 2648 2649

    for (int i = 0; i < 5; i += 1) {
      sendKeyEventWithCode(22, true, true, false); // RIGHT_ARROW keydown
      await tester.pumpAndSettle();
    }

    expect(c1.selection.extentOffset - c1.selection.baseOffset, 0);
    expect(c2.selection.extentOffset - c2.selection.baseOffset, 5);
  });

2650
  testWidgets('Caret works when maxLines is null', (WidgetTester tester) async {
2651
    final TextEditingController controller = TextEditingController();
2652 2653 2654

    await tester.pumpWidget(
      overlay(
2655
        child: TextField(
2656 2657 2658 2659 2660 2661
          controller: controller,
          maxLines: null,
        ),
      )
    );

2662
    const String testValue = 'x';
2663 2664 2665 2666 2667 2668
    await tester.enterText(find.byType(TextField), testValue);
    await skipPastScrollingAnimation(tester);
    expect(controller.selection.baseOffset, -1);

    // Tap the selection handle to bring up the "paste / select all" menu.
    await tester.tapAt(textOffsetToPosition(tester, 0));
2669
    await tester.pump();
2670 2671 2672 2673 2674
    await tester.pump(const Duration(milliseconds: 200)); // skip past the frame where the opacity is

    // Confirm that the selection was updated.
    expect(controller.selection.baseOffset, 0);
  });
2675 2676

  testWidgets('TextField baseline alignment', (WidgetTester tester) async {
2677 2678 2679 2680
    final TextEditingController controllerA = TextEditingController(text: 'A');
    final TextEditingController controllerB = TextEditingController(text: 'B');
    final Key keyA = UniqueKey();
    final Key keyB = UniqueKey();
2681 2682 2683

    await tester.pumpWidget(
      overlay(
2684
        child: Row(
2685 2686 2687
          crossAxisAlignment: CrossAxisAlignment.baseline,
          textBaseline: TextBaseline.alphabetic,
          children: <Widget>[
2688 2689
            Expanded(
              child: TextField(
2690 2691 2692 2693 2694 2695 2696 2697
                key: keyA,
                decoration: null,
                controller: controllerA,
                style: const TextStyle(fontSize: 10.0),
              )
            ),
            const Text(
              'abc',
2698
              style: TextStyle(fontSize: 20.0),
2699
            ),
2700 2701
            Expanded(
              child: TextField(
2702 2703 2704 2705 2706 2707 2708 2709 2710 2711 2712 2713 2714 2715 2716 2717 2718 2719 2720 2721
                key: keyB,
                decoration: null,
                controller: controllerB,
                style: const TextStyle(fontSize: 30.0),
              ),
            ),
          ],
        ),
      ),
    );

    // The Ahem font extends 0.2 * fontSize below the baseline.
    // So the three row elements line up like this:
    //
    //  A  abc  B
    //  ---------   baseline
    //  2  4    6   space below the baseline = 0.2 * fontSize
    //  ---------   rowBottomY

    final double rowBottomY = tester.getBottomLeft(find.byType(Row)).dy;
2722 2723
    expect(tester.getBottomLeft(find.byKey(keyA)).dy, closeTo(rowBottomY - 4.0, 0.001));
    expect(tester.getBottomLeft(find.text('abc')).dy, closeTo(rowBottomY - 2.0, 0.001));
2724 2725 2726
    expect(tester.getBottomLeft(find.byKey(keyB)).dy, rowBottomY);
  });

2727
  testWidgets('TextField semantics', (WidgetTester tester) async {
2728 2729 2730
    final SemanticsTester semantics = SemanticsTester(tester);
    final TextEditingController controller = TextEditingController();
    final Key key = UniqueKey();
2731 2732 2733

    await tester.pumpWidget(
      overlay(
2734
        child: TextField(
2735 2736
          key: key,
          controller: controller,
2737
        ),
2738 2739 2740
      ),
    );

2741
    expect(semantics, hasSemantics(TestSemantics.root(
2742
      children: <TestSemantics>[
2743
        TestSemantics.rootChild(
2744
          id: 1,
2745 2746 2747 2748 2749 2750 2751 2752 2753 2754 2755 2756 2757 2758
          textDirection: TextDirection.ltr,
          actions: <SemanticsAction>[
            SemanticsAction.tap,
          ],
          flags: <SemanticsFlag>[
            SemanticsFlag.isTextField,
          ],
        ),
      ],
    ), ignoreTransform: true, ignoreRect: true));

    controller.text = 'Guten Tag';
    await tester.pump();

2759
    expect(semantics, hasSemantics(TestSemantics.root(
2760
      children: <TestSemantics>[
2761
        TestSemantics.rootChild(
2762
          id: 1,
2763 2764 2765 2766 2767 2768 2769 2770 2771 2772 2773 2774 2775
          textDirection: TextDirection.ltr,
          value: 'Guten Tag',
          actions: <SemanticsAction>[
            SemanticsAction.tap,
          ],
          flags: <SemanticsFlag>[
            SemanticsFlag.isTextField,
          ],
        ),
      ],
    ), ignoreTransform: true, ignoreRect: true));

    await tester.tap(find.byKey(key));
2776
    await tester.pump();
2777

2778
    expect(semantics, hasSemantics(TestSemantics.root(
2779
      children: <TestSemantics>[
2780
        TestSemantics.rootChild(
2781
          id: 1,
2782 2783
          textDirection: TextDirection.ltr,
          value: 'Guten Tag',
2784
          textSelection: const TextSelection.collapsed(offset: 9),
2785 2786 2787
          actions: <SemanticsAction>[
            SemanticsAction.tap,
            SemanticsAction.moveCursorBackwardByCharacter,
2788
            SemanticsAction.moveCursorBackwardByWord,
2789
            SemanticsAction.setSelection,
2790
            SemanticsAction.paste,
2791 2792 2793 2794 2795 2796 2797 2798 2799 2800 2801 2802
          ],
          flags: <SemanticsFlag>[
            SemanticsFlag.isTextField,
            SemanticsFlag.isFocused,
          ],
        ),
      ],
    ), ignoreTransform: true, ignoreRect: true));

    controller.selection = const TextSelection.collapsed(offset: 4);
    await tester.pump();

2803
    expect(semantics, hasSemantics(TestSemantics.root(
2804
      children: <TestSemantics>[
2805
        TestSemantics.rootChild(
2806
          id: 1,
2807
          textDirection: TextDirection.ltr,
2808
          textSelection: const TextSelection.collapsed(offset: 4),
2809 2810 2811 2812 2813
          value: 'Guten Tag',
          actions: <SemanticsAction>[
            SemanticsAction.tap,
            SemanticsAction.moveCursorBackwardByCharacter,
            SemanticsAction.moveCursorForwardByCharacter,
2814 2815
            SemanticsAction.moveCursorBackwardByWord,
            SemanticsAction.moveCursorForwardByWord,
2816
            SemanticsAction.setSelection,
2817
            SemanticsAction.paste,
2818 2819 2820 2821 2822 2823 2824 2825 2826 2827 2828 2829 2830
          ],
          flags: <SemanticsFlag>[
            SemanticsFlag.isTextField,
            SemanticsFlag.isFocused,
          ],
        ),
      ],
    ), ignoreTransform: true, ignoreRect: true));

    controller.text = 'Schönen Feierabend';
    controller.selection = const TextSelection.collapsed(offset: 0);
    await tester.pump();

2831
    expect(semantics, hasSemantics(TestSemantics.root(
2832
      children: <TestSemantics>[
2833
        TestSemantics.rootChild(
2834
          id: 1,
2835
          textDirection: TextDirection.ltr,
2836
          textSelection: const TextSelection.collapsed(offset: 0),
2837 2838 2839 2840
          value: 'Schönen Feierabend',
          actions: <SemanticsAction>[
            SemanticsAction.tap,
            SemanticsAction.moveCursorForwardByCharacter,
2841
            SemanticsAction.moveCursorForwardByWord,
2842
            SemanticsAction.setSelection,
2843
            SemanticsAction.paste,
2844 2845 2846 2847 2848 2849 2850 2851 2852 2853 2854 2855
          ],
          flags: <SemanticsFlag>[
            SemanticsFlag.isTextField,
            SemanticsFlag.isFocused,
          ],
        ),
      ],
    ), ignoreTransform: true, ignoreRect: true));

    semantics.dispose();
  });

2856 2857 2858 2859 2860 2861 2862 2863 2864 2865 2866 2867 2868 2869 2870 2871 2872 2873 2874 2875 2876 2877 2878 2879 2880 2881 2882 2883 2884 2885 2886 2887 2888 2889 2890 2891 2892 2893 2894 2895 2896 2897
  testWidgets('TextField semantics, enableInteractiveSelection = false', (WidgetTester tester) async {
    final SemanticsTester semantics = SemanticsTester(tester);
    final TextEditingController controller = TextEditingController();
    final Key key = UniqueKey();

    await tester.pumpWidget(
      overlay(
        child: TextField(
          key: key,
          controller: controller,
          enableInteractiveSelection: false,
        ),
      ),
    );

    await tester.tap(find.byKey(key));
    await tester.pump();

    expect(semantics, hasSemantics(TestSemantics.root(
      children: <TestSemantics>[
        TestSemantics.rootChild(
          id: 1,
          textDirection: TextDirection.ltr,
          actions: <SemanticsAction>[
            SemanticsAction.tap,
            // Absent the following because enableInteractiveSelection: false
            // SemanticsAction.moveCursorBackwardByCharacter,
            // SemanticsAction.moveCursorBackwardByWord,
            // SemanticsAction.setSelection,
            // SemanticsAction.paste,
          ],
          flags: <SemanticsFlag>[
            SemanticsFlag.isTextField,
            SemanticsFlag.isFocused,
          ],
        ),
      ],
    ), ignoreTransform: true, ignoreRect: true));

    semantics.dispose();
  });

2898
  testWidgets('TextField semantics for selections', (WidgetTester tester) async {
2899 2900
    final SemanticsTester semantics = SemanticsTester(tester);
    final TextEditingController controller = TextEditingController()
2901
      ..text = 'Hello';
2902
    final Key key = UniqueKey();
2903 2904 2905

    await tester.pumpWidget(
      overlay(
2906
        child: TextField(
2907 2908 2909
          key: key,
          controller: controller,
        ),
2910 2911 2912
      ),
    );

2913
    expect(semantics, hasSemantics(TestSemantics.root(
2914
      children: <TestSemantics>[
2915
        TestSemantics.rootChild(
2916
          id: 1,
2917 2918 2919 2920 2921 2922 2923 2924 2925 2926 2927 2928 2929 2930
          value: 'Hello',
          textDirection: TextDirection.ltr,
          actions: <SemanticsAction>[
            SemanticsAction.tap,
          ],
          flags: <SemanticsFlag>[
            SemanticsFlag.isTextField,
          ],
        ),
      ],
    ), ignoreTransform: true, ignoreRect: true));

    // Focus the text field
    await tester.tap(find.byKey(key));
2931
    await tester.pump();
2932

2933
    expect(semantics, hasSemantics(TestSemantics.root(
2934
      children: <TestSemantics>[
2935
        TestSemantics.rootChild(
2936
          id: 1,
2937 2938 2939 2940 2941 2942
          value: 'Hello',
          textSelection: const TextSelection.collapsed(offset: 5),
          textDirection: TextDirection.ltr,
          actions: <SemanticsAction>[
            SemanticsAction.tap,
            SemanticsAction.moveCursorBackwardByCharacter,
2943
            SemanticsAction.moveCursorBackwardByWord,
2944
            SemanticsAction.setSelection,
2945
            SemanticsAction.paste,
2946 2947 2948 2949 2950 2951 2952 2953 2954 2955 2956 2957
          ],
          flags: <SemanticsFlag>[
            SemanticsFlag.isTextField,
            SemanticsFlag.isFocused,
          ],
        ),
      ],
    ), ignoreTransform: true, ignoreRect: true));

    controller.selection = const TextSelection(baseOffset: 5, extentOffset: 3);
    await tester.pump();

2958
    expect(semantics, hasSemantics(TestSemantics.root(
2959
      children: <TestSemantics>[
2960
        TestSemantics.rootChild(
2961
          id: 1,
2962 2963 2964 2965 2966 2967 2968
          value: 'Hello',
          textSelection: const TextSelection(baseOffset: 5, extentOffset: 3),
          textDirection: TextDirection.ltr,
          actions: <SemanticsAction>[
            SemanticsAction.tap,
            SemanticsAction.moveCursorBackwardByCharacter,
            SemanticsAction.moveCursorForwardByCharacter,
2969 2970
            SemanticsAction.moveCursorBackwardByWord,
            SemanticsAction.moveCursorForwardByWord,
2971
            SemanticsAction.setSelection,
2972 2973 2974
            SemanticsAction.paste,
            SemanticsAction.cut,
            SemanticsAction.copy,
2975 2976 2977 2978 2979 2980 2981 2982 2983 2984 2985 2986
          ],
          flags: <SemanticsFlag>[
            SemanticsFlag.isTextField,
            SemanticsFlag.isFocused,
          ],
        ),
      ],
    ), ignoreTransform: true, ignoreRect: true));

    semantics.dispose();
  });

2987
  testWidgets('TextField change selection with semantics', (WidgetTester tester) async {
2988
    final SemanticsTester semantics = SemanticsTester(tester);
2989
    final SemanticsOwner semanticsOwner = tester.binding.pipelineOwner.semanticsOwner;
2990
    final TextEditingController controller = TextEditingController()
2991
      ..text = 'Hello';
2992
    final Key key = UniqueKey();
2993 2994 2995

    await tester.pumpWidget(
      overlay(
2996
        child: TextField(
2997 2998 2999 3000 3001 3002 3003 3004
          key: key,
          controller: controller,
        ),
      ),
    );

    // Focus the text field
    await tester.tap(find.byKey(key));
3005
    await tester.pump();
3006

3007
    const int inputFieldId = 1;
3008 3009

    expect(controller.selection, const TextSelection.collapsed(offset: 5, affinity: TextAffinity.upstream));
3010
    expect(semantics, hasSemantics(TestSemantics.root(
3011
      children: <TestSemantics>[
3012
        TestSemantics.rootChild(
3013 3014 3015 3016 3017 3018 3019
          id: inputFieldId,
          value: 'Hello',
          textSelection: const TextSelection.collapsed(offset: 5),
          textDirection: TextDirection.ltr,
          actions: <SemanticsAction>[
            SemanticsAction.tap,
            SemanticsAction.moveCursorBackwardByCharacter,
3020
            SemanticsAction.moveCursorBackwardByWord,
3021
            SemanticsAction.setSelection,
3022
            SemanticsAction.paste,
3023 3024 3025 3026 3027 3028 3029 3030 3031 3032 3033 3034 3035 3036 3037 3038 3039 3040 3041 3042 3043 3044 3045 3046 3047 3048 3049 3050 3051 3052 3053 3054
          ],
          flags: <SemanticsFlag>[
            SemanticsFlag.isTextField,
            SemanticsFlag.isFocused,
          ],
        ),
      ],
    ), ignoreTransform: true, ignoreRect: true));

    // move cursor back once
    semanticsOwner.performAction(inputFieldId, SemanticsAction.setSelection, <String, int>{
      'base': 4,
      'extent': 4,
    });
    await tester.pump();
    expect(controller.selection, const TextSelection.collapsed(offset: 4));

    // move cursor to front
    semanticsOwner.performAction(inputFieldId, SemanticsAction.setSelection, <String, int>{
      'base': 0,
      'extent': 0,
    });
    await tester.pump();
    expect(controller.selection, const TextSelection.collapsed(offset: 0));

    // select all
    semanticsOwner.performAction(inputFieldId, SemanticsAction.setSelection, <String, int>{
      'base': 0,
      'extent': 5,
    });
    await tester.pump();
    expect(controller.selection, const TextSelection(baseOffset: 0, extentOffset: 5));
3055
    expect(semantics, hasSemantics(TestSemantics.root(
3056
      children: <TestSemantics>[
3057
        TestSemantics.rootChild(
3058 3059 3060 3061 3062 3063 3064
          id: inputFieldId,
          value: 'Hello',
          textSelection: const TextSelection(baseOffset: 0, extentOffset: 5),
          textDirection: TextDirection.ltr,
          actions: <SemanticsAction>[
            SemanticsAction.tap,
            SemanticsAction.moveCursorBackwardByCharacter,
3065
            SemanticsAction.moveCursorBackwardByWord,
3066
            SemanticsAction.setSelection,
3067 3068 3069
            SemanticsAction.paste,
            SemanticsAction.cut,
            SemanticsAction.copy,
3070 3071 3072 3073 3074 3075 3076 3077 3078 3079 3080 3081
          ],
          flags: <SemanticsFlag>[
            SemanticsFlag.isTextField,
            SemanticsFlag.isFocused,
          ],
        ),
      ],
    ), ignoreTransform: true, ignoreRect: true));

    semantics.dispose();
  });

3082 3083 3084 3085 3086
  testWidgets('Can activate TextField with explicit controller via semantics ', (WidgetTester tester) async {
    // Regression test for https://github.com/flutter/flutter/issues/17801

    const String textInTextField = 'Hello';

3087
    final SemanticsTester semantics = SemanticsTester(tester);
3088
    final SemanticsOwner semanticsOwner = tester.binding.pipelineOwner.semanticsOwner;
3089
    final TextEditingController controller = TextEditingController()
3090
      ..text = textInTextField;
3091
    final Key key = UniqueKey();
3092 3093 3094

    await tester.pumpWidget(
      overlay(
3095
        child: TextField(
3096 3097 3098 3099 3100 3101 3102 3103 3104
          key: key,
          controller: controller,
        ),
      ),
    );

    const int inputFieldId = 1;

    expect(semantics, hasSemantics(
3105
      TestSemantics.root(
3106
        children: <TestSemantics>[
3107
          TestSemantics(
3108 3109 3110 3111 3112 3113 3114 3115 3116 3117 3118 3119 3120 3121 3122
            id: inputFieldId,
            flags: <SemanticsFlag>[SemanticsFlag.isTextField],
            actions: <SemanticsAction>[SemanticsAction.tap],
            value: textInTextField,
            textDirection: TextDirection.ltr,
          ),
        ],
      ),
      ignoreRect: true, ignoreTransform: true,
    ));

    semanticsOwner.performAction(inputFieldId, SemanticsAction.tap);
    await tester.pump();

    expect(semantics, hasSemantics(
3123
      TestSemantics.root(
3124
        children: <TestSemantics>[
3125
          TestSemantics(
3126 3127 3128 3129 3130 3131 3132 3133
            id: inputFieldId,
            flags: <SemanticsFlag>[
              SemanticsFlag.isTextField,
              SemanticsFlag.isFocused,
            ],
            actions: <SemanticsAction>[
              SemanticsAction.tap,
              SemanticsAction.moveCursorBackwardByCharacter,
3134
              SemanticsAction.moveCursorBackwardByWord,
3135 3136 3137 3138 3139 3140 3141 3142 3143 3144 3145 3146 3147 3148 3149 3150 3151 3152
              SemanticsAction.setSelection,
              SemanticsAction.paste,
            ],
            value: textInTextField,
            textDirection: TextDirection.ltr,
            textSelection: const TextSelection(
              baseOffset: textInTextField.length,
              extentOffset: textInTextField.length,
            ),
          ),
        ],
      ),
      ignoreRect: true, ignoreTransform: true,
    ));

    semantics.dispose();
  });

3153
  testWidgets('TextField throws when not descended from a Material widget', (WidgetTester tester) async {
3154
    const Widget textField = TextField();
3155 3156 3157 3158 3159 3160
    await tester.pumpWidget(textField);
    final dynamic exception = tester.takeException();
    expect(exception, isFlutterError);
    expect(exception.toString(), startsWith('No Material widget found.'));
    expect(exception.toString(), endsWith(':\n  $textField\nThe ancestors of this widget were:\n  [root]'));
  });
3161

3162
  testWidgets('TextField loses focus when disabled', (WidgetTester tester) async {
3163
    final FocusNode focusNode = FocusNode();
3164 3165 3166

    await tester.pumpWidget(
      boilerplate(
3167
        child: TextField(
3168 3169 3170 3171 3172 3173 3174 3175 3176 3177
          focusNode: focusNode,
          autofocus: true,
          enabled: true,
        ),
      ),
    );
    expect(focusNode.hasFocus, isTrue);

    await tester.pumpWidget(
      boilerplate(
3178
        child: TextField(
3179 3180 3181 3182 3183 3184 3185 3186 3187
          focusNode: focusNode,
          autofocus: true,
          enabled: false,
        ),
      ),
    );
    expect(focusNode.hasFocus, isFalse);
  });

3188 3189 3190 3191 3192 3193 3194 3195 3196 3197 3198 3199 3200 3201 3202 3203 3204 3205 3206
  testWidgets('TextField displays text with text direction', (WidgetTester tester) async {
    await tester.pumpWidget(
      const MaterialApp(
        home: Material(
          child: TextField(
            textDirection: TextDirection.rtl,
          ),
        ),
      ),
    );

    RenderEditable editable = findRenderEditable(tester);

    await tester.enterText(find.byType(TextField), '0123456789101112');
    await tester.pumpAndSettle();
    Offset topLeft = editable.localToGlobal(
      editable.getLocalRectForCaret(const TextPosition(offset: 10)).topLeft,
    );

3207
    expect(topLeft.dx, equals(701));
3208 3209 3210 3211 3212 3213 3214 3215 3216 3217 3218 3219 3220 3221 3222 3223 3224 3225 3226

    await tester.pumpWidget(
      const MaterialApp(
        home: Material(
          child: TextField(
            textDirection: TextDirection.ltr,
          ),
        ),
      ),
    );

    editable = findRenderEditable(tester);

    await tester.enterText(find.byType(TextField), '0123456789101112');
    await tester.pumpAndSettle();
    topLeft = editable.localToGlobal(
      editable.getLocalRectForCaret(const TextPosition(offset: 10)).topLeft,
    );

3227
    expect(topLeft.dx, equals(160.0));
3228 3229
  });

3230
  testWidgets('TextField semantics', (WidgetTester tester) async {
3231 3232 3233
    final SemanticsTester semantics = SemanticsTester(tester);
    final TextEditingController controller = TextEditingController();
    final Key key = UniqueKey();
3234 3235 3236

    await tester.pumpWidget(
      overlay(
3237
        child: TextField(
3238 3239
          key: key,
          controller: controller,
3240
          maxLength: 10,
3241 3242 3243 3244 3245 3246 3247 3248 3249
          decoration: const InputDecoration(
            labelText: 'label',
            hintText: 'hint',
            helperText: 'helper',
          ),
        ),
      ),
    );

3250
    expect(semantics, hasSemantics(TestSemantics.root(
3251
      children: <TestSemantics>[
3252
        TestSemantics.rootChild(
3253
          label: 'label',
3254 3255 3256 3257 3258 3259 3260 3261
          id: 1,
          textDirection: TextDirection.ltr,
          actions: <SemanticsAction>[
            SemanticsAction.tap,
          ],
          flags: <SemanticsFlag>[
            SemanticsFlag.isTextField,
          ],
3262
          children: <TestSemantics>[
3263
            TestSemantics(
3264
              id: 2,
3265 3266 3267 3268 3269
              label: 'helper',
              textDirection: TextDirection.ltr,
            ),
            TestSemantics(
              id: 3,
3270 3271 3272 3273
              label: '10 characters remaining',
              textDirection: TextDirection.ltr,
            ),
          ],
3274 3275 3276 3277 3278
        ),
      ],
    ), ignoreTransform: true, ignoreRect: true));

    await tester.tap(find.byType(TextField));
3279
    await tester.pump();
3280

3281
    expect(semantics, hasSemantics(TestSemantics.root(
3282
      children: <TestSemantics>[
3283
        TestSemantics.rootChild(
3284
          label: 'hint',
3285 3286 3287 3288 3289 3290 3291 3292 3293 3294 3295 3296
          id: 1,
          textDirection: TextDirection.ltr,
          textSelection: const TextSelection(baseOffset: 0, extentOffset: 0),
          actions: <SemanticsAction>[
            SemanticsAction.tap,
            SemanticsAction.setSelection,
            SemanticsAction.paste,
          ],
          flags: <SemanticsFlag>[
            SemanticsFlag.isTextField,
            SemanticsFlag.isFocused,
          ],
3297
          children: <TestSemantics>[
3298
            TestSemantics(
3299
              id: 2,
3300 3301 3302 3303 3304
              label: 'helper',
              textDirection: TextDirection.ltr,
            ),
            TestSemantics(
              id: 3,
3305
              label: '10 characters remaining',
3306 3307 3308
              flags: <SemanticsFlag>[
                SemanticsFlag.isLiveRegion,
              ],
3309 3310 3311
              textDirection: TextDirection.ltr,
            ),
          ],
3312 3313 3314 3315 3316 3317 3318 3319 3320
        ),
      ],
    ), ignoreTransform: true, ignoreRect: true));

    controller.text = 'hello';
    await tester.pump();
    semantics.dispose();
  });

3321
  testWidgets('InputDecoration counterText can have a semanticCounterText', (WidgetTester tester) async {
3322 3323 3324
    final SemanticsTester semantics = SemanticsTester(tester);
    final TextEditingController controller = TextEditingController();
    final Key key = UniqueKey();
3325 3326 3327

    await tester.pumpWidget(
      overlay(
3328
        child: TextField(
3329 3330 3331 3332 3333 3334 3335 3336 3337 3338 3339 3340 3341
          key: key,
          controller: controller,
          decoration: const InputDecoration(
            labelText: 'label',
            hintText: 'hint',
            helperText: 'helper',
            counterText: '0/10',
            semanticCounterText: '0 out of 10',
          ),
        ),
      ),
    );

3342
    expect(semantics, hasSemantics(TestSemantics.root(
3343
      children: <TestSemantics>[
3344
        TestSemantics.rootChild(
3345
          label: 'label',
3346 3347 3348 3349 3350 3351 3352 3353
          textDirection: TextDirection.ltr,
          actions: <SemanticsAction>[
            SemanticsAction.tap,
          ],
          flags: <SemanticsFlag>[
            SemanticsFlag.isTextField,
          ],
          children: <TestSemantics>[
3354 3355 3356 3357
            TestSemantics(
              label: 'helper',
              textDirection: TextDirection.ltr,
            ),
3358
            TestSemantics(
3359 3360 3361 3362 3363 3364 3365 3366 3367 3368
              label: '0 out of 10',
              textDirection: TextDirection.ltr,
            ),
          ],
        ),
      ],
    ), ignoreTransform: true, ignoreRect: true, ignoreId: true));

    semantics.dispose();
  });
3369

3370 3371 3372 3373 3374 3375 3376 3377 3378 3379 3380 3381 3382 3383 3384 3385 3386 3387 3388 3389 3390 3391 3392 3393 3394 3395 3396 3397 3398 3399 3400 3401 3402 3403 3404 3405 3406 3407 3408 3409 3410 3411 3412 3413 3414 3415
  testWidgets('InputDecoration errorText semantics', (WidgetTester tester) async {
    final SemanticsTester semantics = SemanticsTester(tester);
    final TextEditingController controller = TextEditingController();
    final Key key = UniqueKey();

    await tester.pumpWidget(
      overlay(
        child: TextField(
          key: key,
          controller: controller,
          decoration: const InputDecoration(
            labelText: 'label',
            hintText: 'hint',
            errorText: 'oh no!',
          ),
        ),
      ),
    );

    expect(semantics, hasSemantics(TestSemantics.root(
      children: <TestSemantics>[
        TestSemantics.rootChild(
          label: 'label',
          textDirection: TextDirection.ltr,
          actions: <SemanticsAction>[
            SemanticsAction.tap,
          ],
          flags: <SemanticsFlag>[
            SemanticsFlag.isTextField,
          ],
          children: <TestSemantics>[
            TestSemantics(
              label: 'oh no!',
              flags: <SemanticsFlag>[
                SemanticsFlag.isLiveRegion,
              ],
              textDirection: TextDirection.ltr,
            ),
          ],
        ),
      ],
    ), ignoreTransform: true, ignoreRect: true, ignoreId: true));

    semantics.dispose();
  });

3416 3417 3418 3419 3420
  testWidgets('floating label does not overlap with value at large textScaleFactors', (WidgetTester tester) async {
    final TextEditingController controller = TextEditingController(text: 'Just some text');
    await tester.pumpWidget(
      MaterialApp(
        home: Scaffold(
3421
          body: MediaQuery(
3422 3423 3424 3425 3426 3427 3428 3429 3430 3431 3432
              data: MediaQueryData.fromWindow(ui.window).copyWith(textScaleFactor: 4.0),
              child: Center(
                child: TextField(
                  decoration: const InputDecoration(labelText: 'Label', border: UnderlineInputBorder()),
                  controller: controller,
                ),
              ),
            ),
          ),
        ),
    );
3433

3434 3435 3436 3437 3438
    await tester.tap(find.byType(TextField));
    final Rect labelRect = tester.getRect(find.text('Label'));
    final Rect fieldRect = tester.getRect(find.text('Just some text'));
    expect(labelRect.bottom, lessThanOrEqualTo(fieldRect.top));
  });
3439 3440 3441 3442 3443 3444 3445 3446 3447 3448 3449 3450 3451 3452 3453 3454 3455 3456 3457 3458 3459 3460 3461 3462 3463 3464 3465 3466 3467 3468 3469 3470 3471 3472 3473 3474 3475 3476 3477 3478 3479 3480 3481 3482 3483 3484 3485 3486 3487 3488 3489 3490 3491 3492 3493 3494 3495 3496 3497 3498 3499 3500 3501 3502 3503 3504 3505 3506 3507 3508 3509 3510 3511 3512 3513 3514 3515 3516 3517 3518 3519 3520 3521 3522 3523 3524 3525 3526 3527 3528 3529 3530 3531 3532 3533 3534 3535 3536 3537 3538 3539 3540 3541 3542 3543 3544 3545 3546 3547 3548 3549 3550 3551 3552 3553 3554 3555 3556 3557 3558 3559 3560 3561 3562 3563 3564 3565 3566 3567 3568 3569 3570 3571 3572 3573 3574 3575 3576 3577 3578 3579 3580 3581 3582

  testWidgets('TextField scrolls into view but does not bounce (SingleChildScrollView)', (WidgetTester tester) async {
    // This is a regression test for https://github.com/flutter/flutter/issues/20485

    final Key textField1 = UniqueKey();
    final Key textField2 = UniqueKey();
    final ScrollController scrollController = ScrollController();

    double minOffset;
    double maxOffset;

    scrollController.addListener(() {
      final double offset = scrollController.offset;
      minOffset = math.min(minOffset ?? offset, offset);
      maxOffset = math.max(maxOffset ?? offset, offset);
    });

    Widget buildFrame(Axis scrollDirection) {
      return MaterialApp(
        home: Scaffold(
          body: SafeArea(
            child: SingleChildScrollView(
              physics: const BouncingScrollPhysics(),
              controller: scrollController,
              child: Column(
                children: <Widget>[
                  SizedBox( // visible when scrollOffset is 0.0
                    height: 100.0,
                    width: 100.0,
                    child: TextField(key: textField1, scrollPadding: const EdgeInsets.all(200.0)),
                  ),
                  const SizedBox(
                    height: 600.0, // Same size as the frame. Initially
                    width: 800.0,  // textField2 is not visible
                  ),
                  SizedBox( // visible when scrollOffset is 200.0
                    height: 100.0,
                    width: 100.0,
                    child: TextField(key: textField2, scrollPadding: const EdgeInsets.all(200.0)),
                  ),
                ],
              ),
            ),
          ),
        ),
      );
    }

    await tester.pumpWidget(buildFrame(Axis.vertical));
    await tester.enterText(find.byKey(textField1), '1');
    await tester.pumpAndSettle();
    await tester.enterText(find.byKey(textField2), '2'); //scroll textField2 into view
    await tester.pumpAndSettle();
    await tester.enterText(find.byKey(textField1), '3'); //scroll textField1 back into view
    await tester.pumpAndSettle();

    expect(minOffset, 0.0);
    expect(maxOffset, 200.0);

    minOffset = null;
    maxOffset = null;

    await tester.pumpWidget(buildFrame(Axis.horizontal));
    await tester.enterText(find.byKey(textField1), '1');
    await tester.pumpAndSettle();
    await tester.enterText(find.byKey(textField2), '2'); //scroll textField2 into view
    await tester.pumpAndSettle();
    await tester.enterText(find.byKey(textField1), '3'); //scroll textField1 back into view
    await tester.pumpAndSettle();

    expect(minOffset, 0.0);
    expect(maxOffset, 200.0);
  });

  testWidgets('TextField scrolls into view but does not bounce (ListView)', (WidgetTester tester) async {
    // This is a regression test for https://github.com/flutter/flutter/issues/20485

    final Key textField1 = UniqueKey();
    final Key textField2 = UniqueKey();
    final ScrollController scrollController = ScrollController();

    double minOffset;
    double maxOffset;

    scrollController.addListener(() {
      final double offset = scrollController.offset;
      minOffset = math.min(minOffset ?? offset, offset);
      maxOffset = math.max(maxOffset ?? offset, offset);
    });

    Widget buildFrame(Axis scrollDirection) {
      return MaterialApp(
        home: Scaffold(
          body: SafeArea(
            child: ListView(
              physics: const BouncingScrollPhysics(),
              controller: scrollController,
              children: <Widget>[
                SizedBox( // visible when scrollOffset is 0.0
                  height: 100.0,
                  width: 100.0,
                  child: TextField(key: textField1, scrollPadding: const EdgeInsets.all(200.0)),
                ),
                const SizedBox(
                  height: 450.0, // 50.0 smaller than the overall frame so that both
                  width: 650.0,  // textfields are always partially visible.
                ),
                SizedBox( // visible when scrollOffset = 50.0
                  height: 100.0,
                  width: 100.0,
                  child: TextField(key: textField2, scrollPadding: const EdgeInsets.all(200.0)),
                ),
              ],
            ),
          ),
        ),
      );
    }

    await tester.pumpWidget(buildFrame(Axis.vertical));
    await tester.enterText(find.byKey(textField1), '1'); // textfield1 is visible
    await tester.pumpAndSettle();
    await tester.enterText(find.byKey(textField2), '2'); //scroll textField2 into view
    await tester.pumpAndSettle();
    await tester.enterText(find.byKey(textField1), '3'); //scroll textField1 back into view
    await tester.pumpAndSettle();

    expect(minOffset, 0.0);
    expect(maxOffset, 50.0);

    minOffset = null;
    maxOffset = null;

    await tester.pumpWidget(buildFrame(Axis.horizontal));
    await tester.enterText(find.byKey(textField1), '1'); // textfield1 is visible
    await tester.pumpAndSettle();
    await tester.enterText(find.byKey(textField2), '2'); //scroll textField2 into view
    await tester.pumpAndSettle();
    await tester.enterText(find.byKey(textField1), '3'); //scroll textField1 back into view
    await tester.pumpAndSettle();

    expect(minOffset, 0.0);
    expect(maxOffset, 50.0);
  });
3583 3584 3585 3586 3587 3588 3589 3590 3591 3592 3593 3594 3595 3596 3597

  testWidgets('onTap is called upon tap', (WidgetTester tester) async {
    int tapCount = 0;
    await tester.pumpWidget(
      overlay(
        child: TextField(
          onTap: () {
            tapCount += 1;
          },
        ),
      ),
    );

    expect(tapCount, 0);
    await tester.tap(find.byType(TextField));
3598 3599
    // Wait a bit so they're all single taps and not double taps.
    await tester.pump(const Duration(milliseconds: 300));
3600
    await tester.tap(find.byType(TextField));
3601
    await tester.pump(const Duration(milliseconds: 300));
3602
    await tester.tap(find.byType(TextField));
3603
    await tester.pump(const Duration(milliseconds: 300));
3604 3605 3606 3607 3608 3609 3610 3611 3612 3613 3614 3615 3616 3617 3618 3619 3620 3621 3622 3623 3624 3625
    expect(tapCount, 3);
  });

  testWidgets('onTap is not called, field is disabled', (WidgetTester tester) async {
    int tapCount = 0;
    await tester.pumpWidget(
      overlay(
        child: TextField(
          enabled: false,
          onTap: () {
            tapCount += 1;
          },
        ),
      ),
    );

    expect(tapCount, 0);
    await tester.tap(find.byType(TextField));
    await tester.tap(find.byType(TextField));
    await tester.tap(find.byType(TextField));
    expect(tapCount, 0);
  });
3626

3627 3628 3629 3630 3631 3632 3633 3634 3635 3636 3637 3638 3639 3640 3641 3642 3643 3644 3645 3646 3647 3648 3649 3650 3651 3652 3653 3654 3655 3656 3657 3658 3659 3660 3661 3662 3663 3664 3665 3666 3667 3668 3669 3670 3671 3672 3673 3674 3675 3676 3677 3678 3679 3680 3681 3682 3683 3684 3685 3686 3687 3688 3689 3690 3691 3692 3693 3694 3695 3696 3697 3698 3699 3700
  testWidgets('Includes cursor for TextField', (WidgetTester tester) async {
    // This is a regression test for https://github.com/flutter/flutter/issues/24612

    Widget buildFrame({
      double stepWidth,
      double cursorWidth,
      TextAlign textAlign,
    }) {
      return MaterialApp(
        home: Scaffold(
          body: Center(
            child: Column(
              mainAxisAlignment: MainAxisAlignment.center,
              children: <Widget>[
                IntrinsicWidth(
                  stepWidth: stepWidth,
                  child: TextField(
                    textAlign: textAlign,
                    cursorWidth: cursorWidth,
                  ),
                ),
              ],
            ),
          ),
        ),
      );
    }

    // A cursor of default size doesn't cause the TextField to increase its
    // width.
    const String text = '1234';
    double stepWidth = 80.0;
    await tester.pumpWidget(buildFrame(
      stepWidth: 80.0,
      cursorWidth: 2.0,
      textAlign: TextAlign.left,
    ));
    await tester.enterText(find.byType(TextField), text);
    await tester.pumpAndSettle();
    expect(tester.getSize(find.byType(TextField)).width, stepWidth);

    // A wide cursor is counted in the width of the text and causes the
    // TextField to increase to twice the stepWidth.
    await tester.pumpWidget(buildFrame(
      stepWidth: stepWidth,
      cursorWidth: 18.0,
      textAlign: TextAlign.left,
    ));
    await tester.enterText(find.byType(TextField), text);
    await tester.pumpAndSettle();
    expect(tester.getSize(find.byType(TextField)).width, 2 * stepWidth);

    // A null stepWidth causes the TextField to perfectly wrap the text plus
    // the cursor regardless of alignment.
    stepWidth = null;
    const double WIDTH_OF_CHAR = 16.0;
    await tester.pumpWidget(buildFrame(
      stepWidth: stepWidth,
      cursorWidth: 18.0,
      textAlign: TextAlign.left,
    ));
    await tester.enterText(find.byType(TextField), text);
    await tester.pumpAndSettle();
    expect(tester.getSize(find.byType(TextField)).width, WIDTH_OF_CHAR * text.length + 18.0);
    await tester.pumpWidget(buildFrame(
      stepWidth: stepWidth,
      cursorWidth: 18.0,
      textAlign: TextAlign.right,
    ));
    await tester.enterText(find.byType(TextField), text);
    await tester.pumpAndSettle();
    expect(tester.getSize(find.byType(TextField)).width, WIDTH_OF_CHAR * text.length + 18.0);
  });

3701 3702 3703 3704 3705 3706 3707 3708 3709 3710 3711 3712 3713 3714 3715 3716 3717 3718 3719 3720 3721 3722 3723 3724 3725 3726 3727 3728 3729 3730 3731 3732 3733 3734 3735 3736 3737 3738 3739 3740 3741 3742 3743 3744 3745 3746 3747 3748 3749 3750
  testWidgets('TextField style is merged with theme', (WidgetTester tester) async {
    // Regression test for https://github.com/flutter/flutter/issues/23994

    final ThemeData themeData = ThemeData(
      textTheme: TextTheme(
        subhead: TextStyle(
          color: Colors.blue[500],
        ),
      ),
    );

    Widget buildFrame(TextStyle style) {
      return MaterialApp(
        theme: themeData,
        home: Material(
          child: Center(
            child: TextField(
              style: style,
            ),
          ),
        ),
      );
    }

    // Empty TextStyle is overridden by theme
    await tester.pumpWidget(buildFrame(const TextStyle()));
    EditableText editableText = tester.widget(find.byType(EditableText));
    expect(editableText.style.color, themeData.textTheme.subhead.color);
    expect(editableText.style.background, themeData.textTheme.subhead.background);
    expect(editableText.style.shadows, themeData.textTheme.subhead.shadows);
    expect(editableText.style.decoration, themeData.textTheme.subhead.decoration);
    expect(editableText.style.locale, themeData.textTheme.subhead.locale);
    expect(editableText.style.wordSpacing, themeData.textTheme.subhead.wordSpacing);

    // Properties set on TextStyle override theme
    const Color setColor = Colors.red;
    await tester.pumpWidget(buildFrame(const TextStyle(color: setColor)));
    editableText = tester.widget(find.byType(EditableText));
    expect(editableText.style.color, setColor);

    // inherit: false causes nothing to be merged in from theme
    await tester.pumpWidget(buildFrame(const TextStyle(
      fontSize: 24.0,
      textBaseline: TextBaseline.alphabetic,
      inherit: false,
    )));
    editableText = tester.widget(find.byType(EditableText));
    expect(editableText.style.color, isNull);
  });

3751 3752 3753 3754 3755 3756 3757 3758 3759 3760 3761 3762 3763 3764 3765 3766 3767 3768 3769 3770 3771 3772 3773 3774 3775 3776 3777 3778 3779 3780
  testWidgets('style enforces required fields', (WidgetTester tester) async {
    Widget buildFrame(TextStyle style) {
      return MaterialApp(
        home: Material(
          child: TextField(
            style: style,
          ),
        ),
      );
    }

    await tester.pumpWidget(buildFrame(const TextStyle(
      inherit: false,
      fontSize: 12.0,
      textBaseline: TextBaseline.alphabetic,
    )));
    expect(tester.takeException(), isNull);

    // With inherit not set to false, will pickup required fields from theme
    await tester.pumpWidget(buildFrame(const TextStyle(
      fontSize: 12.0,
    )));
    expect(tester.takeException(), isNull);

    await tester.pumpWidget(buildFrame(const TextStyle(
      inherit: false,
      fontSize: 12.0,
    )));
    expect(tester.takeException(), isNotNull);
  });
3781 3782 3783 3784 3785 3786 3787 3788 3789 3790 3791 3792 3793 3794 3795 3796 3797 3798 3799 3800 3801 3802 3803 3804 3805 3806 3807 3808 3809 3810 3811 3812 3813 3814 3815 3816 3817 3818 3819 3820 3821 3822 3823 3824 3825 3826 3827 3828 3829 3830 3831 3832 3833 3834 3835 3836 3837 3838 3839 3840 3841 3842 3843 3844 3845 3846 3847 3848 3849 3850 3851 3852 3853 3854 3855 3856 3857 3858 3859 3860 3861 3862 3863 3864 3865 3866 3867 3868 3869 3870 3871 3872 3873 3874 3875 3876 3877 3878 3879 3880 3881 3882 3883 3884 3885 3886 3887 3888 3889 3890 3891 3892 3893 3894 3895 3896 3897 3898 3899 3900 3901 3902 3903 3904 3905 3906 3907 3908 3909 3910 3911 3912 3913 3914 3915 3916 3917 3918 3919 3920 3921 3922 3923 3924 3925 3926 3927 3928 3929 3930 3931 3932 3933 3934 3935 3936 3937 3938 3939 3940 3941 3942 3943 3944 3945 3946 3947 3948 3949 3950 3951 3952 3953 3954 3955 3956 3957 3958 3959 3960 3961 3962 3963 3964 3965 3966 3967 3968 3969 3970 3971 3972 3973 3974 3975 3976 3977 3978 3979 3980 3981 3982 3983 3984 3985 3986 3987 3988 3989 3990 3991 3992 3993 3994 3995 3996 3997 3998 3999 4000 4001 4002 4003 4004 4005 4006 4007 4008 4009 4010 4011 4012 4013 4014 4015 4016 4017 4018 4019 4020 4021 4022 4023 4024 4025 4026 4027 4028 4029 4030 4031 4032 4033 4034 4035 4036 4037 4038 4039 4040 4041 4042 4043 4044 4045 4046 4047 4048 4049 4050 4051 4052 4053 4054 4055 4056 4057 4058 4059 4060 4061 4062 4063 4064 4065 4066 4067 4068 4069 4070 4071 4072 4073 4074 4075 4076 4077 4078 4079 4080 4081 4082 4083 4084 4085 4086 4087 4088 4089 4090 4091 4092 4093 4094 4095 4096 4097 4098 4099 4100 4101 4102 4103 4104 4105 4106 4107 4108 4109 4110 4111 4112 4113 4114 4115 4116 4117 4118 4119 4120 4121 4122 4123 4124 4125 4126 4127 4128 4129 4130 4131 4132 4133 4134 4135 4136 4137 4138 4139 4140 4141 4142 4143 4144 4145

  testWidgets(
    'tap moves cursor to the edge of the word it tapped on (iOS)',
    (WidgetTester tester) async {
      final TextEditingController controller = TextEditingController(
        text: 'Atwater Peel Sherbrooke Bonaventure',
      );
      await tester.pumpWidget(
        MaterialApp(
          theme: ThemeData(platform: TargetPlatform.iOS),
          home: Material(
            child: Center(
              child: TextField(
                controller: controller,
              ),
            ),
          ),
        ),
      );

      final Offset textfieldStart = tester.getTopLeft(find.byType(TextField));

      await tester.tapAt(textfieldStart + const Offset(50.0, 5.0));
      await tester.pump();

      // We moved the cursor.
      expect(
        controller.selection,
        const TextSelection.collapsed(offset: 7, affinity: TextAffinity.upstream),
      );

      // But don't trigger the toolbar.
      expect(find.byType(CupertinoButton), findsNothing);
    },
  );

  testWidgets(
    'tap moves cursor to the position tapped (Android)',
    (WidgetTester tester) async {
      final TextEditingController controller = TextEditingController(
        text: 'Atwater Peel Sherbrooke Bonaventure',
      );
      await tester.pumpWidget(
        MaterialApp(
          home: Material(
            child: Center(
              child: TextField(
                controller: controller,
              ),
            ),
          ),
        ),
      );

      final Offset textfieldStart = tester.getTopLeft(find.byType(TextField));

      await tester.tapAt(textfieldStart + const Offset(50.0, 5.0));
      await tester.pump();

      // We moved the cursor.
      expect(
        controller.selection,
        const TextSelection.collapsed(offset: 3),
      );

      // But don't trigger the toolbar.
      expect(find.byType(FlatButton), findsNothing);
    },
  );

  testWidgets(
    'two slow taps do not trigger a word selection (iOS)',
    (WidgetTester tester) async {
      final TextEditingController controller = TextEditingController(
        text: 'Atwater Peel Sherbrooke Bonaventure',
      );
      await tester.pumpWidget(
        MaterialApp(
          theme: ThemeData(platform: TargetPlatform.iOS),
          home: Material(
            child: Center(
              child: TextField(
                controller: controller,
              ),
            ),
          ),
        ),
      );

      final Offset textfieldStart = tester.getTopLeft(find.byType(TextField));

      await tester.tapAt(textfieldStart + const Offset(50.0, 5.0));
      await tester.pump(const Duration(milliseconds: 500));
      await tester.tapAt(textfieldStart + const Offset(50.0, 5.0));
      await tester.pump();

      // Plain collapsed selection.
      expect(
        controller.selection,
        const TextSelection.collapsed(offset: 7, affinity: TextAffinity.upstream),
      );

      // No toolbar.
      expect(find.byType(CupertinoButton), findsNothing);
    },
  );

  testWidgets(
    'double tap selects word and first tap of double tap moves cursor (iOS)',
    (WidgetTester tester) async {
      final TextEditingController controller = TextEditingController(
        text: 'Atwater Peel Sherbrooke Bonaventure',
      );
      await tester.pumpWidget(
        MaterialApp(
          theme: ThemeData(platform: TargetPlatform.iOS),
          home: Material(
            child: Center(
              child: TextField(
                controller: controller,
              ),
            ),
          ),
        ),
      );

      final Offset textfieldStart = tester.getTopLeft(find.byType(TextField));

      // This tap just puts the cursor somewhere different than where the double
      // tap will occur to test that the double tap moves the existing cursor first.
      await tester.tapAt(textfieldStart + const Offset(50.0, 5.0));
      await tester.pump(const Duration(milliseconds: 500));

      await tester.tapAt(textfieldStart + const Offset(150.0, 5.0));
      await tester.pump(const Duration(milliseconds: 50));
      // First tap moved the cursor.
      expect(
        controller.selection,
        const TextSelection.collapsed(offset: 8, affinity: TextAffinity.downstream),
      );
      await tester.tapAt(textfieldStart + const Offset(150.0, 5.0));
      await tester.pump();

      // Second tap selects the word around the cursor.
      expect(
        controller.selection,
        const TextSelection(baseOffset: 8, extentOffset: 12),
      );

      // Selected text shows 3 toolbar buttons.
      expect(find.byType(CupertinoButton), findsNWidgets(3));
    },
  );

  testWidgets(
    'double tap selects word and first tap of double tap moves cursor and shows toolbar (Android)',
    (WidgetTester tester) async {
      final TextEditingController controller = TextEditingController(
        text: 'Atwater Peel Sherbrooke Bonaventure',
      );
      await tester.pumpWidget(
        MaterialApp(
          home: Material(
            child: Center(
              child: TextField(
                controller: controller,
              ),
            ),
          ),
        ),
      );

      final Offset textfieldStart = tester.getTopLeft(find.byType(TextField));

      // This tap just puts the cursor somewhere different than where the double
      // tap will occur to test that the double tap moves the existing cursor first.
      await tester.tapAt(textfieldStart + const Offset(50.0, 5.0));
      await tester.pump(const Duration(milliseconds: 500));

      await tester.tapAt(textfieldStart + const Offset(150.0, 5.0));
      await tester.pump(const Duration(milliseconds: 50));
      // First tap moved the cursor.
      expect(
        controller.selection,
        const TextSelection.collapsed(offset: 9),
      );
      await tester.tapAt(textfieldStart + const Offset(150.0, 5.0));
      await tester.pump();

      // Second tap selects the word around the cursor.
      expect(
        controller.selection,
        const TextSelection(baseOffset: 8, extentOffset: 12),
      );

      // Selected text shows 3 toolbar buttons.
      expect(find.byType(FlatButton), findsNWidgets(3));
    },
  );

  testWidgets(
    'double tap hold selects word (iOS)',
    (WidgetTester tester) async {
      final TextEditingController controller = TextEditingController(
        text: 'Atwater Peel Sherbrooke Bonaventure',
      );
      await tester.pumpWidget(
        MaterialApp(
          theme: ThemeData(platform: TargetPlatform.iOS),
          home: Material(
            child: Center(
              child: TextField(
                controller: controller,
              ),
            ),
          ),
        ),
      );

      final Offset textfieldStart = tester.getTopLeft(find.byType(TextField));

      await tester.tapAt(textfieldStart + const Offset(150.0, 5.0));
      await tester.pump(const Duration(milliseconds: 50));
      final TestGesture gesture =
         await tester.startGesture(textfieldStart + const Offset(150.0, 5.0));
      // Hold the press.
      await tester.pump(const Duration(milliseconds: 500));

      expect(
        controller.selection,
        const TextSelection(baseOffset: 8, extentOffset: 12),
      );

      // Selected text shows 3 toolbar buttons.
      expect(find.byType(CupertinoButton), findsNWidgets(3));

      await gesture.up();
      await tester.pump();

      // Still selected.
      expect(
        controller.selection,
        const TextSelection(baseOffset: 8, extentOffset: 12),
      );
      expect(find.byType(CupertinoButton), findsNWidgets(3));
    },
  );

  testWidgets(
    'tap after a double tap select is not affected (iOS)',
    (WidgetTester tester) async {
      final TextEditingController controller = TextEditingController(
        text: 'Atwater Peel Sherbrooke Bonaventure',
      );
      await tester.pumpWidget(
        MaterialApp(
          theme: ThemeData(platform: TargetPlatform.iOS),
          home: Material(
            child: Center(
              child: TextField(
                controller: controller,
              ),
            ),
          ),
        ),
      );

      final Offset textfieldStart = tester.getTopLeft(find.byType(TextField));

      await tester.tapAt(textfieldStart + const Offset(150.0, 5.0));
      await tester.pump(const Duration(milliseconds: 50));
      // First tap moved the cursor.
      expect(
        controller.selection,
        const TextSelection.collapsed(offset: 8, affinity: TextAffinity.downstream),
      );
      await tester.tapAt(textfieldStart + const Offset(150.0, 5.0));
      await tester.pump(const Duration(milliseconds: 500));

      await tester.tapAt(textfieldStart + const Offset(100.0, 5.0));
      await tester.pump();

      // Plain collapsed selection at the edge of first word. In iOS 12, the
      // the first tap after a double tap ends up putting the cursor at where
      // you tapped instead of the edge like every other single tap. This is
      // likely a bug in iOS 12 and not present in other versions.
      expect(
        controller.selection,
        const TextSelection.collapsed(offset: 7, affinity: TextAffinity.upstream),
      );

      // No toolbar.
      expect(find.byType(CupertinoButton), findsNothing);
    },
  );

  testWidgets(
    'long press moves cursor to the exact long press position and shows toolbar (iOS)',
    (WidgetTester tester) async {
      final TextEditingController controller = TextEditingController(
        text: 'Atwater Peel Sherbrooke Bonaventure',
      );
      await tester.pumpWidget(
        MaterialApp(
          theme: ThemeData(platform: TargetPlatform.iOS),
          home: Material(
            child: Center(
              child: TextField(
                controller: controller,
              ),
            ),
          ),
        ),
      );

      final Offset textfieldStart = tester.getTopLeft(find.byType(TextField));

      await tester.longPressAt(textfieldStart + const Offset(50.0, 5.0));
      await tester.pump();

      // Collapsed cursor for iOS long press.
      expect(
        controller.selection,
        const TextSelection.collapsed(offset: 3),
      );

      // Collapsed toolbar shows 2 buttons.
      expect(find.byType(CupertinoButton), findsNWidgets(2));
    },
  );

  testWidgets(
    'long press selects word and shows toolbar (Android)',
    (WidgetTester tester) async {
      final TextEditingController controller = TextEditingController(
        text: 'Atwater Peel Sherbrooke Bonaventure',
      );
      await tester.pumpWidget(
        MaterialApp(
          home: Material(
            child: Center(
              child: TextField(
                controller: controller,
              ),
            ),
          ),
        ),
      );

      final Offset textfieldStart = tester.getTopLeft(find.byType(TextField));

      await tester.longPressAt(textfieldStart + const Offset(50.0, 5.0));
      await tester.pump();

      expect(
        controller.selection,
        const TextSelection(baseOffset: 0, extentOffset: 7),
      );

      // Collapsed toolbar shows 3 buttons.
      expect(find.byType(FlatButton), findsNWidgets(3));
    },
  );

  testWidgets(
4146
    'long press tap cannot initiate a double tap (iOS)',
4147 4148 4149 4150 4151 4152 4153 4154 4155 4156 4157 4158 4159 4160 4161 4162 4163 4164 4165 4166 4167 4168 4169 4170 4171 4172 4173 4174 4175 4176 4177 4178 4179 4180 4181 4182 4183
    (WidgetTester tester) async {
      final TextEditingController controller = TextEditingController(
        text: 'Atwater Peel Sherbrooke Bonaventure',
      );
      await tester.pumpWidget(
        MaterialApp(
          theme: ThemeData(platform: TargetPlatform.iOS),
          home: Material(
            child: Center(
              child: TextField(
                controller: controller,
              ),
            ),
          ),
        ),
      );

      final Offset textfieldStart = tester.getTopLeft(find.byType(TextField));

      await tester.longPressAt(textfieldStart + const Offset(50.0, 5.0));
      await tester.pump(const Duration(milliseconds: 50));

      await tester.tapAt(textfieldStart + const Offset(50.0, 5.0));
      await tester.pump();

      // We ended up moving the cursor to the edge of the same word and dismissed
      // the toolbar.
      expect(
        controller.selection,
        const TextSelection.collapsed(offset: 7, affinity: TextAffinity.upstream),
      );

      // Collapsed toolbar shows 2 buttons.
      expect(find.byType(CupertinoButton), findsNothing);
    },
  );

4184 4185 4186 4187 4188 4189 4190 4191 4192 4193 4194 4195 4196 4197 4198 4199 4200 4201 4202 4203 4204 4205 4206 4207 4208 4209 4210 4211 4212 4213 4214 4215 4216 4217 4218 4219 4220 4221 4222 4223 4224 4225 4226 4227 4228 4229 4230 4231 4232 4233 4234 4235 4236 4237 4238 4239 4240 4241 4242 4243 4244 4245 4246 4247 4248 4249 4250 4251 4252 4253 4254 4255 4256 4257 4258 4259 4260 4261 4262 4263 4264 4265 4266 4267 4268 4269 4270 4271 4272 4273 4274 4275 4276 4277 4278 4279 4280 4281 4282 4283 4284 4285 4286 4287 4288 4289 4290 4291 4292 4293 4294 4295 4296 4297 4298 4299 4300 4301 4302 4303 4304 4305 4306 4307 4308 4309 4310 4311 4312 4313 4314 4315 4316 4317 4318 4319 4320 4321 4322 4323 4324 4325 4326 4327 4328
  testWidgets(
    'long press drag moves the cursor under the drag and shows toolbar on lift (iOS)',
    (WidgetTester tester) async {
      final TextEditingController controller = TextEditingController(
        text: 'Atwater Peel Sherbrooke Bonaventure',
      );
      await tester.pumpWidget(
        MaterialApp(
          theme: ThemeData(platform: TargetPlatform.iOS),
          home: Material(
            child: Center(
              child: TextField(
                controller: controller,
              ),
            ),
          ),
        ),
      );

      final Offset textfieldStart = tester.getTopLeft(find.byType(TextField));

      final TestGesture gesture =
          await tester.startGesture(textfieldStart + const Offset(50.0, 5.0));
      await tester.pump(const Duration(milliseconds: 500));

      // Long press on iOS shows collapsed selection cursor.
      expect(
        controller.selection,
        const TextSelection.collapsed(offset: 3, affinity: TextAffinity.downstream),
      );
      expect(find.byType(CupertinoButton), findsNothing);

      await gesture.moveBy(const Offset(50, 0));
      await tester.pump();

      // The selection position is now moved with the drag.
      expect(
        controller.selection,
        const TextSelection.collapsed(offset: 6, affinity: TextAffinity.downstream),
      );
      expect(find.byType(CupertinoButton), findsNothing);

      await gesture.moveBy(const Offset(50, 0));
      await tester.pump();

      // The selection position is now moved with the drag.
      expect(
        controller.selection,
        const TextSelection.collapsed(offset: 9, affinity: TextAffinity.downstream),
      );
      expect(find.byType(CupertinoButton), findsNothing);

      await gesture.up();
      await tester.pump();

      // The selection isn't affected by the gesture lift.
      expect(
        controller.selection,
        const TextSelection.collapsed(offset: 9, affinity: TextAffinity.downstream),
      );
      // The toolbar now shows up.
      expect(find.byType(CupertinoButton), findsNWidgets(2));
    },
  );

  testWidgets('long press drag can edge scroll (iOS)', (WidgetTester tester) async {
    final TextEditingController controller = TextEditingController(
        text: 'Atwater Peel Sherbrooke Bonaventure Angrignon Peel Côte-des-Neiges',
      );
      await tester.pumpWidget(
        MaterialApp(
          theme: ThemeData(platform: TargetPlatform.iOS),
          home: Material(
            child: Center(
              child: TextField(
                controller: controller,
                maxLines: 1,
              ),
            ),
          ),
        ),
      );

      final RenderEditable renderEditable = findRenderEditable(tester);

      List<TextSelectionPoint> lastCharEndpoint = renderEditable.getEndpointsForSelection(
        const TextSelection.collapsed(offset: 66), // Last character's position.
      );

      expect(lastCharEndpoint.length, 1);
      // Just testing the test and making sure that the last character is off
      // the right side of the screen.
      expect(lastCharEndpoint[0].point.dx, 1056);

      final Offset textfieldStart = tester.getTopLeft(find.byType(TextField));

      final TestGesture gesture =
          await tester.startGesture(textfieldStart + const Offset(300, 5));
      await tester.pump(const Duration(milliseconds: 500));

      expect(
        controller.selection,
        const TextSelection.collapsed(offset: 19, affinity: TextAffinity.upstream),
      );
      expect(find.byType(CupertinoButton), findsNothing);

      await gesture.moveBy(const Offset(600, 0));
      // To the edge of the screen basically.
      await tester.pump();
      expect(
        controller.selection,
        const TextSelection.collapsed(offset: 56, affinity: TextAffinity.downstream),
      );
      // Keep moving out.
      await gesture.moveBy(const Offset(1, 0));
      await tester.pump();
      expect(
        controller.selection,
        const TextSelection.collapsed(offset: 62, affinity: TextAffinity.downstream),
      );
      await gesture.moveBy(const Offset(1, 0));
      await tester.pump();
      expect(
        controller.selection,
        const TextSelection.collapsed(offset: 66, affinity: TextAffinity.upstream),
      ); // We're at the edge now.
      expect(find.byType(CupertinoButton), findsNothing);

      await gesture.up();
      await tester.pump();

      // The selection isn't affected by the gesture lift.
      expect(
        controller.selection,
        const TextSelection.collapsed(offset: 66, affinity: TextAffinity.upstream),
      );
      // The toolbar now shows up.
      expect(find.byType(CupertinoButton), findsNWidgets(2));

      lastCharEndpoint = renderEditable.getEndpointsForSelection(
        const TextSelection.collapsed(offset: 66), // Last character's position.
      );

      expect(lastCharEndpoint.length, 1);
      // The last character is now on screen.
4329
      expect(lastCharEndpoint[0].point.dx, moreOrLessEquals(798.6666870117188));
4330 4331 4332 4333 4334 4335

      final List<TextSelectionPoint> firstCharEndpoint = renderEditable.getEndpointsForSelection(
        const TextSelection.collapsed(offset: 0), // First character's position.
      );
      expect(firstCharEndpoint.length, 1);
      // The first character is now offscreen to the left.
4336
      expect(firstCharEndpoint[0].point.dx, moreOrLessEquals(-257.33331298828125));
4337 4338
  });

4339 4340 4341 4342 4343 4344 4345 4346 4347 4348 4349 4350 4351 4352 4353 4354 4355 4356 4357 4358 4359 4360 4361 4362 4363 4364 4365 4366 4367 4368 4369 4370 4371 4372 4373 4374 4375 4376 4377 4378 4379 4380 4381 4382 4383 4384 4385 4386 4387 4388 4389 4390 4391 4392 4393 4394 4395 4396 4397 4398 4399 4400 4401 4402 4403 4404 4405 4406 4407 4408 4409 4410 4411 4412 4413 4414 4415 4416 4417 4418 4419 4420 4421 4422 4423 4424 4425 4426 4427 4428 4429 4430 4431 4432 4433 4434 4435 4436 4437 4438 4439 4440 4441 4442 4443 4444 4445 4446 4447 4448 4449 4450 4451 4452 4453 4454 4455 4456 4457 4458 4459 4460 4461 4462 4463 4464 4465 4466 4467 4468 4469 4470 4471 4472 4473 4474 4475 4476 4477 4478 4479 4480 4481 4482 4483 4484 4485 4486 4487 4488 4489 4490 4491 4492 4493
  testWidgets(
    'long tap after a double tap select is not affected (iOS)',
    (WidgetTester tester) async {
      final TextEditingController controller = TextEditingController(
        text: 'Atwater Peel Sherbrooke Bonaventure',
      );
      await tester.pumpWidget(
        MaterialApp(
          theme: ThemeData(platform: TargetPlatform.iOS),
          home: Material(
            child: Center(
              child: TextField(
                controller: controller,
              ),
            ),
          ),
        ),
      );

      final Offset textfieldStart = tester.getTopLeft(find.byType(TextField));

      await tester.tapAt(textfieldStart + const Offset(150.0, 5.0));
      await tester.pump(const Duration(milliseconds: 50));
      // First tap moved the cursor to the beginning of the second word.
      expect(
        controller.selection,
        const TextSelection.collapsed(offset: 8, affinity: TextAffinity.downstream),
      );
      await tester.tapAt(textfieldStart + const Offset(150.0, 5.0));
      await tester.pump(const Duration(milliseconds: 500));

      await tester.longPressAt(textfieldStart + const Offset(100.0, 5.0));
      await tester.pump();

      // Plain collapsed selection at the exact tap position.
      expect(
        controller.selection,
        const TextSelection.collapsed(offset: 6),
      );

      // Long press toolbar.
      expect(find.byType(CupertinoButton), findsNWidgets(2));
    },
  );

  testWidgets(
    'double tap after a long tap is not affected (iOS)',
    (WidgetTester tester) async {
      final TextEditingController controller = TextEditingController(
        text: 'Atwater Peel Sherbrooke Bonaventure',
      );
      await tester.pumpWidget(
        MaterialApp(
          theme: ThemeData(platform: TargetPlatform.iOS),
          home: Material(
            child: Center(
              child: TextField(
                controller: controller,
              ),
            ),
          ),
        ),
      );

      final Offset textfieldStart = tester.getTopLeft(find.byType(TextField));

      await tester.longPressAt(textfieldStart + const Offset(50.0, 5.0));
      await tester.pump(const Duration(milliseconds: 50));

      await tester.tapAt(textfieldStart + const Offset(150.0, 5.0));
      await tester.pump(const Duration(milliseconds: 50));
      // First tap moved the cursor.
      expect(
        controller.selection,
        const TextSelection.collapsed(offset: 8, affinity: TextAffinity.downstream),
      );
      await tester.tapAt(textfieldStart + const Offset(150.0, 5.0));
      await tester.pump();

      // Double tap selection.
      expect(
        controller.selection,
        const TextSelection(baseOffset: 8, extentOffset: 12),
      );
      expect(find.byType(CupertinoButton), findsNWidgets(3));
    },
  );

  testWidgets(
    'double tap chains work (iOS)',
    (WidgetTester tester) async {
      final TextEditingController controller = TextEditingController(
        text: 'Atwater Peel Sherbrooke Bonaventure',
      );
      await tester.pumpWidget(
        MaterialApp(
          theme: ThemeData(platform: TargetPlatform.iOS),
          home: Material(
            child: Center(
              child: TextField(
                controller: controller,
              ),
            ),
          ),
        ),
      );

      final Offset textfieldStart = tester.getTopLeft(find.byType(TextField));

      await tester.tapAt(textfieldStart + const Offset(50.0, 5.0));
      await tester.pump(const Duration(milliseconds: 50));
      expect(
        controller.selection,
        const TextSelection.collapsed(offset: 7, affinity: TextAffinity.upstream),
      );
      await tester.tapAt(textfieldStart + const Offset(50.0, 5.0));
      await tester.pump(const Duration(milliseconds: 50));
      expect(
        controller.selection,
        const TextSelection(baseOffset: 0, extentOffset: 7),
      );
      expect(find.byType(CupertinoButton), findsNWidgets(3));

      // Double tap selecting the same word somewhere else is fine.
      await tester.tapAt(textfieldStart + const Offset(100.0, 5.0));
      await tester.pump(const Duration(milliseconds: 50));
      // First tap moved the cursor.
      expect(
        controller.selection,
        const TextSelection.collapsed(offset: 7, affinity: TextAffinity.upstream),
      );
      await tester.tapAt(textfieldStart + const Offset(100.0, 5.0));
      await tester.pump(const Duration(milliseconds: 50));
      expect(
        controller.selection,
        const TextSelection(baseOffset: 0, extentOffset: 7),
      );
      expect(find.byType(CupertinoButton), findsNWidgets(3));

      await tester.tapAt(textfieldStart + const Offset(150.0, 5.0));
      await tester.pump(const Duration(milliseconds: 50));
      // First tap moved the cursor.
      expect(
        controller.selection,
        const TextSelection.collapsed(offset: 8, affinity: TextAffinity.downstream),
      );
      await tester.tapAt(textfieldStart + const Offset(150.0, 5.0));
      await tester.pump(const Duration(milliseconds: 50));
      expect(
        controller.selection,
        const TextSelection(baseOffset: 8, extentOffset: 12),
      );
      expect(find.byType(CupertinoButton), findsNWidgets(3));
    },
  );
4494

4495 4496 4497 4498 4499 4500 4501 4502 4503 4504 4505 4506 4507 4508 4509 4510 4511 4512 4513 4514 4515 4516 4517 4518 4519 4520 4521 4522 4523 4524 4525 4526 4527 4528 4529 4530 4531 4532 4533 4534 4535 4536 4537 4538 4539 4540 4541 4542 4543 4544 4545 4546 4547 4548 4549 4550 4551 4552 4553 4554 4555 4556 4557 4558
  testWidgets('force press does not select a word on (android)', (WidgetTester tester) async {
    debugDefaultTargetPlatformOverride = TargetPlatform.android;
    final TextEditingController controller = TextEditingController(
      text: 'Atwater Peel Sherbrooke Bonaventure',
    );
    await tester.pumpWidget(
      MaterialApp(
        home: Material(
          child: TextField(
            controller: controller,
          ),
        ),
      ),
    );

    final Offset textfieldStart = tester.getTopLeft(find.byType(TextField));

    const int pointerValue = 1;
    final TestGesture gesture =
    await tester.startGesture(textfieldStart + const Offset(150.0, 5.0));
    await gesture.updateWithCustomEvent(PointerMoveEvent(pointer: pointerValue, position: textfieldStart + const Offset(150.0, 5.0), pressure: 0.5, pressureMin: 0, pressureMax: 1));

    // We don't want this gesture to select any word on Android.
    expect(controller.selection, const TextSelection.collapsed(offset: -1));

    await gesture.up();
    await tester.pumpAndSettle();
    expect(find.byType(FlatButton), findsNothing);
    debugDefaultTargetPlatformOverride = null;
  });

  testWidgets('force press selects word (iOS)', (WidgetTester tester) async {
    debugDefaultTargetPlatformOverride = TargetPlatform.iOS;
    final TextEditingController controller = TextEditingController(
      text: 'Atwater Peel Sherbrooke Bonaventure',
    );
    await tester.pumpWidget(
      CupertinoApp(
        home: Center(
          child: CupertinoTextField(
            controller: controller,
          ),
        ),
      ),
    );

    final Offset textfieldStart = tester.getTopLeft(find.byType(CupertinoTextField));

    const int pointerValue = 1;
    final TestGesture gesture =
    await tester.startGesture(textfieldStart + const Offset(150.0, 5.0));
    await gesture.updateWithCustomEvent(PointerMoveEvent(pointer: pointerValue, position: textfieldStart + const Offset(150.0, 5.0), pressure: 0.5, pressureMin: 0, pressureMax: 1));
    // We expect the force press to select a word at the given location.
    expect(
      controller.selection,
      const TextSelection(baseOffset: 8, extentOffset: 12),
    );

    await gesture.up();
    await tester.pumpAndSettle();
    expect(find.byType(CupertinoButton), findsNWidgets(3));
    debugDefaultTargetPlatformOverride = null;
  });

4559 4560 4561 4562 4563 4564 4565 4566 4567 4568 4569 4570 4571 4572 4573 4574 4575 4576 4577 4578 4579 4580 4581 4582 4583 4584 4585 4586 4587 4588 4589 4590 4591 4592 4593 4594 4595 4596 4597 4598 4599 4600 4601 4602 4603 4604 4605 4606 4607 4608 4609 4610 4611 4612 4613 4614 4615 4616 4617 4618 4619 4620 4621 4622
  testWidgets('default TextField debugFillProperties', (WidgetTester tester) async {
    final DiagnosticPropertiesBuilder builder = DiagnosticPropertiesBuilder();

    const TextField().debugFillProperties(builder);

    final List<String> description = builder.properties
      .where((DiagnosticsNode node) => !node.isFiltered(DiagnosticLevel.info))
      .map((DiagnosticsNode node) => node.toString()).toList();

    expect(description, <String>[]);
  });

  testWidgets('TextField implements debugFillProperties', (WidgetTester tester) async {
    final DiagnosticPropertiesBuilder builder = DiagnosticPropertiesBuilder();

    // Not checking controller, inputFormatters, focusNode
    const TextField(
      decoration: InputDecoration(labelText: 'foo'),
      keyboardType: TextInputType.text,
      textInputAction: TextInputAction.done,
      textCapitalization: TextCapitalization.none,
      style: TextStyle(color: Color(0xff00ff00)),
      textAlign: TextAlign.end,
      textDirection: TextDirection.ltr,
      autofocus: true,
      obscureText: true,
      autocorrect: false,
      maxLines: 10,
      maxLength: 100,
      maxLengthEnforced: false,
      enabled: false,
      cursorWidth: 1.0,
      cursorRadius: Radius.zero,
      cursorColor: Color(0xff00ff00),
      keyboardAppearance: Brightness.dark,
      scrollPadding: EdgeInsets.zero,
      enableInteractiveSelection: false,
    ).debugFillProperties(builder);

    final List<String> description = builder.properties
      .where((DiagnosticsNode node) => !node.isFiltered(DiagnosticLevel.info))
      .map((DiagnosticsNode node) => node.toString()).toList();

    expect(description, <String>[
      'enabled: false',
      'decoration: InputDecoration(labelText: "foo")',
      'style: TextStyle(inherit: true, color: Color(0xff00ff00))',
      'autofocus: true',
      'obscureText: true',
      'autocorrect: false',
      'maxLines: 10',
      'maxLength: 100',
      'maxLength not enforced',
      'textInputAction: done',
      'textAlign: end',
      'textDirection: ltr',
      'cursorWidth: 1.0',
      'cursorRadius: Radius.circular(0.0)',
      'cursorColor: Color(0xff00ff00)',
      'keyboardAppearance: Brightness.dark',
      'scrollPadding: EdgeInsets.zero',
      'selection disabled'
    ]);
  });
4623
}