single_child_scroll_view_test.dart 30.7 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
import 'dart:ui';

7
import 'package:flutter_test/flutter_test.dart';
8
import 'package:flutter/rendering.dart';
9
import 'package:flutter/widgets.dart';
10

11
import '../rendering/rendering_tester.dart';
12 13
import 'semantics_tester.dart';

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

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

40
void main() {
41 42 43 44 45 46 47 48 49 50 51 52 53
  testWidgets('SingleChildScrollView overflow and clipRect test', (WidgetTester tester) async {
    // the test widowSize is Size(800.0, 600.0)
    await tester.pumpWidget(
      Directionality(
        textDirection: TextDirection.ltr,
        child: SingleChildScrollView(
          scrollDirection: Axis.vertical,
          child: Container(height: 600.0,)
        )
      )
    );

    // 1st, check that the render object has received the default clip behavior.
54
    final dynamic renderObject = tester.allRenderObjects.where((RenderObject o) => o.runtimeType.toString() == '_RenderSingleChildViewport').first; // ignore: unnecessary_nullable_for_final_variable_declarations
55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103
    expect(renderObject.clipBehavior, equals(Clip.hardEdge));

    // 2nd, height == widow.height test: check that the painting context does not call pushClipRect .
    TestClipPaintingContext context = TestClipPaintingContext();
    renderObject.paint(context, Offset.zero);
    expect(context.clipBehavior, equals(Clip.none));

    // 3rd, height overflow test: check that the painting context call pushClipRect.
    await tester.pumpWidget(
      Directionality(
        textDirection: TextDirection.ltr,
        child: SingleChildScrollView(
          scrollDirection: Axis.vertical,
          child: Container(height: 600.1,)
        )
      )
    );
    renderObject.paint(context, Offset.zero);
    expect(context.clipBehavior, equals(Clip.hardEdge));

    // 4th, width == widow.width test: check that the painting context do not call pushClipRect.
    context = TestClipPaintingContext();
    expect(context.clipBehavior, equals(Clip.none)); // initial value
    await tester.pumpWidget(
      Directionality(
        textDirection: TextDirection.ltr,
        child: SingleChildScrollView(
          scrollDirection: Axis.horizontal,
          child: Container(width: 800.0,)
        )
      )
    );
    renderObject.paint(context, Offset.zero);
    expect(context.clipBehavior, equals(Clip.none));

    // 5th, width overflow test: check that the painting context call pushClipRect.
    await tester.pumpWidget(
      Directionality(
        textDirection: TextDirection.ltr,
        child: SingleChildScrollView(
          scrollDirection: Axis.horizontal,
          child: Container(width: 800.1,)
        )
      )
    );
    renderObject.paint(context, Offset.zero);
    expect(context.clipBehavior, equals(Clip.hardEdge));
  });

104 105 106 107
  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.
108
    final dynamic renderObject = tester.allRenderObjects.where((RenderObject o) => o.runtimeType.toString() == '_RenderSingleChildViewport').first; // ignore: unnecessary_nullable_for_final_variable_declarations
109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124
    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));
  });

125
  testWidgets('SingleChildScrollView control test', (WidgetTester tester) async {
126 127
    await tester.pumpWidget(SingleChildScrollView(
      child: Container(
128
        height: 2000.0,
129
        color: const Color(0xFF00FF00),
130 131 132
      ),
    ));

133
    final RenderBox box = tester.renderObject(find.byType(Container));
134
    expect(box.localToGlobal(Offset.zero), equals(Offset.zero));
135

136
    await tester.drag(find.byType(SingleChildScrollView), const Offset(-200.0, -200.0));
137

138
    expect(box.localToGlobal(Offset.zero), equals(const Offset(0.0, -200.0)));
139
  });
140 141

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

144 145
    await tester.pumpWidget(SingleChildScrollView(
      child: Container(
146
        height: 2000.0,
147
        color: const Color(0xFF00FF00),
148 149 150
      ),
    ));

151
    await tester.pumpWidget(SingleChildScrollView(
152
      controller: controller,
153
      child: Container(
154
        height: 2000.0,
155
        color: const Color(0xFF00FF00),
156 157 158
      ),
    ));

159
    final ScrollableState scrollable = tester.state(find.byType(Scrollable));
Dan Field's avatar
Dan Field committed
160
    expect(scrollable.position, isA<TestScrollPosition>());
161 162
  });

163
  testWidgets('Sets PrimaryScrollController when primary', (WidgetTester tester) async {
164 165
    final ScrollController primaryScrollController = ScrollController();
    await tester.pumpWidget(PrimaryScrollController(
166
      controller: primaryScrollController,
167
      child: SingleChildScrollView(
168
        primary: true,
169
        child: Container(
170
          height: 2000.0,
171
          color: const Color(0xFF00FF00),
172 173 174 175
        ),
      ),
    ));

176
    final Scrollable scrollable = tester.widget(find.byType(Scrollable));
177 178 179 180
    expect(scrollable.controller, primaryScrollController);
  });


181
  testWidgets('Changing scroll controller inside dirty layout builder does not assert', (WidgetTester tester) async {
182
    final ScrollController controller = ScrollController();
183

184 185
    await tester.pumpWidget(Center(
      child: SizedBox(
186
        width: 750.0,
187
        child: LayoutBuilder(
188
          builder: (BuildContext context, BoxConstraints constraints) {
189 190
            return SingleChildScrollView(
              child: Container(
191
                height: 2000.0,
192
                color: const Color(0xFF00FF00),
193 194 195 196 197 198 199
              ),
            );
          },
        ),
      ),
    ));

200 201
    await tester.pumpWidget(Center(
      child: SizedBox(
202
        width: 700.0,
203
        child: LayoutBuilder(
204
          builder: (BuildContext context, BoxConstraints constraints) {
205
            return SingleChildScrollView(
206
              controller: controller,
207
              child: Container(
208
                height: 2000.0,
209
                color: const Color(0xFF00FF00),
210 211 212 213 214 215 216
              ),
            );
          },
        ),
      ),
    ));
  });
217 218

  testWidgets('Vertical SingleChildScrollViews are primary by default', (WidgetTester tester) async {
219
    const SingleChildScrollView view = SingleChildScrollView(scrollDirection: Axis.vertical);
220 221 222 223
    expect(view.primary, isTrue);
  });

  testWidgets('Horizontal SingleChildScrollViews are non-primary by default', (WidgetTester tester) async {
224
    const SingleChildScrollView view = SingleChildScrollView(scrollDirection: Axis.horizontal);
225 226 227 228
    expect(view.primary, isFalse);
  });

  testWidgets('SingleChildScrollViews with controllers are non-primary by default', (WidgetTester tester) async {
229 230
    final SingleChildScrollView view = SingleChildScrollView(
      controller: ScrollController(),
231 232 233 234 235 236
      scrollDirection: Axis.vertical,
    );
    expect(view.primary, isFalse);
  });

  testWidgets('Nested scrollables have a null PrimaryScrollController', (WidgetTester tester) async {
237
    const Key innerKey = Key('inner');
238
    final ScrollController primaryScrollController = ScrollController();
239
    await tester.pumpWidget(
240
      Directionality(
241
        textDirection: TextDirection.ltr,
242
        child: PrimaryScrollController(
243
          controller: primaryScrollController,
244
          child: SingleChildScrollView(
245
            primary: true,
246
            child: Container(
247
              constraints: const BoxConstraints(maxHeight: 200.0),
248
              child: ListView(key: innerKey, primary: true),
249 250
            ),
          ),
251 252
        ),
      ),
253
    );
254

255
    final Scrollable innerScrollable = tester.widget(
256 257 258 259 260 261 262
      find.descendant(
        of: find.byKey(innerKey),
        matching: find.byType(Scrollable),
      ),
    );
    expect(innerScrollable.controller, isNull);
  });
263 264

  testWidgets('SingleChildScrollView semantics', (WidgetTester tester) async {
265 266
    final SemanticsTester semantics = SemanticsTester(tester);
    final ScrollController controller = ScrollController();
267 268

    await tester.pumpWidget(
269
      Directionality(
270
        textDirection: TextDirection.ltr,
271
        child: SingleChildScrollView(
272
          controller: controller,
273 274 275
          child: Column(
            children: List<Widget>.generate(30, (int i) {
              return Container(
276
                height: 200.0,
277
                child: Text('Tile $i'),
278 279 280 281 282 283 284 285
              );
            }),
          ),
        ),
      ),
    );

    expect(semantics, hasSemantics(
286
      TestSemantics(
287
        children: <TestSemantics>[
288
          TestSemantics(
289 290 291
            flags: <SemanticsFlag>[
              SemanticsFlag.hasImplicitScrolling,
            ],
292 293 294 295
            actions: <SemanticsAction>[
              SemanticsAction.scrollUp,
            ],
            children: <TestSemantics>[
296
              TestSemantics(
297 298 299
                label: r'Tile 0',
                textDirection: TextDirection.ltr,
              ),
300
              TestSemantics(
301 302 303
                label: r'Tile 1',
                textDirection: TextDirection.ltr,
              ),
304
              TestSemantics(
305 306 307
                label: r'Tile 2',
                textDirection: TextDirection.ltr,
              ),
308
              TestSemantics(
309 310 311 312 313 314
                flags: <SemanticsFlag>[
                  SemanticsFlag.isHidden,
                ],
                label: r'Tile 3',
                textDirection: TextDirection.ltr,
              ),
315
              TestSemantics(
316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331
                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(
332
      TestSemantics(
333
        children: <TestSemantics>[
334
          TestSemantics(
335 336 337
            flags: <SemanticsFlag>[
              SemanticsFlag.hasImplicitScrolling,
            ],
338 339 340 341 342
            actions: <SemanticsAction>[
              SemanticsAction.scrollUp,
              SemanticsAction.scrollDown,
            ],
            children: <TestSemantics>[
343
              TestSemantics(
344 345 346 347 348 349
                flags: <SemanticsFlag>[
                  SemanticsFlag.isHidden,
                ],
                label: r'Tile 13',
                textDirection: TextDirection.ltr,
              ),
350
              TestSemantics(
351 352 353 354 355 356
                flags: <SemanticsFlag>[
                  SemanticsFlag.isHidden,
                ],
                label: r'Tile 14',
                textDirection: TextDirection.ltr,
              ),
357
              TestSemantics(
358 359 360
                label: r'Tile 15',
                textDirection: TextDirection.ltr,
              ),
361
              TestSemantics(
362 363 364
                label: r'Tile 16',
                textDirection: TextDirection.ltr,
              ),
365
              TestSemantics(
366 367 368
                label: r'Tile 17',
                textDirection: TextDirection.ltr,
              ),
369
              TestSemantics(
370 371 372 373 374 375
                flags: <SemanticsFlag>[
                  SemanticsFlag.isHidden,
                ],
                label: r'Tile 18',
                textDirection: TextDirection.ltr,
              ),
376
              TestSemantics(
377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393
                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(
394
      TestSemantics(
395
        children: <TestSemantics>[
396
          TestSemantics(
397 398 399
            flags: <SemanticsFlag>[
              SemanticsFlag.hasImplicitScrolling,
            ],
400 401 402 403
            actions: <SemanticsAction>[
              SemanticsAction.scrollDown,
            ],
            children: <TestSemantics>[
404
              TestSemantics(
405 406 407 408 409 410
                flags: <SemanticsFlag>[
                  SemanticsFlag.isHidden,
                ],
                label: r'Tile 25',
                textDirection: TextDirection.ltr,
              ),
411
              TestSemantics(
412 413 414 415 416 417
                flags: <SemanticsFlag>[
                  SemanticsFlag.isHidden,
                ],
                label: r'Tile 26',
                textDirection: TextDirection.ltr,
              ),
418
              TestSemantics(
419 420 421
                label: r'Tile 27',
                textDirection: TextDirection.ltr,
              ),
422
              TestSemantics(
423 424 425
                label: r'Tile 28',
                textDirection: TextDirection.ltr,
              ),
426
              TestSemantics(
427 428 429 430 431 432 433 434 435 436 437 438
                label: r'Tile 29',
                textDirection: TextDirection.ltr,
              ),
            ],
          ),
        ],
      ),
      ignoreRect: true, ignoreTransform: true, ignoreId: true,
    ));

    semantics.dispose();
  });
439 440 441 442

  testWidgets('SingleChildScrollView getOffsetToReveal - down', (WidgetTester tester) async {
    List<Widget> children;
    await tester.pumpWidget(
443
      Directionality(
444
        textDirection: TextDirection.ltr,
445
        child: Center(
446 447 448
          child: Container(
            height: 200.0,
            width: 300.0,
449 450 451 452 453
            child: SingleChildScrollView(
              controller: ScrollController(initialScrollOffset: 300.0),
              child: Column(
                children: children = List<Widget>.generate(20, (int i) {
                  return Container(
454 455
                    height: 100.0,
                    width: 300.0,
456
                    child: Text('Tile $i'),
457 458 459 460 461 462 463 464 465
                  );
                }),
              ),
            ),
          ),
        ),
      ),
    );

466
    final RenderAbstractViewport viewport = tester.allRenderObjects.whereType<RenderAbstractViewport>().first;
467 468 469 470

    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
471
    expect(revealed.rect, const Rect.fromLTWH(0.0, 0.0, 300.0, 100.0));
472 473 474

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

Dan Field's avatar
Dan Field committed
477
    revealed = viewport.getOffsetToReveal(target, 0.0, rect: const Rect.fromLTWH(40.0, 40.0, 10.0, 10.0));
478
    expect(revealed.offset, 540.0);
Dan Field's avatar
Dan Field committed
479
    expect(revealed.rect, const Rect.fromLTWH(40.0, 0.0, 10.0, 10.0));
480

Dan Field's avatar
Dan Field committed
481
    revealed = viewport.getOffsetToReveal(target, 1.0, rect: const Rect.fromLTWH(40.0, 40.0, 10.0, 10.0));
482
    expect(revealed.offset, 350.0);
Dan Field's avatar
Dan Field committed
483
    expect(revealed.rect, const Rect.fromLTWH(40.0, 190.0, 10.0, 10.0));
484 485 486
  });

  testWidgets('SingleChildScrollView getOffsetToReveal - up', (WidgetTester tester) async {
487 488
    final List<Widget> children = List<Widget>.generate(20, (int i) {
      return Container(
489 490
        height: 100.0,
        width: 300.0,
491
        child: Text('Tile $i'),
492 493 494
      );
    });
    await tester.pumpWidget(
495
      Directionality(
496
        textDirection: TextDirection.ltr,
497
        child: Center(
498 499 500
          child: Container(
            height: 200.0,
            width: 300.0,
501 502
            child: SingleChildScrollView(
              controller: ScrollController(initialScrollOffset: 300.0),
503
              reverse: true,
504
              child: Column(
505 506 507 508 509 510 511 512
                children: children.reversed.toList(),
              ),
            ),
          ),
        ),
      ),
    );

513
    final RenderAbstractViewport viewport = tester.allRenderObjects.whereType<RenderAbstractViewport>().first;
514 515 516 517

    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
518
    expect(revealed.rect, const Rect.fromLTWH(0.0, 100.0, 300.0, 100.0));
519 520 521

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

Dan Field's avatar
Dan Field committed
524
    revealed = viewport.getOffsetToReveal(target, 0.0, rect: const Rect.fromLTWH(40.0, 40.0, 10.0, 10.0));
525
    expect(revealed.offset, 550.0);
Dan Field's avatar
Dan Field committed
526
    expect(revealed.rect, const Rect.fromLTWH(40.0, 190.0, 10.0, 10.0));
527

Dan Field's avatar
Dan Field committed
528
    revealed = viewport.getOffsetToReveal(target, 1.0, rect: const Rect.fromLTWH(40.0, 40.0, 10.0, 10.0));
529
    expect(revealed.offset, 360.0);
Dan Field's avatar
Dan Field committed
530
    expect(revealed.rect, const Rect.fromLTWH(40.0, 0.0, 10.0, 10.0));
531 532 533 534 535 536
  });

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

    await tester.pumpWidget(
537
      Directionality(
538
        textDirection: TextDirection.ltr,
539
        child: Center(
540 541 542
          child: Container(
            height: 300.0,
            width: 200.0,
543
            child: SingleChildScrollView(
544
              scrollDirection: Axis.horizontal,
545 546 547 548
              controller: ScrollController(initialScrollOffset: 300.0),
              child: Row(
                children: children = List<Widget>.generate(20, (int i) {
                  return Container(
549 550
                    height: 300.0,
                    width: 100.0,
551
                    child: Text('Tile $i'),
552 553 554 555 556 557 558 559 560
                  );
                }),
              ),
            ),
          ),
        ),
      ),
    );

561
    final RenderAbstractViewport viewport = tester.allRenderObjects.whereType<RenderAbstractViewport>().first;
562 563 564 565

    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
566
    expect(revealed.rect, const Rect.fromLTWH(0.0, 0.0, 100.0, 300.0));
567 568 569

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

Dan Field's avatar
Dan Field committed
572
    revealed = viewport.getOffsetToReveal(target, 0.0, rect: const Rect.fromLTWH(40.0, 40.0, 10.0, 10.0));
573
    expect(revealed.offset, 540.0);
Dan Field's avatar
Dan Field committed
574
    expect(revealed.rect, const Rect.fromLTWH(0.0, 40.0, 10.0, 10.0));
575

Dan Field's avatar
Dan Field committed
576
    revealed = viewport.getOffsetToReveal(target, 1.0, rect: const Rect.fromLTWH(40.0, 40.0, 10.0, 10.0));
577
    expect(revealed.offset, 350.0);
Dan Field's avatar
Dan Field committed
578
    expect(revealed.rect, const Rect.fromLTWH(190.0, 40.0, 10.0, 10.0));
579 580 581
  });

  testWidgets('SingleChildScrollView getOffsetToReveal - left', (WidgetTester tester) async {
582 583
    final List<Widget> children = List<Widget>.generate(20, (int i) {
      return Container(
584 585
        height: 300.0,
        width: 100.0,
586
        child: Text('Tile $i'),
587 588 589 590
      );
    });

    await tester.pumpWidget(
591
      Directionality(
592
        textDirection: TextDirection.ltr,
593
        child: Center(
594 595 596
          child: Container(
            height: 300.0,
            width: 200.0,
597
            child: SingleChildScrollView(
598 599
              scrollDirection: Axis.horizontal,
              reverse: true,
600 601
              controller: ScrollController(initialScrollOffset: 300.0),
              child: Row(
602 603 604 605 606 607 608 609
                children: children.reversed.toList(),
              ),
            ),
          ),
        ),
      ),
    );

610
    final RenderAbstractViewport viewport = tester.allRenderObjects.whereType<RenderAbstractViewport>().first;
611 612 613 614

    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
615
    expect(revealed.rect, const Rect.fromLTWH(100.0, 0.0, 100.0, 300.0));
616 617 618

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

Dan Field's avatar
Dan Field committed
621
    revealed = viewport.getOffsetToReveal(target, 0.0, rect: const Rect.fromLTWH(40.0, 40.0, 10.0, 10.0));
622
    expect(revealed.offset, 550.0);
Dan Field's avatar
Dan Field committed
623
    expect(revealed.rect, const Rect.fromLTWH(190.0, 40.0, 10.0, 10.0));
624

Dan Field's avatar
Dan Field committed
625
    revealed = viewport.getOffsetToReveal(target, 1.0, rect: const Rect.fromLTWH(40.0, 40.0, 10.0, 10.0));
626
    expect(revealed.offset, 360.0);
Dan Field's avatar
Dan Field committed
627
    expect(revealed.rect, const Rect.fromLTWH(0.0, 40.0, 10.0, 10.0));
628 629 630
  });

  testWidgets('Nested SingleChildScrollView showOnScreen', (WidgetTester tester) async {
631 632 633 634 635 636 637 638 639
    final List<List<Widget>> children = List<List<Widget>>.generate(10, (int x) {
      return List<Widget>.generate(10, (int y) {
        return SizedBox(
          key: UniqueKey(),
          height: 100.0,
          width: 100.0,
        );
      });
    });
640 641 642 643 644 645 646 647 648 649 650 651 652 653 654 655 656 657 658 659 660 661
    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(
662
      Directionality(
663
        textDirection: TextDirection.ltr,
664
        child: Center(
665 666 667
          child: Container(
            height: 200.0,
            width: 200.0,
668 669 670 671
            child: SingleChildScrollView(
              controller: controllerY = ScrollController(initialScrollOffset: 400.0),
              child: SingleChildScrollView(
                controller: controllerX = ScrollController(initialScrollOffset: 400.0),
672
                scrollDirection: Axis.horizontal,
673
                child: Column(
674
                  children: children.map((List<Widget> widgets) {
675
                    return Row(
676
                      children: widgets,
677
                    );
678
                  }).toList(),
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 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 792 793 794
                ),
              ),
            ),
          ),
        ),
      ),
    );

    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', () {
795
    late List<Widget> children;
796

797
    Future<void> buildNestedScroller({ required WidgetTester tester, ScrollController? inner, ScrollController? outer }) {
798
      return tester.pumpWidget(
799
        Directionality(
800
          textDirection: TextDirection.ltr,
801
          child: Center(
802 803 804
            child: Container(
              height: 200.0,
              width: 300.0,
805
              child: SingleChildScrollView(
806
                controller: outer,
807
                child: Column(
808
                  children: <Widget>[
809
                    Container(
810 811
                      height: 200.0,
                    ),
812
                    Container(
813 814
                      height: 200.0,
                      width: 300.0,
815
                      child: SingleChildScrollView(
816
                        controller: inner,
817 818 819
                        child: Column(
                          children: children = List<Widget>.generate(10, (int i) {
                            return Container(
820 821
                              height: 100.0,
                              width: 300.0,
822
                              child: Text('$i'),
823 824 825 826 827
                            );
                          }),
                        ),
                      ),
                    ),
828
                    Container(
829
                      height: 200.0,
830
                    ),
831 832 833 834 835 836 837 838 839 840
                  ],
                ),
              ),
            ),
          ),
        ),
      );
    }

    testWidgets('in view in inner, but not in outer', (WidgetTester tester) async {
841 842
      final ScrollController inner = ScrollController();
      final ScrollController outer = ScrollController();
843 844 845 846 847 848 849 850 851 852 853 854 855 856 857
      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 {
858 859
      final ScrollController inner = ScrollController();
      final ScrollController outer = ScrollController();
860 861 862 863 864 865 866 867 868 869 870 871 872 873 874
      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 {
875 876
      final ScrollController inner = ScrollController(initialScrollOffset: 200.0);
      final ScrollController outer = ScrollController(initialScrollOffset: 200.0);
877 878 879 880 881 882 883 884 885 886 887 888 889 890 891
      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 {
892 893
      final ScrollController inner = ScrollController(initialScrollOffset: 200.0);
      final ScrollController outer = ScrollController(initialScrollOffset: 200.0);
894 895 896 897 898 899 900 901 902 903 904 905 906 907 908
      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 {
909 910
      final ScrollController inner = ScrollController();
      final ScrollController outer = ScrollController(initialScrollOffset: 100.0);
911 912 913 914 915 916 917 918 919 920 921 922 923 924
      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);
    });
  });
925
}