single_child_scroll_view_test.dart 33.5 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
import 'package:flutter/material.dart';
6
import 'package:flutter/rendering.dart';
7 8
import 'package:flutter_test/flutter_test.dart';

9
import '../rendering/rendering_tester.dart' show TestClipPaintingContext;
10 11
import 'semantics_tester.dart';

12
class TestScrollPosition extends ScrollPositionWithSingleContext {
13
  TestScrollPosition({
14
    required super.physics,
15
    required ScrollContext state,
16 17
    double super.initialPixels,
    super.oldPosition,
18
  }) : super(
19
    context: state,
20 21 22 23 24
  );
}

class TestScrollController extends ScrollController {
  @override
25
  ScrollPosition createScrollPosition(ScrollPhysics physics, ScrollContext context, ScrollPosition? oldPosition) {
26
    return TestScrollPosition(
27
      physics: physics,
28
      state: context,
29 30 31 32 33 34
      initialPixels: initialScrollOffset,
      oldPosition: oldPosition,
    );
  }
}

35 36 37 38 39 40 41 42 43 44 45 46 47
Widget primaryScrollControllerBoilerplate({ required Widget child, required ScrollController controller }) {
  return Directionality(
    textDirection: TextDirection.ltr,
    child: MediaQuery(
      data: const MediaQueryData(),
      child: PrimaryScrollController(
        controller: controller,
        child: child,
      ),
    ),
  );
}

48
void main() {
49 50 51 52 53 54
  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(
55
          child: Container(height: 600.0),
56 57
        ),
      ),
58 59 60
    );

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

    // 2nd, height == widow.height test: check that the painting context does not call pushClipRect .
    TestClipPaintingContext context = TestClipPaintingContext();
66
    renderObject.paint(context, Offset.zero); // ignore: avoid_dynamic_calls
67 68 69 70 71 72 73
    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(
74 75 76
          child: Container(height: 600.1),
        ),
      ),
77
    );
78
    renderObject.paint(context, Offset.zero); // ignore: avoid_dynamic_calls
79 80 81 82 83 84 85 86 87 88
    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,
89 90 91
          child: Container(width: 800.0),
        ),
      ),
92
    );
93
    renderObject.paint(context, Offset.zero); // ignore: avoid_dynamic_calls
94 95 96 97 98 99 100 101
    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,
102 103 104
          child: Container(width: 800.1),
        ),
      ),
105
    );
106
    renderObject.paint(context, Offset.zero); // ignore: avoid_dynamic_calls
107 108 109
    expect(context.clipBehavior, equals(Clip.hardEdge));
  });

110 111 112 113
  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.
114
    final dynamic renderObject = tester.allRenderObjects.where((RenderObject o) => o.runtimeType.toString() == '_RenderSingleChildViewport').first;
115
    expect(renderObject.clipBehavior, equals(Clip.hardEdge)); // ignore: avoid_dynamic_calls
116 117 118

    // 2nd, check that the painting context has received the default clip behavior.
    final TestClipPaintingContext context = TestClipPaintingContext();
119
    renderObject.paint(context, Offset.zero); // ignore: avoid_dynamic_calls
120 121 122 123
    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)));
124
    expect(renderObject.clipBehavior, equals(Clip.antiAlias)); // ignore: avoid_dynamic_calls
125 126

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

131
  testWidgets('SingleChildScrollView control test', (WidgetTester tester) async {
132 133
    await tester.pumpWidget(SingleChildScrollView(
      child: Container(
134
        height: 2000.0,
135
        color: const Color(0xFF00FF00),
136 137 138
      ),
    ));

139
    final RenderBox box = tester.renderObject(find.byType(Container));
140
    expect(box.localToGlobal(Offset.zero), equals(Offset.zero));
141

142
    await tester.drag(find.byType(SingleChildScrollView), const Offset(-200.0, -200.0));
143

144
    expect(box.localToGlobal(Offset.zero), equals(const Offset(0.0, -200.0)));
145
  });
146 147

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

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

157
    await tester.pumpWidget(SingleChildScrollView(
158
      controller: controller,
159
      child: Container(
160
        height: 2000.0,
161
        color: const Color(0xFF00FF00),
162 163 164
      ),
    ));

165
    final ScrollableState scrollable = tester.state(find.byType(Scrollable));
Dan Field's avatar
Dan Field committed
166
    expect(scrollable.position, isA<TestScrollPosition>());
167 168
  });

169
  testWidgets('Sets PrimaryScrollController when primary', (WidgetTester tester) async {
170 171
    final ScrollController primaryScrollController = ScrollController();
    await tester.pumpWidget(PrimaryScrollController(
172
      controller: primaryScrollController,
173
      child: SingleChildScrollView(
174
        primary: true,
175
        child: Container(
176
          height: 2000.0,
177
          color: const Color(0xFF00FF00),
178 179 180 181
        ),
      ),
    ));

182
    final Scrollable scrollable = tester.widget(find.byType(Scrollable));
183 184 185 186
    expect(scrollable.controller, primaryScrollController);
  });


187
  testWidgets('Changing scroll controller inside dirty layout builder does not assert', (WidgetTester tester) async {
188
    final ScrollController controller = ScrollController();
189

190 191
    await tester.pumpWidget(Center(
      child: SizedBox(
192
        width: 750.0,
193
        child: LayoutBuilder(
194
          builder: (BuildContext context, BoxConstraints constraints) {
195 196
            return SingleChildScrollView(
              child: Container(
197
                height: 2000.0,
198
                color: const Color(0xFF00FF00),
199 200 201 202 203 204 205
              ),
            );
          },
        ),
      ),
    ));

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

224
  testWidgets('Vertical SingleChildScrollViews are not primary by default', (WidgetTester tester) async {
225
    const SingleChildScrollView view = SingleChildScrollView();
226
    expect(view.primary, isNull);
227 228
  });

229
  testWidgets('Horizontal SingleChildScrollViews are not primary by default', (WidgetTester tester) async {
230
    const SingleChildScrollView view = SingleChildScrollView(scrollDirection: Axis.horizontal);
231
    expect(view.primary, isNull);
232 233
  });

234
  testWidgets('SingleChildScrollViews with controllers are not primary by default', (WidgetTester tester) async {
235 236
    final SingleChildScrollView view = SingleChildScrollView(
      controller: ScrollController(),
237
    );
238
    expect(view.primary, isNull);
239 240
  });

241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258
  testWidgets('Vertical SingleChildScrollViews use PrimaryScrollController by default on mobile', (WidgetTester tester) async {
    final ScrollController controller = ScrollController();
    await tester.pumpWidget(primaryScrollControllerBoilerplate(
      child: const SingleChildScrollView(),
      controller: controller,
    ));
    expect(controller.hasClients, isTrue);
  }, variant: TargetPlatformVariant.mobile());

  testWidgets("Vertical SingleChildScrollViews don't use PrimaryScrollController by default on desktop", (WidgetTester tester) async {
    final ScrollController controller = ScrollController();
    await tester.pumpWidget(primaryScrollControllerBoilerplate(
      child: const SingleChildScrollView(),
      controller: controller,
    ));
    expect(controller.hasClients, isFalse);
  }, variant: TargetPlatformVariant.desktop());

259
  testWidgets('Nested scrollables have a null PrimaryScrollController', (WidgetTester tester) async {
260
    const Key innerKey = Key('inner');
261
    final ScrollController primaryScrollController = ScrollController();
262
    await tester.pumpWidget(
263
      Directionality(
264
        textDirection: TextDirection.ltr,
265
        child: PrimaryScrollController(
266
          controller: primaryScrollController,
267
          child: SingleChildScrollView(
268
            primary: true,
269
            child: Container(
270
              constraints: const BoxConstraints(maxHeight: 200.0),
271
              child: ListView(key: innerKey, primary: true),
272 273
            ),
          ),
274 275
        ),
      ),
276
    );
277

278
    final Scrollable innerScrollable = tester.widget(
279 280 281 282 283 284 285
      find.descendant(
        of: find.byKey(innerKey),
        matching: find.byType(Scrollable),
      ),
    );
    expect(innerScrollable.controller, isNull);
  });
286 287

  testWidgets('SingleChildScrollView semantics', (WidgetTester tester) async {
288 289
    final SemanticsTester semantics = SemanticsTester(tester);
    final ScrollController controller = ScrollController();
290 291

    await tester.pumpWidget(
292
      Directionality(
293
        textDirection: TextDirection.ltr,
294
        child: SingleChildScrollView(
295
          controller: controller,
296 297
          child: Column(
            children: List<Widget>.generate(30, (int i) {
298
              return SizedBox(
299
                height: 200.0,
300
                child: Text('Tile $i'),
301 302 303 304 305 306 307 308
              );
            }),
          ),
        ),
      ),
    );

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

    semantics.dispose();
  });
462 463 464 465

  testWidgets('SingleChildScrollView getOffsetToReveal - down', (WidgetTester tester) async {
    List<Widget> children;
    await tester.pumpWidget(
466
      Directionality(
467
        textDirection: TextDirection.ltr,
468
        child: Center(
469
          child: SizedBox(
470 471
            height: 200.0,
            width: 300.0,
472 473 474 475
            child: SingleChildScrollView(
              controller: ScrollController(initialScrollOffset: 300.0),
              child: Column(
                children: children = List<Widget>.generate(20, (int i) {
476
                  return SizedBox(
477 478
                    height: 100.0,
                    width: 300.0,
479
                    child: Text('Tile $i'),
480 481 482 483 484 485 486 487 488
                  );
                }),
              ),
            ),
          ),
        ),
      ),
    );

489
    final RenderAbstractViewport viewport = tester.allRenderObjects.whereType<RenderAbstractViewport>().first;
490 491 492 493

    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
494
    expect(revealed.rect, const Rect.fromLTWH(0.0, 0.0, 300.0, 100.0));
495 496 497

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

Dan Field's avatar
Dan Field committed
500
    revealed = viewport.getOffsetToReveal(target, 0.0, rect: const Rect.fromLTWH(40.0, 40.0, 10.0, 10.0));
501
    expect(revealed.offset, 540.0);
Dan Field's avatar
Dan Field committed
502
    expect(revealed.rect, const Rect.fromLTWH(40.0, 0.0, 10.0, 10.0));
503

Dan Field's avatar
Dan Field committed
504
    revealed = viewport.getOffsetToReveal(target, 1.0, rect: const Rect.fromLTWH(40.0, 40.0, 10.0, 10.0));
505
    expect(revealed.offset, 350.0);
Dan Field's avatar
Dan Field committed
506
    expect(revealed.rect, const Rect.fromLTWH(40.0, 190.0, 10.0, 10.0));
507 508 509
  });

  testWidgets('SingleChildScrollView getOffsetToReveal - up', (WidgetTester tester) async {
510
    final List<Widget> children = List<Widget>.generate(20, (int i) {
511
      return SizedBox(
512 513
        height: 100.0,
        width: 300.0,
514
        child: Text('Tile $i'),
515 516 517
      );
    });
    await tester.pumpWidget(
518
      Directionality(
519
        textDirection: TextDirection.ltr,
520
        child: Center(
521
          child: SizedBox(
522 523
            height: 200.0,
            width: 300.0,
524 525
            child: SingleChildScrollView(
              controller: ScrollController(initialScrollOffset: 300.0),
526
              reverse: true,
527
              child: Column(
528 529 530 531 532 533 534 535
                children: children.reversed.toList(),
              ),
            ),
          ),
        ),
      ),
    );

536
    final RenderAbstractViewport viewport = tester.allRenderObjects.whereType<RenderAbstractViewport>().first;
537 538 539 540

    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
541
    expect(revealed.rect, const Rect.fromLTWH(0.0, 100.0, 300.0, 100.0));
542 543 544

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

Dan Field's avatar
Dan Field committed
547
    revealed = viewport.getOffsetToReveal(target, 0.0, rect: const Rect.fromLTWH(40.0, 40.0, 10.0, 10.0));
548
    expect(revealed.offset, 550.0);
Dan Field's avatar
Dan Field committed
549
    expect(revealed.rect, const Rect.fromLTWH(40.0, 190.0, 10.0, 10.0));
550

Dan Field's avatar
Dan Field committed
551
    revealed = viewport.getOffsetToReveal(target, 1.0, rect: const Rect.fromLTWH(40.0, 40.0, 10.0, 10.0));
552
    expect(revealed.offset, 360.0);
Dan Field's avatar
Dan Field committed
553
    expect(revealed.rect, const Rect.fromLTWH(40.0, 0.0, 10.0, 10.0));
554 555 556 557 558 559
  });

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

    await tester.pumpWidget(
560
      Directionality(
561
        textDirection: TextDirection.ltr,
562
        child: Center(
563
          child: SizedBox(
564 565
            height: 300.0,
            width: 200.0,
566
            child: SingleChildScrollView(
567
              scrollDirection: Axis.horizontal,
568 569 570
              controller: ScrollController(initialScrollOffset: 300.0),
              child: Row(
                children: children = List<Widget>.generate(20, (int i) {
571
                  return SizedBox(
572 573
                    height: 300.0,
                    width: 100.0,
574
                    child: Text('Tile $i'),
575 576 577 578 579 580 581 582 583
                  );
                }),
              ),
            ),
          ),
        ),
      ),
    );

584
    final RenderAbstractViewport viewport = tester.allRenderObjects.whereType<RenderAbstractViewport>().first;
585 586 587 588

    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
589
    expect(revealed.rect, const Rect.fromLTWH(0.0, 0.0, 100.0, 300.0));
590 591 592

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

Dan Field's avatar
Dan Field committed
595
    revealed = viewport.getOffsetToReveal(target, 0.0, rect: const Rect.fromLTWH(40.0, 40.0, 10.0, 10.0));
596
    expect(revealed.offset, 540.0);
Dan Field's avatar
Dan Field committed
597
    expect(revealed.rect, const Rect.fromLTWH(0.0, 40.0, 10.0, 10.0));
598

Dan Field's avatar
Dan Field committed
599
    revealed = viewport.getOffsetToReveal(target, 1.0, rect: const Rect.fromLTWH(40.0, 40.0, 10.0, 10.0));
600
    expect(revealed.offset, 350.0);
Dan Field's avatar
Dan Field committed
601
    expect(revealed.rect, const Rect.fromLTWH(190.0, 40.0, 10.0, 10.0));
602 603 604
  });

  testWidgets('SingleChildScrollView getOffsetToReveal - left', (WidgetTester tester) async {
605
    final List<Widget> children = List<Widget>.generate(20, (int i) {
606
      return SizedBox(
607 608
        height: 300.0,
        width: 100.0,
609
        child: Text('Tile $i'),
610 611 612 613
      );
    });

    await tester.pumpWidget(
614
      Directionality(
615
        textDirection: TextDirection.ltr,
616
        child: Center(
617
          child: SizedBox(
618 619
            height: 300.0,
            width: 200.0,
620
            child: SingleChildScrollView(
621 622
              scrollDirection: Axis.horizontal,
              reverse: true,
623 624
              controller: ScrollController(initialScrollOffset: 300.0),
              child: Row(
625 626 627 628 629 630 631 632
                children: children.reversed.toList(),
              ),
            ),
          ),
        ),
      ),
    );

633
    final RenderAbstractViewport viewport = tester.allRenderObjects.whereType<RenderAbstractViewport>().first;
634 635 636 637

    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
638
    expect(revealed.rect, const Rect.fromLTWH(100.0, 0.0, 100.0, 300.0));
639 640 641

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

Dan Field's avatar
Dan Field committed
644
    revealed = viewport.getOffsetToReveal(target, 0.0, rect: const Rect.fromLTWH(40.0, 40.0, 10.0, 10.0));
645
    expect(revealed.offset, 550.0);
Dan Field's avatar
Dan Field committed
646
    expect(revealed.rect, const Rect.fromLTWH(190.0, 40.0, 10.0, 10.0));
647

Dan Field's avatar
Dan Field committed
648
    revealed = viewport.getOffsetToReveal(target, 1.0, rect: const Rect.fromLTWH(40.0, 40.0, 10.0, 10.0));
649
    expect(revealed.offset, 360.0);
Dan Field's avatar
Dan Field committed
650
    expect(revealed.rect, const Rect.fromLTWH(0.0, 40.0, 10.0, 10.0));
651 652 653
  });

  testWidgets('Nested SingleChildScrollView showOnScreen', (WidgetTester tester) async {
654 655 656 657 658 659 660 661 662
    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,
        );
      });
    });
663 664 665 666 667 668 669 670 671 672 673 674 675 676 677 678 679 680 681 682 683 684
    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(
685
      Directionality(
686
        textDirection: TextDirection.ltr,
687
        child: Center(
688
          child: SizedBox(
689 690
            height: 200.0,
            width: 200.0,
691 692 693 694
            child: SingleChildScrollView(
              controller: controllerY = ScrollController(initialScrollOffset: 400.0),
              child: SingleChildScrollView(
                controller: controllerX = ScrollController(initialScrollOffset: 400.0),
695
                scrollDirection: Axis.horizontal,
696
                child: Column(
697
                  children: children.map((List<Widget> widgets) {
698
                    return Row(
699
                      children: widgets,
700
                    );
701
                  }).toList(),
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 795 796 797 798 799 800 801 802 803 804 805 806 807 808 809 810 811 812 813 814 815 816 817
                ),
              ),
            ),
          ),
        ),
      ),
    );

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

820
    Future<void> buildNestedScroller({ required WidgetTester tester, ScrollController? inner, ScrollController? outer }) {
821
      return tester.pumpWidget(
822
        Directionality(
823
          textDirection: TextDirection.ltr,
824
          child: Center(
825
            child: SizedBox(
826 827
              height: 200.0,
              width: 300.0,
828
              child: SingleChildScrollView(
829
                controller: outer,
830
                child: Column(
831
                  children: <Widget>[
832
                    const SizedBox(
833 834
                      height: 200.0,
                    ),
835
                    SizedBox(
836 837
                      height: 200.0,
                      width: 300.0,
838
                      child: SingleChildScrollView(
839
                        controller: inner,
840 841
                        child: Column(
                          children: children = List<Widget>.generate(10, (int i) {
842
                            return SizedBox(
843 844
                              height: 100.0,
                              width: 300.0,
845
                              child: Text('$i'),
846 847 848 849 850
                            );
                          }),
                        ),
                      ),
                    ),
851
                    const SizedBox(
852
                      height: 200.0,
853
                    ),
854 855 856 857 858 859 860 861 862 863
                  ],
                ),
              ),
            ),
          ),
        ),
      );
    }

    testWidgets('in view in inner, but not in outer', (WidgetTester tester) async {
864 865
      final ScrollController inner = ScrollController();
      final ScrollController outer = ScrollController();
866 867 868 869 870 871 872 873 874 875 876 877 878 879 880
      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 {
881 882
      final ScrollController inner = ScrollController();
      final ScrollController outer = ScrollController();
883 884 885 886 887 888 889 890 891 892 893 894 895 896 897
      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 {
898 899
      final ScrollController inner = ScrollController(initialScrollOffset: 200.0);
      final ScrollController outer = ScrollController(initialScrollOffset: 200.0);
900 901 902 903 904 905 906 907 908 909 910 911 912 913 914
      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 {
915 916
      final ScrollController inner = ScrollController(initialScrollOffset: 200.0);
      final ScrollController outer = ScrollController(initialScrollOffset: 200.0);
917 918 919 920 921 922 923 924 925 926 927 928 929 930 931
      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 {
932 933
      final ScrollController inner = ScrollController();
      final ScrollController outer = ScrollController(initialScrollOffset: 100.0);
934 935 936 937 938 939 940 941 942 943 944 945 946 947
      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);
    });
  });
948 949 950 951 952 953 954 955 956 957 958 959 960

  testWidgets('keyboardDismissBehavior tests', (WidgetTester tester) async {
    final List<FocusNode> focusNodes = List<FocusNode>.generate(50, (int i) => FocusNode());

    Future<void> boilerplate(ScrollViewKeyboardDismissBehavior behavior) {
      return tester.pumpWidget(
        MaterialApp(
          home: Scaffold(
            body: SingleChildScrollView(
              padding: EdgeInsets.zero,
              keyboardDismissBehavior: behavior,
              child: Column(
                children: focusNodes.map((FocusNode focusNode) {
961
                  return SizedBox(
962 963 964 965 966 967 968 969 970 971 972 973 974 975 976 977 978 979 980 981 982 983 984 985 986 987 988 989 990 991 992 993 994 995 996
                    height: 50,
                    child: TextField(focusNode: focusNode),
                  );
                }).toList(),
              ),
            ),
          ),
        ),
      );
    }

    // ScrollViewKeyboardDismissBehavior.onDrag dismiss keyboard on drag
    await boilerplate(ScrollViewKeyboardDismissBehavior.onDrag);

    Finder finder = find.byType(TextField).first;
    TextField textField = tester.widget(finder);
    await tester.showKeyboard(finder);
    expect(textField.focusNode!.hasFocus, isTrue);

    await tester.drag(finder, const Offset(0.0, -40.0));
    await tester.pumpAndSettle();
    expect(textField.focusNode!.hasFocus, isFalse);

    // ScrollViewKeyboardDismissBehavior.manual does no dismiss the keyboard
    await boilerplate(ScrollViewKeyboardDismissBehavior.manual);

    finder = find.byType(TextField).first;
    textField = tester.widget(finder);
    await tester.showKeyboard(finder);
    expect(textField.focusNode!.hasFocus, isTrue);

    await tester.drag(finder, const Offset(0.0, -40.0));
    await tester.pumpAndSettle();
    expect(textField.focusNode!.hasFocus, isTrue);
  });
997
}