list_view_test.dart 19.6 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
// @dart = 2.8

7
import 'package:flutter/material.dart';
Adam Barth's avatar
Adam Barth committed
8
import 'package:flutter_test/flutter_test.dart';
9
import 'package:flutter/rendering.dart';
Adam Barth's avatar
Adam Barth committed
10 11
import 'package:flutter/widgets.dart';

12
import '../rendering/mock_canvas.dart';
13
import '../rendering/rendering_tester.dart';
14

15 16 17 18 19 20 21 22 23 24 25
class TestSliverChildListDelegate extends SliverChildListDelegate {
  TestSliverChildListDelegate(List<Widget> children) : super(children);

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

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

26
class Alive extends StatefulWidget {
27
  const Alive(this.alive, this.index, { Key key }) : super(key: key);
28 29 30 31
  final bool alive;
  final int index;

  @override
32
  AliveState createState() => AliveState();
33 34

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

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

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

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

  final WhetherToKeepAlive aliveCallback;
  @override
55
  _StatefulListViewState createState() => _StatefulListViewState();
56 57 58 59 60
}

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

Adam Barth's avatar
Adam Barth committed
83
void main() {
84
  testWidgets('ListView default control', (WidgetTester tester) async {
85
    await tester.pumpWidget(
86
      Directionality(
87
        textDirection: TextDirection.ltr,
88 89
        child: Center(
          child: ListView(itemExtent: 100.0),
90 91 92
        ),
      ),
    );
93 94
  });

95
  testWidgets('ListView itemExtent control test', (WidgetTester tester) async {
Adam Barth's avatar
Adam Barth committed
96
    await tester.pumpWidget(
97
      Directionality(
98
        textDirection: TextDirection.ltr,
99
        child: ListView(
100
          itemExtent: 200.0,
101 102 103
          children: List<Widget>.generate(20, (int i) {
            return Container(
              child: Text('$i'),
104 105 106
            );
          }),
        ),
Adam Barth's avatar
Adam Barth committed
107 108 109
      ),
    );

110
    final RenderBox box = tester.renderObject<RenderBox>(find.byType(Container).first);
Adam Barth's avatar
Adam Barth committed
111 112 113 114 115
    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
116
    expect(find.text('3'), findsNothing);
Adam Barth's avatar
Adam Barth committed
117 118
    expect(find.text('4'), findsNothing);

119
    await tester.drag(find.byType(ListView), const Offset(0.0, -250.0));
Adam Barth's avatar
Adam Barth committed
120 121 122 123 124 125 126
    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
127
    expect(find.text('5'), findsNothing);
Adam Barth's avatar
Adam Barth committed
128 129
    expect(find.text('6'), findsNothing);

130
    await tester.drag(find.byType(ListView), const Offset(0.0, 200.0));
Adam Barth's avatar
Adam Barth committed
131 132 133 134 135 136
    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
137
    expect(find.text('4'), findsNothing);
Adam Barth's avatar
Adam Barth committed
138 139 140
    expect(find.text('5'), findsNothing);
  });

141
  testWidgets('ListView large scroll jump', (WidgetTester tester) async {
142
    final List<int> log = <int>[];
Adam Barth's avatar
Adam Barth committed
143 144

    await tester.pumpWidget(
145
      Directionality(
146
        textDirection: TextDirection.ltr,
147
        child: ListView(
148
          itemExtent: 200.0,
149 150
          children: List<Widget>.generate(20, (int i) {
            return Builder(
151 152
              builder: (BuildContext context) {
                log.add(i);
153 154
                return Container(
                  child: Text('$i'),
155
                );
156
              },
157 158 159
            );
          }),
        ),
Adam Barth's avatar
Adam Barth committed
160 161 162
      ),
    );

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

166 167
    final ScrollableState state = tester.state(find.byType(Scrollable));
    final ScrollPosition position = state.position;
Adam Barth's avatar
Adam Barth committed
168 169 170 171 172
    position.jumpTo(2025.0);

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

173
    expect(log, equals(<int>[8, 9, 10, 11, 12, 13, 14]));
Adam Barth's avatar
Adam Barth committed
174 175 176 177 178 179 180
    log.clear();

    position.jumpTo(975.0);

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

181
    expect(log, equals(<int>[7, 6, 5, 4, 3]));
Adam Barth's avatar
Adam Barth committed
182 183
    log.clear();
  });
184

185
  testWidgets('ListView large scroll jump and keepAlive first child not keepAlive', (WidgetTester tester) async {
186
    Future<void> checkAndScroll([ String zero = '0:false' ]) async {
187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214
      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);
    }

215
    await tester.pumpWidget(_StatefulListView((int i) => i > 2 && i % 3 == 0));
216 217
    await checkAndScroll();

218
    await tester.pumpWidget(_StatefulListView((int i) => i % 3 == 0));
219
    await checkAndScroll('0:true');
220
  });
221

222 223
  testWidgets('ListView can build out of underflow', (WidgetTester tester) async {
    await tester.pumpWidget(
224
      Directionality(
225
        textDirection: TextDirection.ltr,
226
        child: ListView(
227 228
          itemExtent: 100.0,
        ),
229 230 231 232 233 234 235 236 237 238 239
      ),
    );

    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(
240
      Directionality(
241
        textDirection: TextDirection.ltr,
242
        child: ListView(
243
          itemExtent: 100.0,
244 245 246
          children: List<Widget>.generate(2, (int i) {
            return Container(
              child: Text('$i'),
247 248 249
            );
          }),
        ),
250 251 252 253 254 255 256 257 258 259 260
      ),
    );

    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(
261
      Directionality(
262
        textDirection: TextDirection.ltr,
263
        child: ListView(
264
          itemExtent: 100.0,
265 266 267
          children: List<Widget>.generate(5, (int i) {
            return Container(
              child: Text('$i'),
268 269 270
            );
          }),
        ),
271 272 273 274 275 276 277 278 279 280
      ),
    );

    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);
  });
281

282 283
  testWidgets('ListView can build out of overflow padding', (WidgetTester tester) async {
    await tester.pumpWidget(
284
      Directionality(
285
        textDirection: TextDirection.ltr,
286 287
        child: Center(
          child: SizedBox(
288 289
            width: 0.0,
            height: 0.0,
290
            child: ListView(
291
              padding: const EdgeInsets.all(8.0),
292
              children: const <Widget>[
293
                Text('padded', textDirection: TextDirection.ltr),
294 295
              ],
            ),
296 297 298 299
          ),
        ),
      ),
    );
300
    expect(find.text('padded', skipOffstage: false), findsOneWidget);
301 302
  });

303 304
  testWidgets('ListView with itemExtent in unbounded context', (WidgetTester tester) async {
    await tester.pumpWidget(
305
      Directionality(
306
        textDirection: TextDirection.ltr,
307 308
        child: SingleChildScrollView(
          child: ListView(
309 310
            itemExtent: 100.0,
            shrinkWrap: true,
311 312 313
            children: List<Widget>.generate(20, (int i) {
              return Container(
                child: Text('$i'),
314 315 316
              );
            }),
          ),
317 318 319 320 321 322 323
        ),
      ),
    );

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

Josh Soref's avatar
Josh Soref committed
325
  testWidgets('didFinishLayout has correct indices', (WidgetTester tester) async {
326 327
    final TestSliverChildListDelegate delegate = TestSliverChildListDelegate(
      List<Widget>.generate(
328 329
        20,
        (int i) {
330 331
          return Container(
            child: Text('$i', textDirection: TextDirection.ltr),
332 333
          );
        },
334
      ),
335 336 337
    );

    await tester.pumpWidget(
338
      Directionality(
339
        textDirection: TextDirection.ltr,
340
        child: ListView.custom(
341 342 343
          itemExtent: 110.0,
          childrenDelegate: delegate,
        ),
344 345 346
      ),
    );

347
    expect(delegate.log, equals(<String>['didFinishLayout firstIndex=0 lastIndex=7']));
348 349 350
    delegate.log.clear();

    await tester.pumpWidget(
351
      Directionality(
352
        textDirection: TextDirection.ltr,
353
        child: ListView.custom(
354 355 356
          itemExtent: 210.0,
          childrenDelegate: delegate,
        ),
357 358 359
      ),
    );

360
    expect(delegate.log, equals(<String>['didFinishLayout firstIndex=0 lastIndex=4']));
361 362 363 364 365 366 367 368
    delegate.log.clear();

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

    expect(delegate.log, isEmpty);

    await tester.pump();

369
    expect(delegate.log, equals(<String>['didFinishLayout firstIndex=1 lastIndex=6']));
370 371
    delegate.log.clear();
  });
372 373 374 375 376

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

    await tester.pumpWidget(
377
      Directionality(
378
        textDirection: TextDirection.ltr,
379
        child: MediaQuery(
380
          data: const MediaQueryData(
381
            padding: EdgeInsets.all(30.0),
382
          ),
383
          child: ListView(
384 385
            children: <Widget>[
              const Text('top', textDirection: TextDirection.ltr),
386
              Builder(builder: (BuildContext context) {
387
                innerMediaQueryPadding = MediaQuery.of(context).padding;
388
                return Container();
389 390 391 392 393 394 395 396 397 398 399
              }),
            ],
          ),
        ),
      ),
    );
    // 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));
  });
400 401 402 403 404

  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(
405
      Directionality(
406
        textDirection: TextDirection.ltr,
407 408
        child: Center(
          child: Container(
409
            height: 200.0,
410
            child: ListView(
411 412
              cacheExtent: 500.0,
              children: <Widget>[
413
                Container(
414 415
                  height: 90.0,
                ),
416
                Container(
417 418
                  height: 110.0,
                ),
419
                Container(
420 421 422 423 424 425 426 427 428 429 430 431 432 433
                  height: 80.0,
                ),
              ],
            ),
          ),
        ),
      ),
    );

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

  testWidgets('ListView does not clips if no overflow', (WidgetTester tester) async {
    await tester.pumpWidget(
434
      Directionality(
435
        textDirection: TextDirection.ltr,
436 437
        child: Center(
          child: Container(
438
            height: 200.0,
439
            child: ListView(
440 441
              cacheExtent: 500.0,
              children: <Widget>[
442
                Container(
443 444 445 446 447 448 449
                  height: 100.0,
                ),
              ],
            ),
          ),
        ),
      ),
450
    );
451 452 453 454 455 456 457 458

    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(
459
      Directionality(
460
        textDirection: TextDirection.ltr,
461 462
        child: Center(
          child: Container(
463
            height: 200.0,
464
            child: ListView(
465 466 467
              itemExtent: 100.0,
              cacheExtent: 500.0,
              children: <Widget>[
468
                Container(
469 470
                  height: 100.0,
                ),
471
                Container(
472 473
                  height: 100.0,
                ),
474
                Container(
475 476 477 478 479 480 481 482 483 484 485 486 487 488
                  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(
489
      Directionality(
490
        textDirection: TextDirection.ltr,
491 492
        child: Center(
          child: Container(
493
            height: 200.0,
494
            child: ListView(
495 496 497
              itemExtent: 100.0,
              cacheExtent: 500.0,
              children: <Widget>[
498
                Container(
499 500 501 502 503 504 505 506 507 508 509
                  height: 100.0,
                ),
              ],
            ),
          ),
        ),
      ),
    );

    expect(find.byType(Viewport), isNot(paints..clipRect()));
  });
510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535

  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(
          child: Container(
            height: 200.0,
            child: ListView(
              scrollDirection: Axis.horizontal,
              itemExtent: 100.0,
              children: <Widget>[
                Container(
                  height: 100.0,
                ),
              ],
            ),
          ),
        ),
      ),
    );
    expect(tester.getSemantics(find.byType(Scrollable)), matchesSemantics(
      children: <Matcher>[
        matchesSemantics(
          children: <Matcher>[
536
            matchesSemantics(hasImplicitScrolling: true),
537 538 539 540 541 542
          ],
        ),
      ],
    ));
    handle.dispose();
  });
543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580

  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();

    Widget buildListView({@required Axis scrollDirection}) {
      assert(scrollDirection != null);
      return Directionality(
        textDirection: TextDirection.ltr,
        child: Center(
          child: Container(
            height: 200.0,
            width: 100.0,
            child: ListView(
              controller: controller,
              scrollDirection: scrollDirection,
              itemExtent: 50.0,
              children: <Widget>[
                Container(
                  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);
  });
581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601 602 603 604 605 606 607 608 609 610 611 612 613 614 615 616 617 618 619 620 621 622 623 624 625 626 627 628 629 630 631 632 633 634 635 636 637 638 639 640 641 642 643 644 645 646 647 648 649 650 651 652 653 654 655 656 657 658 659 660 661 662 663 664

  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(
          children: <Widget>[Container(height: 2000.0)],
          clipBehavior: Clip.antiAlias,
        ),
      ),
    );
    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
665
}