input_test.dart 25 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 6
import 'dart:async';

Adam Barth's avatar
Adam Barth committed
7 8
import 'package:flutter_test/flutter_test.dart';
import 'package:flutter/material.dart';
9
import 'package:flutter/rendering.dart';
10
import 'package:flutter/services.dart';
11

12
import 'mock_text_input.dart';
13

14 15 16 17
class MockClipboard {
  Object _clipboardData = <String, dynamic>{
    'text': null
  };
18

19 20 21
  Future<dynamic> handleJSONMessage(dynamic message) async {
    final String method = message['method'];
    final List<dynamic> args= message['args'];
22 23 24 25 26 27 28
    switch (method) {
      case 'Clipboard.getData':
        return _clipboardData;
      case 'Clipboard.setData':
        _clipboardData = args[0];
        break;
    }
29 30 31
  }
}

32
void main() {
33
  MockTextInput mockTextInput = new MockTextInput()..register();
34
  MockClipboard mockClipboard = new MockClipboard();
35
  PlatformMessages.setMockJSONMessageHandler('flutter/platform', mockClipboard.handleJSONMessage);
36

37 38 39 40 41 42 43 44
  const String kThreeLines =
    'First line of text is here abcdef ghijkl mnopqrst. ' +
    'Second line of text goes until abcdef ghijkl mnopq. ' +
    'Third line of stuff keeps going until abcdef ghijk. ';
  const String kFourLines =
    kThreeLines +
    'Fourth line won\'t display and ends at abcdef ghi. ';

45 46 47 48 49 50
  void updateEditingState(TextEditingState state) {
    mockTextInput.updateEditingState(state);
  }

  void enterText(String text) {
    mockTextInput.enterText(text);
51 52
  }

53 54 55 56 57 58
  Future<Null> showKeyboard(WidgetTester tester) async {
    RawInputState editable = tester.state(find.byType(RawInput).first);
    editable.requestKeyboard();
    await tester.pump();
  }

59 60 61
  // Returns the first RenderEditable.
  RenderEditable findRenderEditable(WidgetTester tester) {
    RenderObject root = tester.renderObject(find.byType(RawInput));
62 63
    expect(root, isNotNull);

64
    RenderEditable renderEditable;
65
    void recursiveFinder(RenderObject child) {
66 67
      if (child is RenderEditable) {
        renderEditable = child;
68 69 70 71 72
        return;
      }
      child.visitChildren(recursiveFinder);
    }
    root.visitChildren(recursiveFinder);
73 74
    expect(renderEditable, isNotNull);
    return renderEditable;
75 76 77
  }

  Point textOffsetToPosition(WidgetTester tester, int offset) {
78 79
    RenderEditable renderEditable = findRenderEditable(tester);
    List<TextSelectionPoint> endpoints = renderEditable.getEndpointsForSelection(
80 81 82 83 84
        new TextSelection.collapsed(offset: offset));
    expect(endpoints.length, 1);
    return endpoints[0].point + new Offset(0.0, -2.0);
  }

85
  testWidgets('Editable text has consistent size', (WidgetTester tester) async {
86 87 88 89 90 91 92 93 94 95 96
    GlobalKey inputKey = new GlobalKey();
    InputValue inputValue = InputValue.empty;

    Widget builder() {
      return new Center(
        child: new Material(
          child: new Input(
            value: inputValue,
            key: inputKey,
            hintText: 'Placeholder',
            onChanged: (InputValue value) { inputValue = value; }
97
          )
98 99 100
        )
      );
    }
101

102
    await tester.pumpWidget(builder());
103
    await showKeyboard(tester);
104

105
    RenderBox findInputBox() => tester.renderObject(find.byKey(inputKey));
106

107 108
    RenderBox inputBox = findInputBox();
    Size emptyInputSize = inputBox.size;
109

110
    Future<Null> checkText(String testValue) async {
111
      enterText(testValue);
112
      await tester.idle();
113

114 115
      // Check that the onChanged event handler fired.
      expect(inputValue.text, equals(testValue));
116

117
      return await tester.pumpWidget(builder());
118
    }
119

120
    await checkText(' ');
121 122
    expect(findInputBox(), equals(inputBox));
    expect(inputBox.size, equals(emptyInputSize));
123

124
    await checkText('Test');
125 126
    expect(findInputBox(), equals(inputBox));
    expect(inputBox.size, equals(emptyInputSize));
127
  });
128

129
  testWidgets('Cursor blinks', (WidgetTester tester) async {
130
    GlobalKey inputKey = new GlobalKey();
131

132 133 134 135 136 137 138 139 140 141
    Widget builder() {
      return new Center(
        child: new Material(
          child: new Input(
            key: inputKey,
            hintText: 'Placeholder'
          )
        )
      );
    }
142

143
    await tester.pumpWidget(builder());
144
    await showKeyboard(tester);
145

146
    RawInputState editableText = tester.state(find.byType(RawInput));
147 148

    // Check that the cursor visibility toggles after each blink interval.
149
    Future<Null> checkCursorToggle() async {
150
      bool initialShowCursor = editableText.cursorCurrentlyVisible;
151
      await tester.pump(editableText.cursorBlinkInterval);
152
      expect(editableText.cursorCurrentlyVisible, equals(!initialShowCursor));
153
      await tester.pump(editableText.cursorBlinkInterval);
154
      expect(editableText.cursorCurrentlyVisible, equals(initialShowCursor));
155
      await tester.pump(editableText.cursorBlinkInterval ~/ 10);
156
      expect(editableText.cursorCurrentlyVisible, equals(initialShowCursor));
157
      await tester.pump(editableText.cursorBlinkInterval);
158
      expect(editableText.cursorCurrentlyVisible, equals(!initialShowCursor));
159
      await tester.pump(editableText.cursorBlinkInterval);
160 161
      expect(editableText.cursorCurrentlyVisible, equals(initialShowCursor));
    }
162

163
    await checkCursorToggle();
164

165
    // Try the test again with a nonempty EditableText.
166 167 168 169 170
    updateEditingState(new TextEditingState(
      text: 'X',
      selectionBase: 1,
      selectionExtent: 1,
    ));
171
    await checkCursorToggle();
172
  });
173

174
  testWidgets('hideText control test', (WidgetTester tester) async {
175 176 177 178 179 180 181 182 183
    GlobalKey inputKey = new GlobalKey();

    Widget builder() {
      return new Center(
        child: new Material(
          child: new Input(
            key: inputKey,
            hideText: true,
            hintText: 'Placeholder'
Adam Barth's avatar
Adam Barth committed
184
          )
185 186 187
        )
      );
    }
Adam Barth's avatar
Adam Barth committed
188

189
    await tester.pumpWidget(builder());
190
    await showKeyboard(tester);
Adam Barth's avatar
Adam Barth committed
191

192
    const String testValue = 'ABC';
193 194 195 196 197
    updateEditingState(new TextEditingState(
      text: testValue,
      selectionBase: testValue.length,
      selectionExtent: testValue.length,
    ));
Adam Barth's avatar
Adam Barth committed
198

199
    await tester.pump();
Adam Barth's avatar
Adam Barth committed
200
  });
201

202
  testWidgets('Can long press to select', (WidgetTester tester) async {
203 204 205 206 207 208 209 210 211 212 213 214 215 216
    GlobalKey inputKey = new GlobalKey();
    InputValue inputValue = InputValue.empty;

    Widget builder() {
      return new Overlay(
        initialEntries: <OverlayEntry>[
          new OverlayEntry(
            builder: (BuildContext context) {
              return new Center(
                child: new Material(
                  child: new Input(
                    value: inputValue,
                    key: inputKey,
                    onChanged: (InputValue value) { inputValue = value; }
217
                  )
218 219 220 221 222 223 224
                )
              );
            }
          )
        ]
      );
    }
225

226
    await tester.pumpWidget(builder());
227
    await showKeyboard(tester);
228

229 230
    String testValue = 'abc def ghi';
    enterText(testValue);
231
    await tester.idle();
232
    expect(inputValue.text, testValue);
233

234
    await tester.pumpWidget(builder());
235

236
    expect(inputValue.selection.isCollapsed, true);
237

238 239
    // Long press the 'e' to select 'def'.
    Point ePos = textOffsetToPosition(tester, testValue.indexOf('e'));
240 241 242 243
    TestGesture gesture = await tester.startGesture(ePos, pointer: 7);
    await tester.pump(const Duration(seconds: 2));
    await gesture.up();
    await tester.pump();
244

245 246 247
    // 'def' is selected.
    expect(inputValue.selection.baseOffset, testValue.indexOf('d'));
    expect(inputValue.selection.extentOffset, testValue.indexOf('f')+1);
248 249
  });

250
  testWidgets('Can drag handles to change selection', (WidgetTester tester) async {
251 252 253 254 255 256 257 258 259 260 261 262 263 264
    GlobalKey inputKey = new GlobalKey();
    InputValue inputValue = InputValue.empty;

    Widget builder() {
      return new Overlay(
        initialEntries: <OverlayEntry>[
          new OverlayEntry(
            builder: (BuildContext context) {
              return new Center(
                child: new Material(
                  child: new Input(
                    value: inputValue,
                    key: inputKey,
                    onChanged: (InputValue value) { inputValue = value; }
265
                  )
266 267 268 269 270 271 272
                )
              );
            }
          )
        ]
      );
    }
273

274
    await tester.pumpWidget(builder());
275
    await showKeyboard(tester);
276

277 278
    String testValue = 'abc def ghi';
    enterText(testValue);
279
    await tester.idle();
280

281
    await tester.pumpWidget(builder());
282

283 284
    // Long press the 'e' to select 'def'.
    Point ePos = textOffsetToPosition(tester, testValue.indexOf('e'));
285 286 287 288
    TestGesture gesture = await tester.startGesture(ePos, pointer: 7);
    await tester.pump(const Duration(seconds: 2));
    await gesture.up();
    await tester.pump();
289 290

    TextSelection selection = inputValue.selection;
291

292 293
    RenderEditable renderEditable = findRenderEditable(tester);
    List<TextSelectionPoint> endpoints = renderEditable.getEndpointsForSelection(
294 295 296 297 298 299 300 301
        selection);
    expect(endpoints.length, 2);

    // Drag the right handle 2 letters to the right.
    // Note: use a small offset because the endpoint is on the very corner
    // of the handle.
    Point handlePos = endpoints[1].point + new Offset(1.0, 1.0);
    Point newHandlePos = textOffsetToPosition(tester, selection.extentOffset+2);
302 303 304 305 306
    gesture = await tester.startGesture(handlePos, pointer: 7);
    await tester.pump();
    await gesture.moveTo(newHandlePos);
    await tester.pump();
    await gesture.up();
307
    await tester.pumpWidget(builder());
308 309 310 311 312 313 314

    expect(inputValue.selection.baseOffset, selection.baseOffset);
    expect(inputValue.selection.extentOffset, selection.extentOffset+2);

    // Drag the left handle 2 letters to the left.
    handlePos = endpoints[0].point + new Offset(-1.0, 1.0);
    newHandlePos = textOffsetToPosition(tester, selection.baseOffset-2);
315 316 317 318 319 320
    gesture = await tester.startGesture(handlePos, pointer: 7);
    await tester.pump();
    await gesture.moveTo(newHandlePos);
    await tester.pump();
    await gesture.up();
    await tester.pumpWidget(builder());
321 322 323

    expect(inputValue.selection.baseOffset, selection.baseOffset-2);
    expect(inputValue.selection.extentOffset, selection.extentOffset+2);
324 325
  });

326
  testWidgets('Can use selection toolbar', (WidgetTester tester) async {
327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349
    GlobalKey inputKey = new GlobalKey();
    InputValue inputValue = InputValue.empty;

    Widget builder() {
      return new Overlay(
        initialEntries: <OverlayEntry>[
          new OverlayEntry(
            builder: (BuildContext context) {
              return new Center(
                child: new Material(
                  child: new Input(
                    value: inputValue,
                    key: inputKey,
                    onChanged: (InputValue value) { inputValue = value; }
                  )
                )
              );
            }
          )
        ]
      );
    }

350
    await tester.pumpWidget(builder());
351
    await showKeyboard(tester);
352 353 354

    String testValue = 'abc def ghi';
    enterText(testValue);
355
    await tester.idle();
356
    await tester.pumpWidget(builder());
357

358
    // Tap the selection handle to bring up the "paste / select all" menu.
359 360
    await tester.tapAt(textOffsetToPosition(tester, testValue.indexOf('e')));
    await tester.pumpWidget(builder());
361 362
    RenderEditable renderEditable = findRenderEditable(tester);
    List<TextSelectionPoint> endpoints = renderEditable.getEndpointsForSelection(
363
        inputValue.selection);
364 365
    await tester.tapAt(endpoints[0].point + new Offset(1.0, 1.0));
    await tester.pumpWidget(builder());
366 367

    // SELECT ALL should select all the text.
368 369
    await tester.tap(find.text('SELECT ALL'));
    await tester.pumpWidget(builder());
370 371 372 373
    expect(inputValue.selection.baseOffset, 0);
    expect(inputValue.selection.extentOffset, testValue.length);

    // COPY should reset the selection.
374 375
    await tester.tap(find.text('COPY'));
    await tester.pumpWidget(builder());
376 377 378
    expect(inputValue.selection.isCollapsed, true);

    // Tap again to bring back the menu.
379 380
    await tester.tapAt(textOffsetToPosition(tester, testValue.indexOf('e')));
    await tester.pumpWidget(builder());
381 382
    renderEditable = findRenderEditable(tester);
    endpoints = renderEditable.getEndpointsForSelection(inputValue.selection);
383 384
    await tester.tapAt(endpoints[0].point + new Offset(1.0, 1.0));
    await tester.pumpWidget(builder());
385 386

    // PASTE right before the 'e'.
387 388
    await tester.tap(find.text('PASTE'));
    await tester.pumpWidget(builder());
389 390
    expect(inputValue.text, 'abc d${testValue}ef ghi');
  });
391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416

  testWidgets('Selection toolbar fades in', (WidgetTester tester) async {
    GlobalKey inputKey = new GlobalKey();
    InputValue inputValue = InputValue.empty;

    Widget builder() {
      return new Overlay(
        initialEntries: <OverlayEntry>[
          new OverlayEntry(
            builder: (BuildContext context) {
              return new Center(
                child: new Material(
                  child: new Input(
                    value: inputValue,
                    key: inputKey,
                    onChanged: (InputValue value) { inputValue = value; }
                  )
                )
              );
            }
          )
        ]
      );
    }

    await tester.pumpWidget(builder());
417
    await showKeyboard(tester);
418 419 420

    String testValue = 'abc def ghi';
    enterText(testValue);
421
    await tester.idle();
422 423 424 425 426
    await tester.pumpWidget(builder());

    // Tap the selection handle to bring up the "paste / select all" menu.
    await tester.tapAt(textOffsetToPosition(tester, testValue.indexOf('e')));
    await tester.pumpWidget(builder());
427 428
    RenderEditable renderEditable = findRenderEditable(tester);
    List<TextSelectionPoint> endpoints = renderEditable.getEndpointsForSelection(
429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446
        inputValue.selection);
    await tester.tapAt(endpoints[0].point + new Offset(1.0, 1.0));
    await tester.pumpWidget(builder());

    // Toolbar should fade in. Starting at 0% opacity.
    Element target = tester.element(find.text('SELECT ALL'));
    Opacity opacity = target.ancestorWidgetOfExactType(Opacity);
    expect(opacity, isNotNull);
    expect(opacity.opacity, equals(0.0));

    // Still fading in.
    await tester.pump(const Duration(milliseconds: 50));
    opacity = target.ancestorWidgetOfExactType(Opacity);
    expect(opacity.opacity, greaterThan(0.0));
    expect(opacity.opacity, lessThan(1.0));

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

448
  testWidgets('Multiline text will wrap up to maxLines', (WidgetTester tester) async {
449 450 451
    GlobalKey inputKey = new GlobalKey();
    InputValue inputValue = InputValue.empty;

452
    Widget builder(int maxLines) {
453 454 455 456 457 458
      return new Center(
        child: new Material(
          child: new Input(
            value: inputValue,
            key: inputKey,
            style: const TextStyle(color: Colors.black, fontSize: 34.0),
459
            maxLines: maxLines,
460 461 462 463 464 465 466
            hintText: 'Placeholder',
            onChanged: (InputValue value) { inputValue = value; }
          )
        )
      );
    }

467
    await tester.pumpWidget(builder(3));
468
    await showKeyboard(tester);
469 470 471 472 473 474

    RenderBox findInputBox() => tester.renderObject(find.byKey(inputKey));

    RenderBox inputBox = findInputBox();
    Size emptyInputSize = inputBox.size;

475
    enterText('No wrapping here.');
476
    await tester.idle();
477 478 479 480 481
    await tester.pumpWidget(builder(3));
    expect(findInputBox(), equals(inputBox));
    expect(inputBox.size, equals(emptyInputSize));

    enterText(kThreeLines);
482
    await tester.idle();
483
    await tester.pumpWidget(builder(3));
484 485 486
    expect(findInputBox(), equals(inputBox));
    expect(inputBox.size, greaterThan(emptyInputSize));

487 488 489 490
    Size threeLineInputSize = inputBox.size;

    // An extra line won't increase the size because we max at 3.
    enterText(kFourLines);
491
    await tester.idle();
492
    await tester.pumpWidget(builder(3));
493
    expect(findInputBox(), equals(inputBox));
494 495 496 497
    expect(inputBox.size, threeLineInputSize);

    // But now it will.
    enterText(kFourLines);
498
    await tester.idle();
499 500 501
    await tester.pumpWidget(builder(4));
    expect(findInputBox(), equals(inputBox));
    expect(inputBox.size, greaterThan(threeLineInputSize));
502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518
  });

  testWidgets('Can drag handles to change selection in multiline', (WidgetTester tester) async {
    GlobalKey inputKey = new GlobalKey();
    InputValue inputValue = InputValue.empty;

    Widget builder() {
      return new Overlay(
        initialEntries: <OverlayEntry>[
          new OverlayEntry(
            builder: (BuildContext context) {
              return new Center(
                child: new Material(
                  child: new Input(
                    value: inputValue,
                    key: inputKey,
                    style: const TextStyle(color: Colors.black, fontSize: 34.0),
519
                    maxLines: 3,
520 521 522 523 524 525 526 527 528 529 530
                    onChanged: (InputValue value) { inputValue = value; }
                  )
                )
              );
            }
          )
        ]
      );
    }

    await tester.pumpWidget(builder());
531
    await showKeyboard(tester);
532

533 534
    String testValue = kThreeLines;
    String cutValue = 'First line of stuff keeps going until abcdef ghijk. ';
535
    enterText(testValue);
536
    await tester.idle();
537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558

    await tester.pumpWidget(builder());

    // Check that the text spans multiple lines.
    Point firstPos = textOffsetToPosition(tester, testValue.indexOf('First'));
    Point secondPos = textOffsetToPosition(tester, testValue.indexOf('Second'));
    Point thirdPos = textOffsetToPosition(tester, testValue.indexOf('Third'));
    expect(firstPos.x, secondPos.x);
    expect(firstPos.x, thirdPos.x);
    expect(firstPos.y, lessThan(secondPos.y));
    expect(secondPos.y, lessThan(thirdPos.y));

    // Long press the 'n' in 'until' to select the word.
    Point untilPos = textOffsetToPosition(tester, testValue.indexOf('until')+1);
    TestGesture gesture = await tester.startGesture(untilPos, pointer: 7);
    await tester.pump(const Duration(seconds: 2));
    await gesture.up();
    await tester.pump();

    expect(inputValue.selection.baseOffset, 76);
    expect(inputValue.selection.extentOffset, 81);

559 560
    RenderEditable renderEditable = findRenderEditable(tester);
    List<TextSelectionPoint> endpoints = renderEditable.getEndpointsForSelection(
561 562 563 564 565 566 567 568 569 570 571
        inputValue.selection);
    expect(endpoints.length, 2);

    // Drag the right handle to the third line, just after 'Third'.
    Point handlePos = endpoints[1].point + new Offset(1.0, 1.0);
    Point newHandlePos = textOffsetToPosition(tester, testValue.indexOf('Third') + 5);
    gesture = await tester.startGesture(handlePos, pointer: 7);
    await tester.pump();
    await gesture.moveTo(newHandlePos);
    await tester.pump();
    await gesture.up();
572
    await tester.pumpWidget(builder());
573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595

    expect(inputValue.selection.baseOffset, 76);
    expect(inputValue.selection.extentOffset, 108);

    // Drag the left handle to the first line, just after 'First'.
    handlePos = endpoints[0].point + new Offset(-1.0, 1.0);
    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();
    await tester.pumpWidget(builder());

    expect(inputValue.selection.baseOffset, 5);
    expect(inputValue.selection.extentOffset, 108);

    await tester.tap(find.text('CUT'));
    await tester.pumpWidget(builder());
    expect(inputValue.selection.isCollapsed, true);
    expect(inputValue.text, cutValue);
  });

596 597 598 599 600 601 602 603 604 605 606 607 608 609 610 611 612 613 614 615 616 617 618 619 620 621 622
  testWidgets('Can scroll multiline input', (WidgetTester tester) async {
    GlobalKey inputKey = new GlobalKey();
    InputValue inputValue = InputValue.empty;

    Widget builder() {
      return new Overlay(
        initialEntries: <OverlayEntry>[
          new OverlayEntry(
            builder: (BuildContext context) {
              return new Center(
                child: new Material(
                  child: new Input(
                    value: inputValue,
                    key: inputKey,
                    style: const TextStyle(color: Colors.black, fontSize: 34.0),
                    maxLines: 2,
                    onChanged: (InputValue value) { inputValue = value; }
                  )
                )
              );
            }
          )
        ]
      );
    }

    await tester.pumpWidget(builder());
623
    await showKeyboard(tester);
624 625

    enterText(kFourLines);
626
    await tester.idle();
627 628 629 630 631 632 633 634 635 636 637 638 639 640 641 642 643

    await tester.pumpWidget(builder());

    RenderBox findInputBox() => tester.renderObject(find.byKey(inputKey));
    RenderBox inputBox = findInputBox();

    // Check that the last line of text is not displayed.
    Point firstPos = textOffsetToPosition(tester, kFourLines.indexOf('First'));
    Point fourthPos = textOffsetToPosition(tester, kFourLines.indexOf('Fourth'));
    expect(firstPos.x, fourthPos.x);
    expect(firstPos.y, lessThan(fourthPos.y));
    expect(inputBox.hitTest(new HitTestResult(), position: inputBox.globalToLocal(firstPos)), isTrue);
    expect(inputBox.hitTest(new HitTestResult(), position: inputBox.globalToLocal(fourthPos)), isFalse);

    TestGesture gesture = await tester.startGesture(firstPos, pointer: 7);
    await tester.pump();
    await gesture.moveBy(new Offset(0.0, -1000.0));
644 645 646 647 648
    await tester.pump(const Duration(seconds: 2));
    // 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.)
    await gesture.moveBy(new Offset(0.0, -1000.0));
    await tester.pump(const Duration(seconds: 2));
649 650 651 652 653 654 655 656 657 658 659 660 661 662 663 664 665 666 667 668
    await gesture.up();
    await tester.pump();

    // Now the first line is scrolled up, and the fourth line is visible.
    Point newFirstPos = textOffsetToPosition(tester, kFourLines.indexOf('First'));
    Point newFourthPos = textOffsetToPosition(tester, kFourLines.indexOf('Fourth'));
    expect(newFirstPos.y, lessThan(firstPos.y));
    expect(inputBox.hitTest(new HitTestResult(), position: inputBox.globalToLocal(newFirstPos)), isFalse);
    expect(inputBox.hitTest(new HitTestResult(), position: inputBox.globalToLocal(newFourthPos)), isTrue);

    // Now try scrolling by dragging the selection handle.

    // Long press the 'i' in 'Fourth line' to select the word.
    await tester.pump(const Duration(seconds: 2));
    Point untilPos = textOffsetToPosition(tester, kFourLines.indexOf('Fourth line')+8);
    gesture = await tester.startGesture(untilPos, pointer: 7);
    await tester.pump(const Duration(seconds: 2));
    await gesture.up();
    await tester.pump();

669 670
    RenderEditable renderEditable = findRenderEditable(tester);
    List<TextSelectionPoint> endpoints = renderEditable.getEndpointsForSelection(
671 672 673 674 675 676 677 678 679 680 681 682 683 684 685 686 687 688 689 690 691
        inputValue.selection);
    expect(endpoints.length, 2);

    // Drag the left handle to the first line, just after 'First'.
    Point handlePos = endpoints[0].point + new Offset(-1.0, 1.0);
    Point newHandlePos = textOffsetToPosition(tester, kFourLines.indexOf('First') + 5);
    gesture = await tester.startGesture(handlePos, pointer: 7);
    await tester.pump();
    await gesture.moveTo(newHandlePos + new Offset(0.0, -10.0));
    await tester.pump();
    await gesture.up();
    await tester.pump();

    // The text should have scrolled up with the handle to keep the active
    // cursor visible, back to its original position.
    newFirstPos = textOffsetToPosition(tester, kFourLines.indexOf('First'));
    newFourthPos = textOffsetToPosition(tester, kFourLines.indexOf('Fourth'));
    expect(newFirstPos.y, firstPos.y);
    expect(inputBox.hitTest(new HitTestResult(), position: inputBox.globalToLocal(newFirstPos)), isTrue);
    expect(inputBox.hitTest(new HitTestResult(), position: inputBox.globalToLocal(newFourthPos)), isFalse);
  });
692 693 694 695 696 697 698 699 700 701 702 703 704 705 706 707 708

  testWidgets('InputField smoke test', (WidgetTester tester) async {
    InputValue inputValue = InputValue.empty;

    Widget builder() {
      return new Center(
        child: new Material(
          child: new InputField(
            value: inputValue,
            hintText: 'Placeholder',
            onChanged: (InputValue value) { inputValue = value; }
          )
        )
      );
    }

    await tester.pumpWidget(builder());
709
    await showKeyboard(tester);
710 711 712 713 714 715 716 717 718 719 720 721 722 723 724 725 726 727 728 729 730 731 732 733 734 735 736 737 738 739 740 741

    Future<Null> checkText(String testValue) async {
      enterText(testValue);
      await tester.idle();

      // Check that the onChanged event handler fired.
      expect(inputValue.text, equals(testValue));

      return await tester.pumpWidget(builder());
    }

    checkText('Hello World');
  });

  testWidgets('InputField with global key', (WidgetTester tester) async {
    GlobalKey inputFieldKey = new GlobalKey(debugLabel: 'inputFieldKey');
    InputValue inputValue = InputValue.empty;

    Widget builder() {
      return new Center(
        child: new Material(
          child: new InputField(
            key: inputFieldKey,
            value: inputValue,
            hintText: 'Placeholder',
            onChanged: (InputValue value) { inputValue = value; }
          )
        )
      );
    }

    await tester.pumpWidget(builder());
742
    await showKeyboard(tester);
743 744 745 746 747 748 749 750 751 752 753 754 755

    Future<Null> checkText(String testValue) async {
      enterText(testValue);
      await tester.idle();

      // Check that the onChanged event handler fired.
      expect(inputValue.text, equals(testValue));

      return await tester.pumpWidget(builder());
    }

    checkText('Hello World');
  });
756
}