single_child_scroll_view_test.dart 28.3 KB
Newer Older
Ian Hickson's avatar
Ian Hickson committed
1
// Copyright 2014 The Flutter Authors. All rights reserved.
2 3 4
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

5 6
// @dart = 2.8

7 8
import 'dart:ui';

9
import 'package:flutter_test/flutter_test.dart';
10
import 'package:flutter/rendering.dart';
11
import 'package:flutter/widgets.dart';
12

13
import '../rendering/rendering_tester.dart';
14 15
import 'semantics_tester.dart';

16
class TestScrollPosition extends ScrollPositionWithSingleContext {
17 18
  TestScrollPosition({
    ScrollPhysics physics,
19
    ScrollContext state,
20
    double initialPixels = 0.0,
21 22 23
    ScrollPosition oldPosition,
  }) : super(
    physics: physics,
24
    context: state,
25 26 27 28 29 30 31
    initialPixels: initialPixels,
    oldPosition: oldPosition,
  );
}

class TestScrollController extends ScrollController {
  @override
32
  ScrollPosition createScrollPosition(ScrollPhysics physics, ScrollContext context, ScrollPosition oldPosition) {
33
    return TestScrollPosition(
34
      physics: physics,
35
      state: context,
36 37 38 39 40 41
      initialPixels: initialScrollOffset,
      oldPosition: oldPosition,
    );
  }
}

42
void main() {
43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63
  testWidgets('SingleChildScrollView respects clipBehavior', (WidgetTester tester) async {
    await tester.pumpWidget(SingleChildScrollView(child: Container(height: 2000.0)));

    // 1st, check that the render object has received the default clip behavior.
    final dynamic renderObject = tester.allRenderObjects.where((RenderObject o) => o.runtimeType.toString() == '_RenderSingleChildViewport').first;
    expect(renderObject.clipBehavior, equals(Clip.hardEdge));

    // 2nd, check that the painting context has received the default clip behavior.
    final TestClipPaintingContext context = TestClipPaintingContext();
    renderObject.paint(context, Offset.zero);
    expect(context.clipBehavior, equals(Clip.hardEdge));

    // 3rd, pump a new widget to check that the render object can update its clip behavior.
    await tester.pumpWidget(SingleChildScrollView(clipBehavior: Clip.antiAlias, child: Container(height: 2000.0)));
    expect(renderObject.clipBehavior, equals(Clip.antiAlias));

    // 4th, check that a non-default clip behavior can be sent to the painting context.
    renderObject.paint(context, Offset.zero);
    expect(context.clipBehavior, equals(Clip.antiAlias));
  });

64
  testWidgets('SingleChildScrollView control test', (WidgetTester tester) async {
65 66
    await tester.pumpWidget(SingleChildScrollView(
      child: Container(
67
        height: 2000.0,
68
        color: const Color(0xFF00FF00),
69 70 71
      ),
    ));

72
    final RenderBox box = tester.renderObject(find.byType(Container));
73
    expect(box.localToGlobal(Offset.zero), equals(Offset.zero));
74

75
    await tester.drag(find.byType(SingleChildScrollView), const Offset(-200.0, -200.0));
76

77
    expect(box.localToGlobal(Offset.zero), equals(const Offset(0.0, -200.0)));
78
  });
79 80

  testWidgets('Changing controllers changes scroll position', (WidgetTester tester) async {
81
    final TestScrollController controller = TestScrollController();
82

83 84
    await tester.pumpWidget(SingleChildScrollView(
      child: Container(
85
        height: 2000.0,
86
        color: const Color(0xFF00FF00),
87 88 89
      ),
    ));

90
    await tester.pumpWidget(SingleChildScrollView(
91
      controller: controller,
92
      child: Container(
93
        height: 2000.0,
94
        color: const Color(0xFF00FF00),
95 96 97
      ),
    ));

98
    final ScrollableState scrollable = tester.state(find.byType(Scrollable));
Dan Field's avatar
Dan Field committed
99
    expect(scrollable.position, isA<TestScrollPosition>());
100 101
  });

102
  testWidgets('Sets PrimaryScrollController when primary', (WidgetTester tester) async {
103 104
    final ScrollController primaryScrollController = ScrollController();
    await tester.pumpWidget(PrimaryScrollController(
105
      controller: primaryScrollController,
106
      child: SingleChildScrollView(
107
        primary: true,
108
        child: Container(
109
          height: 2000.0,
110
          color: const Color(0xFF00FF00),
111 112 113 114
        ),
      ),
    ));

115
    final Scrollable scrollable = tester.widget(find.byType(Scrollable));
116 117 118 119
    expect(scrollable.controller, primaryScrollController);
  });


120
  testWidgets('Changing scroll controller inside dirty layout builder does not assert', (WidgetTester tester) async {
121
    final ScrollController controller = ScrollController();
122

123 124
    await tester.pumpWidget(Center(
      child: SizedBox(
125
        width: 750.0,
126
        child: LayoutBuilder(
127
          builder: (BuildContext context, BoxConstraints constraints) {
128 129
            return SingleChildScrollView(
              child: Container(
130
                height: 2000.0,
131
                color: const Color(0xFF00FF00),
132 133 134 135 136 137 138
              ),
            );
          },
        ),
      ),
    ));

139 140
    await tester.pumpWidget(Center(
      child: SizedBox(
141
        width: 700.0,
142
        child: LayoutBuilder(
143
          builder: (BuildContext context, BoxConstraints constraints) {
144
            return SingleChildScrollView(
145
              controller: controller,
146
              child: Container(
147
                height: 2000.0,
148
                color: const Color(0xFF00FF00),
149 150 151 152 153 154 155
              ),
            );
          },
        ),
      ),
    ));
  });
156 157

  testWidgets('Vertical SingleChildScrollViews are primary by default', (WidgetTester tester) async {
158
    const SingleChildScrollView view = SingleChildScrollView(scrollDirection: Axis.vertical);
159 160 161 162
    expect(view.primary, isTrue);
  });

  testWidgets('Horizontal SingleChildScrollViews are non-primary by default', (WidgetTester tester) async {
163
    const SingleChildScrollView view = SingleChildScrollView(scrollDirection: Axis.horizontal);
164 165 166 167
    expect(view.primary, isFalse);
  });

  testWidgets('SingleChildScrollViews with controllers are non-primary by default', (WidgetTester tester) async {
168 169
    final SingleChildScrollView view = SingleChildScrollView(
      controller: ScrollController(),
170 171 172 173 174 175
      scrollDirection: Axis.vertical,
    );
    expect(view.primary, isFalse);
  });

  testWidgets('Nested scrollables have a null PrimaryScrollController', (WidgetTester tester) async {
176
    const Key innerKey = Key('inner');
177
    final ScrollController primaryScrollController = ScrollController();
178
    await tester.pumpWidget(
179
      Directionality(
180
        textDirection: TextDirection.ltr,
181
        child: PrimaryScrollController(
182
          controller: primaryScrollController,
183
          child: SingleChildScrollView(
184
            primary: true,
185
            child: Container(
186
              constraints: const BoxConstraints(maxHeight: 200.0),
187
              child: ListView(key: innerKey, primary: true),
188 189
            ),
          ),
190 191
        ),
      ),
192
    );
193

194
    final Scrollable innerScrollable = tester.widget(
195 196 197 198 199 200 201
      find.descendant(
        of: find.byKey(innerKey),
        matching: find.byType(Scrollable),
      ),
    );
    expect(innerScrollable.controller, isNull);
  });
202 203

  testWidgets('SingleChildScrollView semantics', (WidgetTester tester) async {
204 205
    final SemanticsTester semantics = SemanticsTester(tester);
    final ScrollController controller = ScrollController();
206 207

    await tester.pumpWidget(
208
      Directionality(
209
        textDirection: TextDirection.ltr,
210
        child: SingleChildScrollView(
211
          controller: controller,
212 213 214
          child: Column(
            children: List<Widget>.generate(30, (int i) {
              return Container(
215
                height: 200.0,
216
                child: Text('Tile $i'),
217 218 219 220 221 222 223 224
              );
            }),
          ),
        ),
      ),
    );

    expect(semantics, hasSemantics(
225
      TestSemantics(
226
        children: <TestSemantics>[
227
          TestSemantics(
228 229 230
            flags: <SemanticsFlag>[
              SemanticsFlag.hasImplicitScrolling,
            ],
231 232 233 234
            actions: <SemanticsAction>[
              SemanticsAction.scrollUp,
            ],
            children: <TestSemantics>[
235
              TestSemantics(
236 237 238
                label: r'Tile 0',
                textDirection: TextDirection.ltr,
              ),
239
              TestSemantics(
240 241 242
                label: r'Tile 1',
                textDirection: TextDirection.ltr,
              ),
243
              TestSemantics(
244 245 246
                label: r'Tile 2',
                textDirection: TextDirection.ltr,
              ),
247
              TestSemantics(
248 249 250 251 252 253
                flags: <SemanticsFlag>[
                  SemanticsFlag.isHidden,
                ],
                label: r'Tile 3',
                textDirection: TextDirection.ltr,
              ),
254
              TestSemantics(
255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270
                flags: <SemanticsFlag>[
                  SemanticsFlag.isHidden,],
                label: r'Tile 4',
                textDirection: TextDirection.ltr,
              ),
            ],
          ),
        ],
      ),
      ignoreRect: true, ignoreTransform: true, ignoreId: true,
    ));

    controller.jumpTo(3000.0);
    await tester.pumpAndSettle();

    expect(semantics, hasSemantics(
271
      TestSemantics(
272
        children: <TestSemantics>[
273
          TestSemantics(
274 275 276
            flags: <SemanticsFlag>[
              SemanticsFlag.hasImplicitScrolling,
            ],
277 278 279 280 281
            actions: <SemanticsAction>[
              SemanticsAction.scrollUp,
              SemanticsAction.scrollDown,
            ],
            children: <TestSemantics>[
282
              TestSemantics(
283 284 285 286 287 288
                flags: <SemanticsFlag>[
                  SemanticsFlag.isHidden,
                ],
                label: r'Tile 13',
                textDirection: TextDirection.ltr,
              ),
289
              TestSemantics(
290 291 292 293 294 295
                flags: <SemanticsFlag>[
                  SemanticsFlag.isHidden,
                ],
                label: r'Tile 14',
                textDirection: TextDirection.ltr,
              ),
296
              TestSemantics(
297 298 299
                label: r'Tile 15',
                textDirection: TextDirection.ltr,
              ),
300
              TestSemantics(
301 302 303
                label: r'Tile 16',
                textDirection: TextDirection.ltr,
              ),
304
              TestSemantics(
305 306 307
                label: r'Tile 17',
                textDirection: TextDirection.ltr,
              ),
308
              TestSemantics(
309 310 311 312 313 314
                flags: <SemanticsFlag>[
                  SemanticsFlag.isHidden,
                ],
                label: r'Tile 18',
                textDirection: TextDirection.ltr,
              ),
315
              TestSemantics(
316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332
                flags: <SemanticsFlag>[
                  SemanticsFlag.isHidden,
                ],
                label: r'Tile 19',
                textDirection: TextDirection.ltr,
              ),
            ],
          ),
        ],
      ),
      ignoreRect: true, ignoreTransform: true, ignoreId: true,
    ));

    controller.jumpTo(6000.0);
    await tester.pumpAndSettle();

    expect(semantics, hasSemantics(
333
      TestSemantics(
334
        children: <TestSemantics>[
335
          TestSemantics(
336 337 338
            flags: <SemanticsFlag>[
              SemanticsFlag.hasImplicitScrolling,
            ],
339 340 341 342
            actions: <SemanticsAction>[
              SemanticsAction.scrollDown,
            ],
            children: <TestSemantics>[
343
              TestSemantics(
344 345 346 347 348 349
                flags: <SemanticsFlag>[
                  SemanticsFlag.isHidden,
                ],
                label: r'Tile 25',
                textDirection: TextDirection.ltr,
              ),
350
              TestSemantics(
351 352 353 354 355 356
                flags: <SemanticsFlag>[
                  SemanticsFlag.isHidden,
                ],
                label: r'Tile 26',
                textDirection: TextDirection.ltr,
              ),
357
              TestSemantics(
358 359 360
                label: r'Tile 27',
                textDirection: TextDirection.ltr,
              ),
361
              TestSemantics(
362 363 364
                label: r'Tile 28',
                textDirection: TextDirection.ltr,
              ),
365
              TestSemantics(
366 367 368 369 370 371 372 373 374 375 376 377
                label: r'Tile 29',
                textDirection: TextDirection.ltr,
              ),
            ],
          ),
        ],
      ),
      ignoreRect: true, ignoreTransform: true, ignoreId: true,
    ));

    semantics.dispose();
  });
378 379 380 381

  testWidgets('SingleChildScrollView getOffsetToReveal - down', (WidgetTester tester) async {
    List<Widget> children;
    await tester.pumpWidget(
382
      Directionality(
383
        textDirection: TextDirection.ltr,
384
        child: Center(
385 386 387
          child: Container(
            height: 200.0,
            width: 300.0,
388 389 390 391 392
            child: SingleChildScrollView(
              controller: ScrollController(initialScrollOffset: 300.0),
              child: Column(
                children: children = List<Widget>.generate(20, (int i) {
                  return Container(
393 394
                    height: 100.0,
                    width: 300.0,
395
                    child: Text('Tile $i'),
396 397 398 399 400 401 402 403 404
                  );
                }),
              ),
            ),
          ),
        ),
      ),
    );

405
    final RenderAbstractViewport viewport = tester.allRenderObjects.whereType<RenderAbstractViewport>().first;
406 407 408 409

    final RenderObject target = tester.renderObject(find.byWidget(children[5]));
    RevealedOffset revealed = viewport.getOffsetToReveal(target, 0.0);
    expect(revealed.offset, 500.0);
Dan Field's avatar
Dan Field committed
410
    expect(revealed.rect, const Rect.fromLTWH(0.0, 0.0, 300.0, 100.0));
411 412 413

    revealed = viewport.getOffsetToReveal(target, 1.0);
    expect(revealed.offset, 400.0);
Dan Field's avatar
Dan Field committed
414
    expect(revealed.rect, const Rect.fromLTWH(0.0, 100.0, 300.0, 100.0));
415

Dan Field's avatar
Dan Field committed
416
    revealed = viewport.getOffsetToReveal(target, 0.0, rect: const Rect.fromLTWH(40.0, 40.0, 10.0, 10.0));
417
    expect(revealed.offset, 540.0);
Dan Field's avatar
Dan Field committed
418
    expect(revealed.rect, const Rect.fromLTWH(40.0, 0.0, 10.0, 10.0));
419

Dan Field's avatar
Dan Field committed
420
    revealed = viewport.getOffsetToReveal(target, 1.0, rect: const Rect.fromLTWH(40.0, 40.0, 10.0, 10.0));
421
    expect(revealed.offset, 350.0);
Dan Field's avatar
Dan Field committed
422
    expect(revealed.rect, const Rect.fromLTWH(40.0, 190.0, 10.0, 10.0));
423 424 425
  });

  testWidgets('SingleChildScrollView getOffsetToReveal - up', (WidgetTester tester) async {
426 427
    final List<Widget> children = List<Widget>.generate(20, (int i) {
      return Container(
428 429
        height: 100.0,
        width: 300.0,
430
        child: Text('Tile $i'),
431 432 433
      );
    });
    await tester.pumpWidget(
434
      Directionality(
435
        textDirection: TextDirection.ltr,
436
        child: Center(
437 438 439
          child: Container(
            height: 200.0,
            width: 300.0,
440 441
            child: SingleChildScrollView(
              controller: ScrollController(initialScrollOffset: 300.0),
442
              reverse: true,
443
              child: Column(
444 445 446 447 448 449 450 451
                children: children.reversed.toList(),
              ),
            ),
          ),
        ),
      ),
    );

452
    final RenderAbstractViewport viewport = tester.allRenderObjects.whereType<RenderAbstractViewport>().first;
453 454 455 456

    final RenderObject target = tester.renderObject(find.byWidget(children[5]));
    RevealedOffset revealed = viewport.getOffsetToReveal(target, 0.0);
    expect(revealed.offset, 500.0);
Dan Field's avatar
Dan Field committed
457
    expect(revealed.rect, const Rect.fromLTWH(0.0, 100.0, 300.0, 100.0));
458 459 460

    revealed = viewport.getOffsetToReveal(target, 1.0);
    expect(revealed.offset, 400.0);
Dan Field's avatar
Dan Field committed
461
    expect(revealed.rect, const Rect.fromLTWH(0.0, 0.0, 300.0, 100.0));
462

Dan Field's avatar
Dan Field committed
463
    revealed = viewport.getOffsetToReveal(target, 0.0, rect: const Rect.fromLTWH(40.0, 40.0, 10.0, 10.0));
464
    expect(revealed.offset, 550.0);
Dan Field's avatar
Dan Field committed
465
    expect(revealed.rect, const Rect.fromLTWH(40.0, 190.0, 10.0, 10.0));
466

Dan Field's avatar
Dan Field committed
467
    revealed = viewport.getOffsetToReveal(target, 1.0, rect: const Rect.fromLTWH(40.0, 40.0, 10.0, 10.0));
468
    expect(revealed.offset, 360.0);
Dan Field's avatar
Dan Field committed
469
    expect(revealed.rect, const Rect.fromLTWH(40.0, 0.0, 10.0, 10.0));
470 471 472 473 474 475
  });

  testWidgets('SingleChildScrollView getOffsetToReveal - right', (WidgetTester tester) async {
    List<Widget> children;

    await tester.pumpWidget(
476
      Directionality(
477
        textDirection: TextDirection.ltr,
478
        child: Center(
479 480 481
          child: Container(
            height: 300.0,
            width: 200.0,
482
            child: SingleChildScrollView(
483
              scrollDirection: Axis.horizontal,
484 485 486 487
              controller: ScrollController(initialScrollOffset: 300.0),
              child: Row(
                children: children = List<Widget>.generate(20, (int i) {
                  return Container(
488 489
                    height: 300.0,
                    width: 100.0,
490
                    child: Text('Tile $i'),
491 492 493 494 495 496 497 498 499
                  );
                }),
              ),
            ),
          ),
        ),
      ),
    );

500
    final RenderAbstractViewport viewport = tester.allRenderObjects.whereType<RenderAbstractViewport>().first;
501 502 503 504

    final RenderObject target = tester.renderObject(find.byWidget(children[5]));
    RevealedOffset revealed = viewport.getOffsetToReveal(target, 0.0);
    expect(revealed.offset, 500.0);
Dan Field's avatar
Dan Field committed
505
    expect(revealed.rect, const Rect.fromLTWH(0.0, 0.0, 100.0, 300.0));
506 507 508

    revealed = viewport.getOffsetToReveal(target, 1.0);
    expect(revealed.offset, 400.0);
Dan Field's avatar
Dan Field committed
509
    expect(revealed.rect, const Rect.fromLTWH(100.0, 0.0, 100.0, 300.0));
510

Dan Field's avatar
Dan Field committed
511
    revealed = viewport.getOffsetToReveal(target, 0.0, rect: const Rect.fromLTWH(40.0, 40.0, 10.0, 10.0));
512
    expect(revealed.offset, 540.0);
Dan Field's avatar
Dan Field committed
513
    expect(revealed.rect, const Rect.fromLTWH(0.0, 40.0, 10.0, 10.0));
514

Dan Field's avatar
Dan Field committed
515
    revealed = viewport.getOffsetToReveal(target, 1.0, rect: const Rect.fromLTWH(40.0, 40.0, 10.0, 10.0));
516
    expect(revealed.offset, 350.0);
Dan Field's avatar
Dan Field committed
517
    expect(revealed.rect, const Rect.fromLTWH(190.0, 40.0, 10.0, 10.0));
518 519 520
  });

  testWidgets('SingleChildScrollView getOffsetToReveal - left', (WidgetTester tester) async {
521 522
    final List<Widget> children = List<Widget>.generate(20, (int i) {
      return Container(
523 524
        height: 300.0,
        width: 100.0,
525
        child: Text('Tile $i'),
526 527 528 529
      );
    });

    await tester.pumpWidget(
530
      Directionality(
531
        textDirection: TextDirection.ltr,
532
        child: Center(
533 534 535
          child: Container(
            height: 300.0,
            width: 200.0,
536
            child: SingleChildScrollView(
537 538
              scrollDirection: Axis.horizontal,
              reverse: true,
539 540
              controller: ScrollController(initialScrollOffset: 300.0),
              child: Row(
541 542 543 544 545 546 547 548
                children: children.reversed.toList(),
              ),
            ),
          ),
        ),
      ),
    );

549
    final RenderAbstractViewport viewport = tester.allRenderObjects.whereType<RenderAbstractViewport>().first;
550 551 552 553

    final RenderObject target = tester.renderObject(find.byWidget(children[5]));
    RevealedOffset revealed = viewport.getOffsetToReveal(target, 0.0);
    expect(revealed.offset, 500.0);
Dan Field's avatar
Dan Field committed
554
    expect(revealed.rect, const Rect.fromLTWH(100.0, 0.0, 100.0, 300.0));
555 556 557

    revealed = viewport.getOffsetToReveal(target, 1.0);
    expect(revealed.offset, 400.0);
Dan Field's avatar
Dan Field committed
558
    expect(revealed.rect, const Rect.fromLTWH(0.0, 0.0, 100.0, 300.0));
559

Dan Field's avatar
Dan Field committed
560
    revealed = viewport.getOffsetToReveal(target, 0.0, rect: const Rect.fromLTWH(40.0, 40.0, 10.0, 10.0));
561
    expect(revealed.offset, 550.0);
Dan Field's avatar
Dan Field committed
562
    expect(revealed.rect, const Rect.fromLTWH(190.0, 40.0, 10.0, 10.0));
563

Dan Field's avatar
Dan Field committed
564
    revealed = viewport.getOffsetToReveal(target, 1.0, rect: const Rect.fromLTWH(40.0, 40.0, 10.0, 10.0));
565
    expect(revealed.offset, 360.0);
Dan Field's avatar
Dan Field committed
566
    expect(revealed.rect, const Rect.fromLTWH(0.0, 40.0, 10.0, 10.0));
567 568 569
  });

  testWidgets('Nested SingleChildScrollView showOnScreen', (WidgetTester tester) async {
570
    final List<List<Widget>> children = List<List<Widget>>(10);
571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592
    ScrollController controllerX;
    ScrollController controllerY;

    /// Builds a gird:
    ///
    ///       <- x ->
    ///   0 1 2 3 4 5 6 7 8 9
    /// 0 c c c c c c c c c c
    /// 1 c c c c c c c c c c
    /// 2 c c c c c c c c c c
    /// 3 c c c c c c c c c c  y
    /// 4 c c c c v v c c c c
    /// 5 c c c c v v c c c c
    /// 6 c c c c c c c c c c
    /// 7 c c c c c c c c c c
    /// 8 c c c c c c c c c c
    /// 9 c c c c c c c c c c
    ///
    /// Each c is a 100x100 container, v are containers visible in initial
    /// viewport.

    await tester.pumpWidget(
593
      Directionality(
594
        textDirection: TextDirection.ltr,
595
        child: Center(
596 597 598
          child: Container(
            height: 200.0,
            width: 200.0,
599 600 601 602
            child: SingleChildScrollView(
              controller: controllerY = ScrollController(initialScrollOffset: 400.0),
              child: SingleChildScrollView(
                controller: controllerX = ScrollController(initialScrollOffset: 400.0),
603
                scrollDirection: Axis.horizontal,
604 605 606 607
                child: Column(
                  children: List<Widget>.generate(10, (int y) {
                    return Row(
                      children: children[y] = List<Widget>.generate(10, (int x) {
608
                        return SizedBox(
609 610
                          key: UniqueKey(),
                          height: 100.0,
611 612
                          width: 100.0,
                        );
613
                      }),
614 615 616 617 618 619 620 621 622 623 624 625 626 627 628 629 630 631 632 633 634 635 636 637 638 639 640 641 642 643 644 645 646 647 648 649 650 651 652 653 654 655 656 657 658 659 660 661 662 663 664 665 666 667 668 669 670 671 672 673 674 675 676 677 678 679 680 681 682 683 684 685 686 687 688 689 690 691 692 693 694 695 696 697 698 699 700 701 702 703 704 705 706 707 708 709 710 711 712 713 714 715 716 717 718 719 720 721 722 723 724 725 726 727 728 729 730 731 732 733
                    );
                  }),
                ),
              ),
            ),
          ),
        ),
      ),
    );

    expect(controllerX.offset, 400.0);
    expect(controllerY.offset, 400.0);

    // Already in viewport
    tester.renderObject(find.byWidget(children[4][4])).showOnScreen();
    await tester.pumpAndSettle();
    expect(controllerX.offset, 400.0);
    expect(controllerY.offset, 400.0);

    controllerX.jumpTo(400.0);
    controllerY.jumpTo(400.0);
    await tester.pumpAndSettle();

    // Above viewport
    tester.renderObject(find.byWidget(children[3][4])).showOnScreen();
    await tester.pumpAndSettle();
    expect(controllerX.offset, 400.0);
    expect(controllerY.offset, 300.0);

    controllerX.jumpTo(400.0);
    controllerY.jumpTo(400.0);
    await tester.pumpAndSettle();

    // Below viewport
    tester.renderObject(find.byWidget(children[6][4])).showOnScreen();
    await tester.pumpAndSettle();
    expect(controllerX.offset, 400.0);
    expect(controllerY.offset, 500.0);

    controllerX.jumpTo(400.0);
    controllerY.jumpTo(400.0);
    await tester.pumpAndSettle();

    // Left of viewport
    tester.renderObject(find.byWidget(children[4][3])).showOnScreen();
    await tester.pumpAndSettle();
    expect(controllerX.offset, 300.0);
    expect(controllerY.offset, 400.0);

    controllerX.jumpTo(400.0);
    controllerY.jumpTo(400.0);
    await tester.pumpAndSettle();

    // Right of viewport
    tester.renderObject(find.byWidget(children[4][6])).showOnScreen();
    await tester.pumpAndSettle();
    expect(controllerX.offset, 500.0);
    expect(controllerY.offset, 400.0);

    controllerX.jumpTo(400.0);
    controllerY.jumpTo(400.0);
    await tester.pumpAndSettle();

    // Above and left of viewport
    tester.renderObject(find.byWidget(children[3][3])).showOnScreen();
    await tester.pumpAndSettle();
    expect(controllerX.offset, 300.0);
    expect(controllerY.offset, 300.0);

    controllerX.jumpTo(400.0);
    controllerY.jumpTo(400.0);
    await tester.pumpAndSettle();

    // Below and left of viewport
    tester.renderObject(find.byWidget(children[6][3])).showOnScreen();
    await tester.pumpAndSettle();
    expect(controllerX.offset, 300.0);
    expect(controllerY.offset, 500.0);

    controllerX.jumpTo(400.0);
    controllerY.jumpTo(400.0);
    await tester.pumpAndSettle();

    // Above and right of viewport
    tester.renderObject(find.byWidget(children[3][6])).showOnScreen();
    await tester.pumpAndSettle();
    expect(controllerX.offset, 500.0);
    expect(controllerY.offset, 300.0);

    controllerX.jumpTo(400.0);
    controllerY.jumpTo(400.0);
    await tester.pumpAndSettle();

    // Below and right of viewport
    tester.renderObject(find.byWidget(children[6][6])).showOnScreen();
    await tester.pumpAndSettle();
    expect(controllerX.offset, 500.0);
    expect(controllerY.offset, 500.0);

    controllerX.jumpTo(400.0);
    controllerY.jumpTo(400.0);
    await tester.pumpAndSettle();

    // Below and right of viewport with animations
    tester.renderObject(find.byWidget(children[6][6])).showOnScreen(duration: const Duration(seconds: 2));
    await tester.pump();
    await tester.pump(const Duration(seconds: 1));
    expect(tester.hasRunningAnimations, isTrue);
    expect(controllerX.offset, greaterThan(400.0));
    expect(controllerX.offset, lessThan(500.0));
    expect(controllerY.offset, greaterThan(400.0));
    expect(controllerY.offset, lessThan(500.0));
    await tester.pumpAndSettle();
    expect(controllerX.offset, 500.0);
    expect(controllerY.offset, 500.0);
  });

  group('Nested SingleChildScrollView (same orientation) showOnScreen', () {
    List<Widget> children;

734
    Future<void> buildNestedScroller({ WidgetTester tester, ScrollController inner, ScrollController outer }) {
735
      return tester.pumpWidget(
736
        Directionality(
737
          textDirection: TextDirection.ltr,
738
          child: Center(
739 740 741
            child: Container(
              height: 200.0,
              width: 300.0,
742
              child: SingleChildScrollView(
743
                controller: outer,
744
                child: Column(
745
                  children: <Widget>[
746
                    Container(
747 748
                      height: 200.0,
                    ),
749
                    Container(
750 751
                      height: 200.0,
                      width: 300.0,
752
                      child: SingleChildScrollView(
753
                        controller: inner,
754 755 756
                        child: Column(
                          children: children = List<Widget>.generate(10, (int i) {
                            return Container(
757 758
                              height: 100.0,
                              width: 300.0,
759
                              child: Text('$i'),
760 761 762 763 764
                            );
                          }),
                        ),
                      ),
                    ),
765
                    Container(
766
                      height: 200.0,
767
                    ),
768 769 770 771 772 773 774 775 776 777
                  ],
                ),
              ),
            ),
          ),
        ),
      );
    }

    testWidgets('in view in inner, but not in outer', (WidgetTester tester) async {
778 779
      final ScrollController inner = ScrollController();
      final ScrollController outer = ScrollController();
780 781 782 783 784 785 786 787 788 789 790 791 792 793 794
      await buildNestedScroller(
        tester: tester,
        inner: inner,
        outer: outer,
      );
      expect(outer.offset, 0.0);
      expect(inner.offset, 0.0);

      tester.renderObject(find.byWidget(children[0])).showOnScreen();
      await tester.pumpAndSettle();
      expect(inner.offset, 0.0);
      expect(outer.offset, 100.0);
    });

    testWidgets('not in view of neither inner nor outer', (WidgetTester tester) async {
795 796
      final ScrollController inner = ScrollController();
      final ScrollController outer = ScrollController();
797 798 799 800 801 802 803 804 805 806 807 808 809 810 811
      await buildNestedScroller(
        tester: tester,
        inner: inner,
        outer: outer,
      );
      expect(outer.offset, 0.0);
      expect(inner.offset, 0.0);

      tester.renderObject(find.byWidget(children[5])).showOnScreen();
      await tester.pumpAndSettle();
      expect(inner.offset, 400.0);
      expect(outer.offset, 200.0);
    });

    testWidgets('in view in inner and outer', (WidgetTester tester) async {
812 813
      final ScrollController inner = ScrollController(initialScrollOffset: 200.0);
      final ScrollController outer = ScrollController(initialScrollOffset: 200.0);
814 815 816 817 818 819 820 821 822 823 824 825 826 827 828
      await buildNestedScroller(
        tester: tester,
        inner: inner,
        outer: outer,
      );
      expect(outer.offset, 200.0);
      expect(inner.offset, 200.0);

      tester.renderObject(find.byWidget(children[2])).showOnScreen();
      await tester.pumpAndSettle();
      expect(outer.offset, 200.0);
      expect(inner.offset, 200.0);
    });

    testWidgets('inner shown in outer, but item not visible', (WidgetTester tester) async {
829 830
      final ScrollController inner = ScrollController(initialScrollOffset: 200.0);
      final ScrollController outer = ScrollController(initialScrollOffset: 200.0);
831 832 833 834 835 836 837 838 839 840 841 842 843 844 845
      await buildNestedScroller(
        tester: tester,
        inner: inner,
        outer: outer,
      );
      expect(outer.offset, 200.0);
      expect(inner.offset, 200.0);

      tester.renderObject(find.byWidget(children[5])).showOnScreen();
      await tester.pumpAndSettle();
      expect(outer.offset, 200.0);
      expect(inner.offset, 400.0);
    });

    testWidgets('inner half shown in outer, item only visible in inner', (WidgetTester tester) async {
846 847
      final ScrollController inner = ScrollController();
      final ScrollController outer = ScrollController(initialScrollOffset: 100.0);
848 849 850 851 852 853 854 855 856 857 858 859 860 861
      await buildNestedScroller(
        tester: tester,
        inner: inner,
        outer: outer,
      );
      expect(outer.offset, 100.0);
      expect(inner.offset, 0.0);

      tester.renderObject(find.byWidget(children[1])).showOnScreen();
      await tester.pumpAndSettle();
      expect(outer.offset, 200.0);
      expect(inner.offset, 0.0);
    });
  });
862
}