list_view_test.dart 14.1 KB
Newer Older
Adam Barth's avatar
Adam Barth committed
1 2 3 4 5 6 7
// Copyright 2015 The Chromium Authors. All rights reserved.
// 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 30 31 32 33 34 35 36 37 38

  @override
  String toString({DiagnosticLevel minLevel}) => '$index $alive';
}

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

    position.jumpTo(975.0);

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

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

178
  testWidgets('ListView large scroll jump and keepAlive first child not keepAlive', (WidgetTester tester) async {
179
    Future<void> checkAndScroll([String zero = '0:false']) async {
180 181 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
      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);
    }

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

211
    await tester.pumpWidget(_StatefulListView((int i) => i % 3 == 0));
212 213 214
    await checkAndScroll('0:true');
  });

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

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

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

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

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

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

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

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

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

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

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

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

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

    expect(delegate.log, isEmpty);

    await tester.pump();

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

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

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

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

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

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

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

    expect(find.byType(Viewport), isNot(paints..clipRect()));
  });
Adam Barth's avatar
Adam Barth committed
503
}