list_view_test.dart 30.3 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
import 'package:collection/collection.dart';
6 7
import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
8
import 'package:flutter_test/flutter_test.dart';
Adam Barth's avatar
Adam Barth committed
9

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
  // Regression test for https://github.com/flutter/flutter/issues/100451
80
  testWidgets('ListView.builder respects findChildIndexCallback', (WidgetTester tester) async {
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
    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
116
  testWidgets('ListView.separator respects findChildIndexCallback', (WidgetTester tester) async {
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
    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
          children: List<Widget>.generate(20, (int i) {
170
            return ColoredBox(
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(ColoredBox).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
  testWidgets('ListView can build out of underflow', (WidgetTester tester) async {
290
    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
  testWidgets('ListView can build out of overflow padding', (WidgetTester tester) async {
346
    await tester.pumpWidget(
347
      Directionality(
348
        textDirection: TextDirection.ltr,
349
        child: Center(
350
          child: SizedBox.shrink(
351
            child: ListView(
352
              padding: const EdgeInsets.all(8.0),
353
              children: const <Widget>[
354
                Text('padded', textDirection: TextDirection.ltr),
355 356
              ],
            ),
357 358 359 360
          ),
        ),
      ),
    );
361
    expect(find.text('padded', skipOffstage: false), findsOneWidget);
362 363
  });

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

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

384
  testWidgets('ListView with shrink wrap in bounded context correctly uses cache extent', (WidgetTester tester) async {
385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400
    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');
            }),
          ),
        ),
      ),
    );
401
    expect(tester.getSemantics(find.text('Text 5')), matchesSemantics());
402 403 404 405 406 407
    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();
  });

408
  testWidgets('ListView hidden items should stay hidden if their semantics are updated', (WidgetTester tester) async {
409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426
    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();
427
    expect(tester.getSemantics(find.text('Text 5')), matchesSemantics());
428 429 430 431 432 433 434 435 436 437 438 439 440 441 442
    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();
  });

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

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

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

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

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

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

    expect(delegate.log, isEmpty);

    await tester.pump();

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

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

    await tester.pumpWidget(
493
      Directionality(
494
        textDirection: TextDirection.ltr,
495
        child: MediaQuery(
496
          data: const MediaQueryData(
497
            padding: EdgeInsets.all(30.0),
498
          ),
499
          child: ListView(
500 501
            children: <Widget>[
              const Text('top', textDirection: TextDirection.ltr),
502
              Builder(builder: (BuildContext context) {
503
                innerMediaQueryPadding = MediaQuery.paddingOf(context);
504
                return Container();
505 506 507 508 509 510 511 512 513 514 515
              }),
            ],
          ),
        ),
      ),
    );
    // 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));
  });
516

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

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

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

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

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

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

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

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

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

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

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

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

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

698
  testWidgets('ListView respects clipBehavior', (WidgetTester tester) async {
699 700 701 702 703 704 705 706 707 708 709 710 711 712 713 714 715 716 717 718 719 720 721 722
    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,
723
          children: <Widget>[Container(height: 2000.0)],
724 725 726 727 728 729 730 731
        ),
      ),
    );
    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));
732 733
    context.dispose();
  });
734

735
  testWidgets('ListView.builder respects clipBehavior', (WidgetTester tester) async {
736 737 738 739 740 741 742 743 744 745 746 747 748 749
    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));
  });

750
  testWidgets('ListView.custom respects clipBehavior', (WidgetTester tester) async {
751 752 753 754 755 756 757 758 759 760 761 762 763 764 765 766
    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));
  });

767
  testWidgets('ListView.separated respects clipBehavior', (WidgetTester tester) async {
768 769 770 771 772 773 774 775 776 777 778 779 780 781
    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));
  });
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
  // Regression test for https://github.com/flutter/flutter/pull/138912
  testWidgets('itemExtentBuilder should respect item count', (WidgetTester tester) async {
    final ScrollController controller = ScrollController();
    addTearDown(controller.dispose);
    final List<double> numbers = <double>[
      10, 20, 30, 40, 50,
    ];
    await tester.pumpWidget(
      Directionality(
        textDirection: TextDirection.ltr,
        child: ListView.builder(
          controller: controller,
          itemCount: numbers.length,
          itemExtentBuilder: (int index, SliverLayoutDimensions dimensions) {
            return numbers[index];
          },
          itemBuilder: (BuildContext context, int index) {
            return SizedBox(
              height: numbers[index],
              child: Text('Item $index'),
            );
          },
        ),
      ),
    );

    expect(find.text('Item 0'), findsOneWidget);
    expect(find.text('Item 4'), findsOneWidget);
    expect(find.text('Item 5'), findsNothing);
  });

814
  // Regression test for https://github.com/flutter/flutter/pull/131393
815
  testWidgets('itemExtentBuilder test', (WidgetTester tester) async {
816
    final ScrollController controller = ScrollController();
817
    addTearDown(controller.dispose);
818 819 820 821 822 823 824 825 826 827 828 829 830 831 832 833 834 835 836 837 838 839 840 841 842 843 844 845 846 847 848 849 850 851 852 853 854 855 856 857 858 859 860 861 862 863 864 865 866 867 868 869 870 871 872 873 874 875 876 877 878 879 880 881 882 883 884 885 886 887 888 889 890 891 892 893 894 895 896 897 898 899 900 901 902 903 904 905 906 907 908 909 910 911 912 913 914 915 916 917 918 919 920 921 922 923 924 925 926 927 928 929 930 931 932 933 934 935 936 937 938 939 940 941 942 943 944 945
    final List<int> buildLog = <int>[];
    late SliverLayoutDimensions sliverLayoutDimensions;
    await tester.pumpWidget(
      Directionality(
        textDirection: TextDirection.ltr,
        child: ListView.builder(
          controller: controller,
          itemExtentBuilder: (int index, SliverLayoutDimensions dimensions) {
            sliverLayoutDimensions = dimensions;
            return 100.0;
          },
          itemBuilder: (BuildContext context, int index) {
            buildLog.insert(0, index);
            return Text('Item $index');
          },
        ),
      ),
    );

    expect(find.text('Item 0'), findsOneWidget);
    expect(find.text('Item 5'), findsOneWidget);
    expect(find.text('Item 6'), findsNothing);
    expect(
      sliverLayoutDimensions,
      const SliverLayoutDimensions(
        scrollOffset: 0.0,
        precedingScrollExtent: 0.0,
        viewportMainAxisExtent: 600.0,
        crossAxisExtent: 800.0,
      )
    );
    // viewport(600.0) + cache extent after(250.0)
    expect(buildLog.length, 9);
    expect(buildLog.min, 0);
    expect(buildLog.max, 8);

    buildLog.clear();

    // Scrolling drastically.
    controller.jumpTo(10000.0);
    await tester.pump();

    expect(find.text('Item 99'), findsNothing);
    expect(find.text('Item 100'), findsOneWidget);
    expect(find.text('Item 105'), findsOneWidget);
    expect(find.text('Item 106'), findsNothing);
    expect(
        sliverLayoutDimensions,
        const SliverLayoutDimensions(
          scrollOffset: 10000.0,
          precedingScrollExtent: 0.0,
          viewportMainAxisExtent: 600.0,
          crossAxisExtent: 800.0,
        )
    );
    // Scrolling drastically only loading the visible and cached area items.
    // cache extent before(250.0) + viewport(600.0) + cache extent after(250.0)
    expect(buildLog.length, 12);
    expect(buildLog.min, 97);
    expect(buildLog.max, 108);

    buildLog.clear();
    controller.jumpTo(5000.0);
    await tester.pump();

    expect(find.text('Item 49'), findsNothing);
    expect(find.text('Item 50'), findsOneWidget);
    expect(find.text('Item 55'), findsOneWidget);
    expect(find.text('Item 56'), findsNothing);
    expect(
        sliverLayoutDimensions,
        const SliverLayoutDimensions(
          scrollOffset: 5000.0,
          precedingScrollExtent: 0.0,
          viewportMainAxisExtent: 600.0,
          crossAxisExtent: 800.0,
        )
    );
    // cache extent before(250.0) + viewport(600.0) + cache extent after(250.0)
    expect(buildLog.length, 12);
    expect(buildLog.min, 47);
    expect(buildLog.max, 58);

    buildLog.clear();
    controller.jumpTo(4700.0);
    await tester.pump();

    expect(find.text('Item 46'), findsNothing);
    expect(find.text('Item 47'), findsOneWidget);
    expect(find.text('Item 52'), findsOneWidget);
    expect(find.text('Item 53'), findsNothing);
    expect(
        sliverLayoutDimensions,
        const SliverLayoutDimensions(
          scrollOffset: 4700.0,
          precedingScrollExtent: 0.0,
          viewportMainAxisExtent: 600.0,
          crossAxisExtent: 800.0,
        )
    );
    // Only newly entered cached area items need to be loaded.
    expect(buildLog.length, 3);
    expect(buildLog.min, 44);
    expect(buildLog.max, 46);

    buildLog.clear();
    controller.jumpTo(5300.0);
    await tester.pump();

    expect(find.text('Item 52'), findsNothing);
    expect(find.text('Item 53'), findsOneWidget);
    expect(find.text('Item 58'), findsOneWidget);
    expect(find.text('Item 59'), findsNothing);
    expect(
        sliverLayoutDimensions,
        const SliverLayoutDimensions(
          scrollOffset: 5300.0,
          precedingScrollExtent: 0.0,
          viewportMainAxisExtent: 600.0,
          crossAxisExtent: 800.0,
        )
    );
    // Only newly entered cached area items need to be loaded.
    expect(buildLog.length, 6);
    expect(buildLog.min, 56);
    expect(buildLog.max, 61);
  });

946
  testWidgets('itemExtent, prototypeItem and itemExtentBuilder conflicts test', (WidgetTester tester) async {
947 948 949 950 951 952 953 954 955 956 957 958 959 960 961 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 997 998
    Object? error;
    try {
      await tester.pumpWidget(
        ListView.builder(
          itemExtentBuilder: (int index, SliverLayoutDimensions dimensions) {
            return 100.0;
          },
          itemExtent: 100.0,
          itemBuilder: (BuildContext context, int index) {
            return Text('Item $index');
          },
        ),
      );
    } catch (e) {
      error = e;
    }
    expect(error, isNotNull);

    error = null;
    try {
      await tester.pumpWidget(
        ListView.builder(
          itemExtentBuilder: (int index, SliverLayoutDimensions dimensions) {
            return 100.0;
          },
          prototypeItem: Container(),
          itemBuilder: (BuildContext context, int index) {
            return Text('Item $index');
          },
        ),
      );
    } catch (e) {
      error = e;
    }
    expect(error, isNotNull);

    error = null;
    try {
      await tester.pumpWidget(
        ListView.builder(
          itemExtent: 100.0,
          prototypeItem: Container(),
          itemBuilder: (BuildContext context, int index) {
            return Text('Item $index');
          },
        ),
      );
    } catch (e) {
      error = e;
    }
    expect(error, isNotNull);
  });
Adam Barth's avatar
Adam Barth committed
999
}