list_view_test.dart 15.1 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 5 6 7
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

import 'package:flutter_test/flutter_test.dart';
import 'package:flutter/widgets.dart';

8 9
import '../rendering/mock_canvas.dart';

10 11 12 13 14 15 16 17 18 19 20
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');
  }
}

21 22 23 24 25 26
class Alive extends StatefulWidget {
  const Alive(this.alive, this.index);
  final bool alive;
  final int index;

  @override
27
  AliveState createState() => AliveState();
28 29

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

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

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

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

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

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

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

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

105
    final RenderBox box = tester.renderObject<RenderBox>(find.byType(Container).first);
Adam Barth's avatar
Adam Barth committed
106 107 108 109 110
    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
111
    expect(find.text('3'), findsNothing);
Adam Barth's avatar
Adam Barth committed
112 113
    expect(find.text('4'), findsNothing);

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

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

136
  testWidgets('ListView large scroll jump', (WidgetTester tester) async {
137
    final List<int> log = <int>[];
Adam Barth's avatar
Adam Barth committed
138 139

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

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

161 162
    final ScrollableState state = tester.state(find.byType(Scrollable));
    final ScrollPosition position = state.position;
Adam Barth's avatar
Adam Barth committed
163 164 165 166 167
    position.jumpTo(2025.0);

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

168
    expect(log, equals(<int>[8, 9, 10, 11, 12, 13, 14]));
Adam Barth's avatar
Adam Barth committed
169 170 171 172 173 174 175
    log.clear();

    position.jumpTo(975.0);

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

176
    expect(log, equals(<int>[7, 6, 5, 4, 3]));
Adam Barth's avatar
Adam Barth committed
177 178
    log.clear();
  });
179

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

210
    await tester.pumpWidget(_StatefulListView((int i) => i > 2 && i % 3 == 0));
211 212
    await checkAndScroll();

213
    await tester.pumpWidget(_StatefulListView((int i) => i % 3 == 0));
214
    await checkAndScroll('0:true');
215
  }, skip: isBrowser);
216

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

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

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

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

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

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

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

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

    await tester.pumpWidget(
333
      Directionality(
334
        textDirection: TextDirection.ltr,
335
        child: ListView.custom(
336 337 338
          itemExtent: 110.0,
          childrenDelegate: delegate,
        ),
339 340 341
      ),
    );

342
    expect(delegate.log, equals(<String>['didFinishLayout firstIndex=0 lastIndex=7']));
343 344 345
    delegate.log.clear();

    await tester.pumpWidget(
346
      Directionality(
347
        textDirection: TextDirection.ltr,
348
        child: ListView.custom(
349 350 351
          itemExtent: 210.0,
          childrenDelegate: delegate,
        ),
352 353 354
      ),
    );

355
    expect(delegate.log, equals(<String>['didFinishLayout firstIndex=0 lastIndex=4']));
356 357 358 359 360 361 362 363
    delegate.log.clear();

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

    expect(delegate.log, isEmpty);

    await tester.pump();

364
    expect(delegate.log, equals(<String>['didFinishLayout firstIndex=1 lastIndex=6']));
365 366
    delegate.log.clear();
  });
367 368 369 370 371

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

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

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

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

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

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

    expect(find.byType(Viewport), isNot(paints..clipRect()));
  });
505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530

  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>[
531
            matchesSemantics(hasImplicitScrolling: true),
532 533 534 535 536 537
          ],
        ),
      ],
    ));
    handle.dispose();
  });
Adam Barth's avatar
Adam Barth committed
538
}