list_view_test.dart 23.8 KB
Newer Older
Ian Hickson's avatar
Ian Hickson committed
1
// Copyright 2014 The Flutter Authors. All rights reserved.
Adam Barth's avatar
Adam Barth committed
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 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
7
import 'package:flutter_test/flutter_test.dart';
Adam Barth's avatar
Adam Barth committed
8

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

12
class TestSliverChildListDelegate extends SliverChildListDelegate {
13
  TestSliverChildListDelegate(super.children);
14 15 16 17 18 19 20 21 22

  final List<String> log = <String>[];

  @override
  void didFinishLayout(int firstIndex, int lastIndex) {
    log.add('didFinishLayout firstIndex=$firstIndex lastIndex=$lastIndex');
  }
}

23
class Alive extends StatefulWidget {
24
  const Alive(this.alive, this.index, { super.key });
25 26 27 28
  final bool alive;
  final int index;

  @override
29
  AliveState createState() => AliveState();
30 31

  @override
32
  String toString({ DiagnosticLevel minLevel = DiagnosticLevel.info }) => '$index $alive';
33 34 35 36 37 38 39
}

class AliveState extends State<Alive> with AutomaticKeepAliveClientMixin {
  @override
  bool get wantKeepAlive => widget.alive;

  @override
40 41 42 43
  Widget build(BuildContext context) {
    super.build(context);
    return Text('${widget.index}:$wantKeepAlive');
  }
44 45 46 47 48 49 50 51
}

typedef WhetherToKeepAlive = bool Function(int);
class _StatefulListView extends StatefulWidget {
  const _StatefulListView(this.aliveCallback);

  final WhetherToKeepAlive aliveCallback;
  @override
52
  _StatefulListViewState createState() => _StatefulListViewState();
53 54 55 56 57
}

class _StatefulListViewState extends State<_StatefulListView> {
  @override
  Widget build(BuildContext context) {
58
    return GestureDetector(
59 60 61
      // force a rebuild - the test(s) using this are verifying that the list is
      // still correct after rebuild
      onTap: () => setState,
62
      child: Directionality(
63
        textDirection: TextDirection.ltr,
64 65 66
        child: ListView(
          children: List<Widget>.generate(200, (int i) {
            return Builder(
67
              builder: (BuildContext context) {
68
                return Alive(widget.aliveCallback(i), i);
69 70 71 72 73 74 75 76 77
              },
            );
          }),
        ),
      ),
    );
  }
}

Adam Barth's avatar
Adam Barth committed
78
void main() {
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 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151
  // Regression test for https://github.com/flutter/flutter/issues/100451
  testWidgets('ListView.builder respects findChildIndexCallback', (WidgetTester tester) async {
    bool finderCalled = false;
    int itemCount = 7;
    late StateSetter stateSetter;

    await tester.pumpWidget(
      Directionality(
        textDirection: TextDirection.ltr,
        child: StatefulBuilder(
          builder: (BuildContext context, StateSetter setState) {
            stateSetter = setState;
            return ListView.builder(
              itemCount: itemCount,
              itemBuilder: (BuildContext _, int index) => Container(
                key: Key('$index'),
                height: 2000.0,
              ),
              findChildIndexCallback: (Key key) {
                finderCalled = true;
                return null;
              },
            );
          },
        ),
      )
    );
    expect(finderCalled, false);

    // Trigger update.
    stateSetter(() => itemCount = 77);
    await tester.pump();

    expect(finderCalled, true);
  });

  // Regression test for https://github.com/flutter/flutter/issues/100451
  testWidgets('ListView.separator respects findChildIndexCallback', (WidgetTester tester) async {
    bool finderCalled = false;
    int itemCount = 7;
    late StateSetter stateSetter;

    await tester.pumpWidget(
      Directionality(
        textDirection: TextDirection.ltr,
        child: StatefulBuilder(
          builder: (BuildContext context, StateSetter setState) {
            stateSetter = setState;
            return ListView.separated(
              itemCount: itemCount,
              itemBuilder: (BuildContext _, int index) => Container(
                key: Key('$index'),
                height: 2000.0,
              ),
              findChildIndexCallback: (Key key) {
                finderCalled = true;
                return null;
              },
              separatorBuilder: (BuildContext _, int __) => const Divider(),
            );
          },
        ),
      )
    );
    expect(finderCalled, false);

    // Trigger update.
    stateSetter(() => itemCount = 77);
    await tester.pump();

    expect(finderCalled, true);
  });

152
  testWidgets('ListView default control', (WidgetTester tester) async {
153
    await tester.pumpWidget(
154
      Directionality(
155
        textDirection: TextDirection.ltr,
156 157
        child: Center(
          child: ListView(itemExtent: 100.0),
158 159 160
        ),
      ),
    );
161 162
  });

163
  testWidgets('ListView itemExtent control test', (WidgetTester tester) async {
Adam Barth's avatar
Adam Barth committed
164
    await tester.pumpWidget(
165
      Directionality(
166
        textDirection: TextDirection.ltr,
167
        child: ListView(
168
          itemExtent: 200.0,
169 170
          children: List<Widget>.generate(20, (int i) {
            return Container(
171
              color: Colors.green,
172
              child: Text('$i'),
173 174 175
            );
          }),
        ),
Adam Barth's avatar
Adam Barth committed
176 177 178
      ),
    );

179
    final RenderBox box = tester.renderObject<RenderBox>(find.byType(Container).first);
Adam Barth's avatar
Adam Barth committed
180 181 182 183 184
    expect(box.size.height, equals(200.0));

    expect(find.text('0'), findsOneWidget);
    expect(find.text('1'), findsOneWidget);
    expect(find.text('2'), findsOneWidget);
Adam Barth's avatar
Adam Barth committed
185
    expect(find.text('3'), findsNothing);
Adam Barth's avatar
Adam Barth committed
186 187
    expect(find.text('4'), findsNothing);

188
    await tester.drag(find.byType(ListView), const Offset(0.0, -250.0));
Adam Barth's avatar
Adam Barth committed
189 190 191 192 193 194 195
    await tester.pump();

    expect(find.text('0'), findsNothing);
    expect(find.text('1'), findsOneWidget);
    expect(find.text('2'), findsOneWidget);
    expect(find.text('3'), findsOneWidget);
    expect(find.text('4'), findsOneWidget);
Adam Barth's avatar
Adam Barth committed
196
    expect(find.text('5'), findsNothing);
Adam Barth's avatar
Adam Barth committed
197 198
    expect(find.text('6'), findsNothing);

199
    await tester.drag(find.byType(ListView), const Offset(0.0, 200.0));
Adam Barth's avatar
Adam Barth committed
200 201 202 203 204 205
    await tester.pump();

    expect(find.text('0'), findsOneWidget);
    expect(find.text('1'), findsOneWidget);
    expect(find.text('2'), findsOneWidget);
    expect(find.text('3'), findsOneWidget);
Adam Barth's avatar
Adam Barth committed
206
    expect(find.text('4'), findsNothing);
Adam Barth's avatar
Adam Barth committed
207 208 209
    expect(find.text('5'), findsNothing);
  });

210
  testWidgets('ListView large scroll jump', (WidgetTester tester) async {
211
    final List<int> log = <int>[];
Adam Barth's avatar
Adam Barth committed
212 213

    await tester.pumpWidget(
214
      Directionality(
215
        textDirection: TextDirection.ltr,
216
        child: ListView(
217
          itemExtent: 200.0,
218 219
          children: List<Widget>.generate(20, (int i) {
            return Builder(
220 221
              builder: (BuildContext context) {
                log.add(i);
222
                return Text('$i');
223
              },
224 225 226
            );
          }),
        ),
Adam Barth's avatar
Adam Barth committed
227 228 229
      ),
    );

230
    expect(log, equals(<int>[0, 1, 2, 3, 4]));
Adam Barth's avatar
Adam Barth committed
231 232
    log.clear();

233 234
    final ScrollableState state = tester.state(find.byType(Scrollable));
    final ScrollPosition position = state.position;
Adam Barth's avatar
Adam Barth committed
235 236 237 238 239
    position.jumpTo(2025.0);

    expect(log, isEmpty);
    await tester.pump();

240
    expect(log, equals(<int>[8, 9, 10, 11, 12, 13, 14]));
Adam Barth's avatar
Adam Barth committed
241 242 243 244 245 246 247
    log.clear();

    position.jumpTo(975.0);

    expect(log, isEmpty);
    await tester.pump();

248
    expect(log, equals(<int>[7, 6, 5, 4, 3]));
Adam Barth's avatar
Adam Barth committed
249 250
    log.clear();
  });
251

252
  testWidgets('ListView large scroll jump and keepAlive first child not keepAlive', (WidgetTester tester) async {
253
    Future<void> checkAndScroll([ String zero = '0:false' ]) async {
254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281
      expect(find.text(zero), findsOneWidget);
      expect(find.text('1:false'), findsOneWidget);
      expect(find.text('2:false'), findsOneWidget);
      expect(find.text('3:true'), findsOneWidget);
      expect(find.text('116:false'), findsNothing);
      final ScrollableState state = tester.state(find.byType(Scrollable));
      final ScrollPosition position = state.position;
      position.jumpTo(1025.0);

      await tester.pump();

      expect(find.text(zero), findsNothing);
      expect(find.text('1:false'), findsNothing);
      expect(find.text('2:false'), findsNothing);
      expect(find.text('3:true', skipOffstage: false), findsOneWidget);
      expect(find.text('116:false'), findsOneWidget);

      await tester.tapAt(const Offset(100.0, 100.0));
      position.jumpTo(0.0);
      await tester.pump();
      await tester.pump();

      expect(find.text(zero), findsOneWidget);
      expect(find.text('1:false'), findsOneWidget);
      expect(find.text('2:false'), findsOneWidget);
      expect(find.text('3:true'), findsOneWidget);
    }

282
    await tester.pumpWidget(_StatefulListView((int i) => i > 2 && i % 3 == 0));
283 284
    await checkAndScroll();

285
    await tester.pumpWidget(_StatefulListView((int i) => i % 3 == 0));
286
    await checkAndScroll('0:true');
287
  });
288

289 290
  testWidgets('ListView can build out of underflow', (WidgetTester tester) async {
    await tester.pumpWidget(
291
      Directionality(
292
        textDirection: TextDirection.ltr,
293
        child: ListView(
294 295
          itemExtent: 100.0,
        ),
296 297 298 299 300 301 302 303 304 305 306
      ),
    );

    expect(find.text('0'), findsNothing);
    expect(find.text('1'), findsNothing);
    expect(find.text('2'), findsNothing);
    expect(find.text('3'), findsNothing);
    expect(find.text('4'), findsNothing);
    expect(find.text('5'), findsNothing);

    await tester.pumpWidget(
307
      Directionality(
308
        textDirection: TextDirection.ltr,
309
        child: ListView(
310
          itemExtent: 100.0,
311
          children: List<Widget>.generate(2, (int i) {
312
            return Text('$i');
313 314
          }),
        ),
315 316 317 318 319 320 321 322 323 324 325
      ),
    );

    expect(find.text('0'), findsOneWidget);
    expect(find.text('1'), findsOneWidget);
    expect(find.text('2'), findsNothing);
    expect(find.text('3'), findsNothing);
    expect(find.text('4'), findsNothing);
    expect(find.text('5'), findsNothing);

    await tester.pumpWidget(
326
      Directionality(
327
        textDirection: TextDirection.ltr,
328
        child: ListView(
329
          itemExtent: 100.0,
330
          children: List<Widget>.generate(5, (int i) {
331
            return Text('$i');
332 333
          }),
        ),
334 335 336 337 338 339 340 341 342 343
      ),
    );

    expect(find.text('0'), findsOneWidget);
    expect(find.text('1'), findsOneWidget);
    expect(find.text('2'), findsOneWidget);
    expect(find.text('3'), findsOneWidget);
    expect(find.text('4'), findsOneWidget);
    expect(find.text('5'), findsNothing);
  });
344

345 346
  testWidgets('ListView can build out of overflow padding', (WidgetTester tester) async {
    await tester.pumpWidget(
347
      Directionality(
348
        textDirection: TextDirection.ltr,
349 350
        child: Center(
          child: SizedBox(
351 352
            width: 0.0,
            height: 0.0,
353
            child: ListView(
354
              padding: const EdgeInsets.all(8.0),
355
              children: const <Widget>[
356
                Text('padded', textDirection: TextDirection.ltr),
357 358
              ],
            ),
359 360 361 362
          ),
        ),
      ),
    );
363
    expect(find.text('padded', skipOffstage: false), findsOneWidget);
364 365
  });

366 367
  testWidgets('ListView with itemExtent in unbounded context', (WidgetTester tester) async {
    await tester.pumpWidget(
368
      Directionality(
369
        textDirection: TextDirection.ltr,
370 371
        child: SingleChildScrollView(
          child: ListView(
372 373
            itemExtent: 100.0,
            shrinkWrap: true,
374
            children: List<Widget>.generate(20, (int i) {
375
              return Text('$i');
376 377
            }),
          ),
378 379 380 381 382 383 384
        ),
      ),
    );

    expect(find.text('0'), findsOneWidget);
    expect(find.text('19'), findsOneWidget);
  });
385

386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402
  testWidgets('ListView with shrink wrap in bounded context correctly uses cache extent', (WidgetTester tester) async {
    final SemanticsHandle handle = tester.ensureSemantics();
    await tester.pumpWidget(
      Directionality(
        textDirection: TextDirection.ltr,
        child: SizedBox(
          height: 400,
          child: ListView(
            itemExtent: 100.0,
            shrinkWrap: true,
            children: List<Widget>.generate(20, (int i) {
              return Text('Text $i');
            }),
          ),
        ),
      ),
    );
403
    expect(tester.getSemantics(find.text('Text 5')), matchesSemantics());
404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428
    expect(tester.getSemantics(find.text('Text 6', skipOffstage: false)), matchesSemantics(isHidden: true));
    expect(tester.getSemantics(find.text('Text 7', skipOffstage: false)), matchesSemantics(isHidden: true));
    expect(tester.getSemantics(find.text('Text 8', skipOffstage: false)), matchesSemantics(isHidden: true));
    handle.dispose();
  });

  testWidgets('ListView hidden items should stay hidden if their semantics are updated', (WidgetTester tester) async {
    final SemanticsHandle handle = tester.ensureSemantics();
    await tester.pumpWidget(
      Directionality(
        textDirection: TextDirection.ltr,
        child: SizedBox(
          height: 400,
          child: ListView(
            itemExtent: 100.0,
            shrinkWrap: true,
            children: List<Widget>.generate(20, (int i) {
              return Text('Text $i');
            }),
          ),
        ),
      ),
    );
    // Scrollable maybe be marked dirty after layout.
    await tester.pumpAndSettle();
429
    expect(tester.getSemantics(find.text('Text 5')), matchesSemantics());
430 431 432 433 434 435 436 437 438 439 440 441 442 443 444
    expect(tester.getSemantics(find.text('Text 6', skipOffstage: false)), matchesSemantics(isHidden: true));
    expect(tester.getSemantics(find.text('Text 7', skipOffstage: false)), matchesSemantics(isHidden: true));
    expect(tester.getSemantics(find.text('Text 8', skipOffstage: false)), matchesSemantics(isHidden: true));

    // Marks Text 6 semantics as dirty.
    final RenderObject text6 = tester.renderObject(find.text('Text 6', skipOffstage: false));
    text6.markNeedsSemanticsUpdate();

    // Verify the semantics is still hidden.
    await tester.pump();
    expect(tester.getSemantics(find.text('Text 6', skipOffstage: false)), matchesSemantics(isHidden: true));

    handle.dispose();
  });

Josh Soref's avatar
Josh Soref committed
445
  testWidgets('didFinishLayout has correct indices', (WidgetTester tester) async {
446 447
    final TestSliverChildListDelegate delegate = TestSliverChildListDelegate(
      List<Widget>.generate(
448 449
        20,
        (int i) {
450
          return Text('$i', textDirection: TextDirection.ltr);
451
        },
452
      ),
453 454 455
    );

    await tester.pumpWidget(
456
      Directionality(
457
        textDirection: TextDirection.ltr,
458
        child: ListView.custom(
459 460 461
          itemExtent: 110.0,
          childrenDelegate: delegate,
        ),
462 463 464
      ),
    );

465
    expect(delegate.log, equals(<String>['didFinishLayout firstIndex=0 lastIndex=7']));
466 467 468
    delegate.log.clear();

    await tester.pumpWidget(
469
      Directionality(
470
        textDirection: TextDirection.ltr,
471
        child: ListView.custom(
472 473 474
          itemExtent: 210.0,
          childrenDelegate: delegate,
        ),
475 476 477
      ),
    );

478
    expect(delegate.log, equals(<String>['didFinishLayout firstIndex=0 lastIndex=4']));
479 480 481 482 483 484 485 486
    delegate.log.clear();

    await tester.drag(find.byType(ListView), const Offset(0.0, -600.0));

    expect(delegate.log, isEmpty);

    await tester.pump();

487
    expect(delegate.log, equals(<String>['didFinishLayout firstIndex=1 lastIndex=6']));
488 489
    delegate.log.clear();
  });
490 491

  testWidgets('ListView automatically pad MediaQuery on axis', (WidgetTester tester) async {
492
    EdgeInsets? innerMediaQueryPadding;
493 494

    await tester.pumpWidget(
495
      Directionality(
496
        textDirection: TextDirection.ltr,
497
        child: MediaQuery(
498
          data: const MediaQueryData(
499
            padding: EdgeInsets.all(30.0),
500
          ),
501
          child: ListView(
502 503
            children: <Widget>[
              const Text('top', textDirection: TextDirection.ltr),
504
              Builder(builder: (BuildContext context) {
505
                innerMediaQueryPadding = MediaQuery.of(context).padding;
506
                return Container();
507 508 509 510 511 512 513 514 515 516 517
              }),
            ],
          ),
        ),
      ),
    );
    // Automatically apply the top/bottom padding into sliver.
    expect(tester.getTopLeft(find.text('top')).dy, 30.0);
    // Leave left/right padding as is for children.
    expect(innerMediaQueryPadding, const EdgeInsets.symmetric(horizontal: 30.0));
  });
518 519 520 521 522

  testWidgets('ListView clips if overflow is smaller than cacheExtent', (WidgetTester tester) async {
    // Regression test for https://github.com/flutter/flutter/issues/17426.

    await tester.pumpWidget(
523
      Directionality(
524
        textDirection: TextDirection.ltr,
525
        child: Center(
526
          child: SizedBox(
527
            height: 200.0,
528
            child: ListView(
529 530
              cacheExtent: 500.0,
              children: <Widget>[
531
                Container(
532 533
                  height: 90.0,
                ),
534
                Container(
535 536
                  height: 110.0,
                ),
537
                Container(
538 539 540 541 542 543 544 545 546 547 548 549 550 551
                  height: 80.0,
                ),
              ],
            ),
          ),
        ),
      ),
    );

    expect(find.byType(Viewport), paints..clipRect());
  });

  testWidgets('ListView does not clips if no overflow', (WidgetTester tester) async {
    await tester.pumpWidget(
552
      Directionality(
553
        textDirection: TextDirection.ltr,
554
        child: Center(
555
          child: SizedBox(
556
            height: 200.0,
557
            child: ListView(
558
              cacheExtent: 500.0,
559 560
              children: const <Widget>[
                SizedBox(
561 562 563 564 565 566 567
                  height: 100.0,
                ),
              ],
            ),
          ),
        ),
      ),
568
    );
569 570 571 572 573 574 575 576

    expect(find.byType(Viewport), isNot(paints..clipRect()));
  });

  testWidgets('ListView (fixed extent) clips if overflow is smaller than cacheExtent', (WidgetTester tester) async {
    // Regression test for https://github.com/flutter/flutter/issues/17426.

    await tester.pumpWidget(
577
      Directionality(
578
        textDirection: TextDirection.ltr,
579
        child: Center(
580
          child: SizedBox(
581
            height: 200.0,
582
            child: ListView(
583 584
              itemExtent: 100.0,
              cacheExtent: 500.0,
585 586
              children: const <Widget>[
                SizedBox(
587 588
                  height: 100.0,
                ),
589
                SizedBox(
590 591
                  height: 100.0,
                ),
592
                SizedBox(
593 594 595 596 597 598 599 600 601 602 603 604 605 606
                  height: 100.0,
                ),
              ],
            ),
          ),
        ),
      ),
    );

    expect(find.byType(Viewport), paints..clipRect());
  });

  testWidgets('ListView (fixed extent) does not clips if no overflow', (WidgetTester tester) async {
    await tester.pumpWidget(
607
      Directionality(
608
        textDirection: TextDirection.ltr,
609
        child: Center(
610
          child: SizedBox(
611
            height: 200.0,
612
            child: ListView(
613 614
              itemExtent: 100.0,
              cacheExtent: 500.0,
615 616
              children: const <Widget>[
                SizedBox(
617 618 619 620 621 622 623 624 625 626 627
                  height: 100.0,
                ),
              ],
            ),
          ),
        ),
      ),
    );

    expect(find.byType(Viewport), isNot(paints..clipRect()));
  });
628 629 630 631 632 633 634

  testWidgets('ListView.horizontal has implicit scrolling by default', (WidgetTester tester) async {
    final SemanticsHandle handle = tester.ensureSemantics();
    await tester.pumpWidget(
      Directionality(
        textDirection: TextDirection.ltr,
        child: Center(
635
          child: SizedBox(
636 637 638 639
            height: 200.0,
            child: ListView(
              scrollDirection: Axis.horizontal,
              itemExtent: 100.0,
640 641
              children: const <Widget>[
                SizedBox(
642 643 644 645 646 647 648 649 650 651 652 653
                  height: 100.0,
                ),
              ],
            ),
          ),
        ),
      ),
    );
    expect(tester.getSemantics(find.byType(Scrollable)), matchesSemantics(
      children: <Matcher>[
        matchesSemantics(
          children: <Matcher>[
654
            matchesSemantics(hasImplicitScrolling: true),
655 656 657 658 659 660
          ],
        ),
      ],
    ));
    handle.dispose();
  });
661 662 663 664 665

  testWidgets('Updates viewport dimensions when scroll direction changes', (WidgetTester tester) async {
    // Regression test for https://github.com/flutter/flutter/issues/43380.
    final ScrollController controller = ScrollController();

666
    Widget buildListView({ required Axis scrollDirection }) {
667 668 669 670
      assert(scrollDirection != null);
      return Directionality(
        textDirection: TextDirection.ltr,
        child: Center(
671
          child: SizedBox(
672 673 674 675 676 677
            height: 200.0,
            width: 100.0,
            child: ListView(
              controller: controller,
              scrollDirection: scrollDirection,
              itemExtent: 50.0,
678 679
              children: const <Widget>[
                SizedBox(
680 681 682 683 684 685 686 687 688 689 690 691 692 693 694 695 696 697 698
                  height: 50.0,
                  width: 50.0,
                ),
              ],
            ),
          ),
        ),
      );
    }

    await tester.pumpWidget(buildListView(scrollDirection: Axis.horizontal));
    expect(controller.position.viewportDimension, 100.0);

    await tester.pumpWidget(buildListView(scrollDirection: Axis.vertical));
    expect(controller.position.viewportDimension, 200.0);

    await tester.pumpWidget(buildListView(scrollDirection: Axis.horizontal));
    expect(controller.position.viewportDimension, 100.0);
  });
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

  testWidgets('ListView respects clipBehavior', (WidgetTester tester) async {
    await tester.pumpWidget(
      Directionality(
        textDirection: TextDirection.ltr,
        child: ListView(
          children: <Widget>[Container(height: 2000.0)],
        ),
      ),
    );

    // 1st, check that the render object has received the default clip behavior.
    final RenderViewport renderObject = tester.allRenderObjects.whereType<RenderViewport>().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(
      Directionality(
        textDirection: TextDirection.ltr,
        child: ListView(
          clipBehavior: Clip.antiAlias,
725
          children: <Widget>[Container(height: 2000.0)],
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
        ),
      ),
    );
    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));
  });

  testWidgets('ListView.builder respects clipBehavior', (WidgetTester tester) async {
    await tester.pumpWidget(
      Directionality(
        textDirection: TextDirection.ltr,
        child: ListView.builder(
          itemCount: 10,
          itemBuilder: (BuildContext _, int __) => Container(height: 2000.0),
          clipBehavior: Clip.antiAlias,
        ),
      ),
    );
    final RenderViewport renderObject = tester.allRenderObjects.whereType<RenderViewport>().first;
    expect(renderObject.clipBehavior, equals(Clip.antiAlias));
  });

  testWidgets('ListView.custom respects clipBehavior', (WidgetTester tester) async {
    await tester.pumpWidget(
      Directionality(
        textDirection: TextDirection.ltr,
        child: ListView.custom(
          childrenDelegate: SliverChildBuilderDelegate(
            (BuildContext context, int index) => Container(height: 2000.0),
            childCount: 1,
          ),
          clipBehavior: Clip.antiAlias,
        ),
      ),
    );
    final RenderViewport renderObject = tester.allRenderObjects.whereType<RenderViewport>().first;
    expect(renderObject.clipBehavior, equals(Clip.antiAlias));
  });

  testWidgets('ListView.separated respects clipBehavior', (WidgetTester tester) async {
    await tester.pumpWidget(
      Directionality(
        textDirection: TextDirection.ltr,
        child: ListView.separated(
          itemCount: 10,
          itemBuilder: (BuildContext _, int __) => Container(height: 2000.0),
          separatorBuilder: (BuildContext _, int __) => const Divider(),
          clipBehavior: Clip.antiAlias,
        ),
      ),
    );
    final RenderViewport renderObject = tester.allRenderObjects.whereType<RenderViewport>().first;
    expect(renderObject.clipBehavior, equals(Clip.antiAlias));
  });
Adam Barth's avatar
Adam Barth committed
783
}