scroll_view_test.dart 19.6 KB
Newer Older
Ian Hickson's avatar
Ian Hickson committed
1
// Copyright 2014 The Flutter Authors. All rights reserved.
2 3 4 5
// 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';
6
import 'package:flutter/widgets.dart';
7
import 'package:flutter/gestures.dart' show DragStartBehavior;
8
import 'package:flutter/material.dart';
9

10
import 'states.dart';
11 12

void main() {
13
  testWidgets('ListView control test', (WidgetTester tester) async {
14
    final List<String> log = <String>[];
15

16
    await tester.pumpWidget(
17
      Directionality(
18
        textDirection: TextDirection.ltr,
19
        child: ListView(
20
          dragStartBehavior: DragStartBehavior.down,
21
          children: kStates.map<Widget>((String state) {
22
            return GestureDetector(
23 24 25
              onTap: () {
                log.add(state);
              },
26
              child: Container(
27 28
                height: 200.0,
                color: const Color(0xFF0000FF),
29
                child: Text(state),
30
              ),
31
              dragStartBehavior: DragStartBehavior.down,
32 33 34 35 36
            );
          }).toList(),
        ),
      ),
    );
37 38 39 40 41 42 43

    await tester.tap(find.text('Alabama'));
    expect(log, equals(<String>['Alabama']));
    log.clear();

    expect(find.text('Nevada'), findsNothing);

44
    await tester.drag(find.text('Alabama'), const Offset(0.0, -4000.0));
45 46 47
    await tester.pump();

    expect(find.text('Alabama'), findsNothing);
48
    expect(tester.getCenter(find.text('Massachusetts')), equals(const Offset(400.0, 100.0)));
49 50 51 52 53

    await tester.tap(find.text('Massachusetts'));
    expect(log, equals(<String>['Massachusetts']));
    log.clear();
  });
54

55 56
  testWidgets('ListView restart ballistic activity out of range', (WidgetTester tester) async {
    Widget buildListView(int n) {
57
      return Directionality(
58
        textDirection: TextDirection.ltr,
59
        child: ListView(
60
          dragStartBehavior: DragStartBehavior.down,
61
          children: kStates.take(n).map<Widget>((String state) {
62
            return Container(
63 64
              height: 200.0,
              color: const Color(0xFF0000FF),
65
              child: Text(state),
66 67 68
            );
          }).toList(),
        ),
69 70 71
      );
    }

72 73 74
    await tester.pumpWidget(buildListView(30));
    await tester.fling(find.byType(ListView), const Offset(0.0, -4000.0), 4000.0);
    await tester.pumpWidget(buildListView(15));
75 76 77 78
    await tester.pump(const Duration(milliseconds: 10));
    await tester.pump(const Duration(milliseconds: 10));
    await tester.pump(const Duration(milliseconds: 10));
    await tester.pump(const Duration(milliseconds: 10));
79
    await tester.pumpAndSettle(const Duration(milliseconds: 100));
80

81
    final Viewport viewport = tester.widget(find.byType(Viewport));
82 83
    expect(viewport.offset.pixels, equals(2400.0));
  });
Adam Barth's avatar
Adam Barth committed
84 85

  testWidgets('CustomScrollView control test', (WidgetTester tester) async {
86
    final List<String> log = <String>[];
Adam Barth's avatar
Adam Barth committed
87

88
    await tester.pumpWidget(
89
      Directionality(
90
        textDirection: TextDirection.ltr,
91
        child: CustomScrollView(
92
          dragStartBehavior: DragStartBehavior.down,
93
          slivers: <Widget>[
94 95
            SliverList(
              delegate: SliverChildListDelegate(
96
                kStates.map<Widget>((String state) {
97
                  return GestureDetector(
98
                    dragStartBehavior: DragStartBehavior.down,
99 100 101
                    onTap: () {
                      log.add(state);
                    },
102
                    child: Container(
103 104
                      height: 200.0,
                      color: const Color(0xFF0000FF),
105
                      child: Text(state),
106 107 108 109 110 111
                    ),
                  );
                }).toList(),
              ),
            ),
          ],
Adam Barth's avatar
Adam Barth committed
112
        ),
113 114
      ),
    );
Adam Barth's avatar
Adam Barth committed
115 116 117 118 119 120 121

    await tester.tap(find.text('Alabama'));
    expect(log, equals(<String>['Alabama']));
    log.clear();

    expect(find.text('Nevada'), findsNothing);

122
    await tester.drag(find.text('Alabama'), const Offset(0.0, -4000.0));
Adam Barth's avatar
Adam Barth committed
123 124 125
    await tester.pump();

    expect(find.text('Alabama'), findsNothing);
126
    expect(tester.getCenter(find.text('Massachusetts')), equals(const Offset(400.0, 100.0)));
Adam Barth's avatar
Adam Barth committed
127 128 129 130 131

    await tester.tap(find.text('Massachusetts'));
    expect(log, equals(<String>['Massachusetts']));
    log.clear();
  });
132 133 134

  testWidgets('Can jumpTo during drag', (WidgetTester tester) async {
    final List<Type> log = <Type>[];
135
    final ScrollController controller = ScrollController();
136

137
    await tester.pumpWidget(
138
      Directionality(
139
        textDirection: TextDirection.ltr,
140
        child: NotificationListener<ScrollNotification>(
141 142 143 144
          onNotification: (ScrollNotification notification) {
            log.add(notification.runtimeType);
            return false;
          },
145
          child: ListView(
146 147
            controller: controller,
            children: kStates.map<Widget>((String state) {
148
              return Container(
149
                height: 200.0,
150
                child: Text(state),
151 152 153 154
              );
            }).toList(),
          ),
        ),
155
      ),
156
    );
157 158 159

    expect(log, isEmpty);

160
    final TestGesture gesture = await tester.startGesture(const Offset(100.0, 100.0));
161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189
    await gesture.moveBy(const Offset(0.0, -100.0));

    expect(log, equals(<Type>[
      ScrollStartNotification,
      UserScrollNotification,
      ScrollUpdateNotification,
    ]));
    log.clear();

    await tester.pump();

    controller.jumpTo(550.0);

    expect(controller.offset, equals(550.0));
    expect(log, equals(<Type>[
      ScrollEndNotification,
      UserScrollNotification,
      ScrollStartNotification,
      ScrollUpdateNotification,
      ScrollEndNotification,
    ]));
    log.clear();

    await tester.pump();
    await gesture.moveBy(const Offset(0.0, -100.0));

    expect(controller.offset, equals(550.0));
    expect(log, isEmpty);
  });
190

191
  testWidgets('Vertical CustomScrollViews are primary by default', (WidgetTester tester) async {
192
    const CustomScrollView view = CustomScrollView(scrollDirection: Axis.vertical);
193 194 195 196
    expect(view.primary, isTrue);
  });

  testWidgets('Vertical ListViews are primary by default', (WidgetTester tester) async {
197
    final ListView view = ListView(scrollDirection: Axis.vertical);
198 199 200 201
    expect(view.primary, isTrue);
  });

  testWidgets('Vertical GridViews are primary by default', (WidgetTester tester) async {
202
    final GridView view = GridView.count(
203 204 205 206 207 208 209
      scrollDirection: Axis.vertical,
      crossAxisCount: 1,
    );
    expect(view.primary, isTrue);
  });

  testWidgets('Horizontal CustomScrollViews are non-primary by default', (WidgetTester tester) async {
210
    const CustomScrollView view = CustomScrollView(scrollDirection: Axis.horizontal);
211 212 213 214
    expect(view.primary, isFalse);
  });

  testWidgets('Horizontal ListViews are non-primary by default', (WidgetTester tester) async {
215
    final ListView view = ListView(scrollDirection: Axis.horizontal);
216 217 218 219
    expect(view.primary, isFalse);
  });

  testWidgets('Horizontal GridViews are non-primary by default', (WidgetTester tester) async {
220
    final GridView view = GridView.count(
221 222 223 224 225 226 227
      scrollDirection: Axis.horizontal,
      crossAxisCount: 1,
    );
    expect(view.primary, isFalse);
  });

  testWidgets('CustomScrollViews with controllers are non-primary by default', (WidgetTester tester) async {
228 229
    final CustomScrollView view = CustomScrollView(
      controller: ScrollController(),
230 231 232 233 234 235
      scrollDirection: Axis.vertical,
    );
    expect(view.primary, isFalse);
  });

  testWidgets('ListViews with controllers are non-primary by default', (WidgetTester tester) async {
236 237
    final ListView view = ListView(
      controller: ScrollController(),
238 239 240 241 242 243
      scrollDirection: Axis.vertical,
    );
    expect(view.primary, isFalse);
  });

  testWidgets('GridViews with controllers are non-primary by default', (WidgetTester tester) async {
244 245
    final GridView view = GridView.count(
      controller: ScrollController(),
246 247 248 249 250 251
      scrollDirection: Axis.vertical,
      crossAxisCount: 1,
    );
    expect(view.primary, isFalse);
  });

252
  testWidgets('CustomScrollView sets PrimaryScrollController when primary', (WidgetTester tester) async {
253
    final ScrollController primaryScrollController = ScrollController();
254
    await tester.pumpWidget(
255
      Directionality(
256
        textDirection: TextDirection.ltr,
257
        child: PrimaryScrollController(
258
          controller: primaryScrollController,
259
          child: const CustomScrollView(primary: true),
260 261 262
        ),
      ),
    );
263
    final Scrollable scrollable = tester.widget(find.byType(Scrollable));
264 265 266 267
    expect(scrollable.controller, primaryScrollController);
  });

  testWidgets('ListView sets PrimaryScrollController when primary', (WidgetTester tester) async {
268
    final ScrollController primaryScrollController = ScrollController();
269
    await tester.pumpWidget(
270
      Directionality(
271
        textDirection: TextDirection.ltr,
272
        child: PrimaryScrollController(
273
          controller: primaryScrollController,
274
          child: ListView(primary: true),
275 276 277
        ),
      ),
    );
278
    final Scrollable scrollable = tester.widget(find.byType(Scrollable));
279 280 281 282
    expect(scrollable.controller, primaryScrollController);
  });

  testWidgets('GridView sets PrimaryScrollController when primary', (WidgetTester tester) async {
283
    final ScrollController primaryScrollController = ScrollController();
284
    await tester.pumpWidget(
285
      Directionality(
286
        textDirection: TextDirection.ltr,
287
        child: PrimaryScrollController(
288
          controller: primaryScrollController,
289
          child: GridView.count(primary: true, crossAxisCount: 1),
290 291 292
        ),
      ),
    );
293
    final Scrollable scrollable = tester.widget(find.byType(Scrollable));
294 295
    expect(scrollable.controller, primaryScrollController);
  });
296 297

  testWidgets('Nested scrollables have a null PrimaryScrollController', (WidgetTester tester) async {
298
    const Key innerKey = Key('inner');
299
    final ScrollController primaryScrollController = ScrollController();
300
    await tester.pumpWidget(
301
      Directionality(
302
        textDirection: TextDirection.ltr,
303
        child: PrimaryScrollController(
304
          controller: primaryScrollController,
305
          child: ListView(
306 307
            primary: true,
            children: <Widget>[
308
              Container(
309
                constraints: const BoxConstraints(maxHeight: 200.0),
310
                child: ListView(key: innerKey, primary: true),
311 312
              ),
            ],
313
          ),
314
        ),
315
      ),
316
    );
317

318
    final Scrollable innerScrollable = tester.widget(
319 320 321 322 323 324 325
      find.descendant(
        of: find.byKey(innerKey),
        matching: find.byType(Scrollable),
      ),
    );
    expect(innerScrollable.controller, isNull);
  });
326 327

  testWidgets('Primary ListViews are always scrollable', (WidgetTester tester) async {
328
    final ListView view = ListView(primary: true);
329
    expect(view.physics, isInstanceOf<AlwaysScrollableScrollPhysics>());
330 331 332
  });

  testWidgets('Non-primary ListViews are not always scrollable', (WidgetTester tester) async {
333
    final ListView view = ListView(primary: false);
334
    expect(view.physics, isNot(isInstanceOf<AlwaysScrollableScrollPhysics>()));
335 336 337
  });

  testWidgets('Defaulting-to-primary ListViews are always scrollable', (WidgetTester tester) async {
338
    final ListView view = ListView(scrollDirection: Axis.vertical);
339
    expect(view.physics, isInstanceOf<AlwaysScrollableScrollPhysics>());
340 341 342
  });

  testWidgets('Defaulting-to-not-primary ListViews are not always scrollable', (WidgetTester tester) async {
343
    final ListView view = ListView(scrollDirection: Axis.horizontal);
344
    expect(view.physics, isNot(isInstanceOf<AlwaysScrollableScrollPhysics>()));
345 346 347 348 349
  });

  testWidgets('primary:true leads to scrolling', (WidgetTester tester) async {
    bool scrolled = false;
    await tester.pumpWidget(
350
      Directionality(
351
        textDirection: TextDirection.ltr,
352
        child: NotificationListener<OverscrollNotification>(
353 354 355 356
          onNotification: (OverscrollNotification message) {
            scrolled = true;
            return false;
          },
357
          child: ListView(
358
            primary: true,
359
            children: const <Widget>[],
360
          ),
361 362 363 364 365 366 367 368 369 370
        ),
      ),
    );
    await tester.dragFrom(const Offset(100.0, 100.0), const Offset(0.0, 100.0));
    expect(scrolled, isTrue);
  });

  testWidgets('primary:false leads to no scrolling', (WidgetTester tester) async {
    bool scrolled = false;
    await tester.pumpWidget(
371
      Directionality(
372
        textDirection: TextDirection.ltr,
373
        child: NotificationListener<OverscrollNotification>(
374 375 376 377
          onNotification: (OverscrollNotification message) {
            scrolled = true;
            return false;
          },
378
          child: ListView(
379
            primary: false,
380
            children: const <Widget>[],
381
          ),
382 383 384 385 386 387 388
        ),
      ),
    );
    await tester.dragFrom(const Offset(100.0, 100.0), const Offset(0.0, 100.0));
    expect(scrolled, isFalse);
  });

Ian Hickson's avatar
Ian Hickson committed
389
  testWidgets('physics:AlwaysScrollableScrollPhysics actually overrides primary:false default behavior', (WidgetTester tester) async {
390 391
    bool scrolled = false;
    await tester.pumpWidget(
392
      Directionality(
393
        textDirection: TextDirection.ltr,
394
        child: NotificationListener<OverscrollNotification>(
395 396 397 398
          onNotification: (OverscrollNotification message) {
            scrolled = true;
            return false;
          },
399
          child: ListView(
400 401
            primary: false,
            physics: const AlwaysScrollableScrollPhysics(),
402
            children: const <Widget>[],
403
          ),
404 405 406 407 408 409 410
        ),
      ),
    );
    await tester.dragFrom(const Offset(100.0, 100.0), const Offset(0.0, 100.0));
    expect(scrolled, isTrue);
  });

Ian Hickson's avatar
Ian Hickson committed
411
  testWidgets('physics:ScrollPhysics actually overrides primary:true default behavior', (WidgetTester tester) async {
412 413
    bool scrolled = false;
    await tester.pumpWidget(
414
      Directionality(
415
        textDirection: TextDirection.ltr,
416
        child: NotificationListener<OverscrollNotification>(
417 418 419 420
          onNotification: (OverscrollNotification message) {
            scrolled = true;
            return false;
          },
421
          child: ListView(
422 423
            primary: true,
            physics: const ScrollPhysics(),
424
            children: const <Widget>[],
425
          ),
426 427 428 429 430 431
        ),
      ),
    );
    await tester.dragFrom(const Offset(100.0, 100.0), const Offset(0.0, 100.0));
    expect(scrolled, isFalse);
  });
432 433 434 435 436 437 438 439 440 441 442 443 444 445

  testWidgets('separatorBuilder must return something', (WidgetTester tester) async {
    const List<String> listOfValues = <String>['ALPHA', 'BETA', 'GAMMA', 'DELTA'];

    Widget buildFrame(Widget firstSeparator) {
      return MaterialApp(
        home: Material(
          child: ListView.separated(
            itemBuilder: (BuildContext context, int index) {
              return Text(listOfValues[index]);
            },
            separatorBuilder: (BuildContext context, int index) {
              if (index == 0) {
                return firstSeparator;
446
              } else {
447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 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 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561
                return const Divider();
              }
            },
            itemCount: listOfValues.length,
          ),
        ),
      );
    }

    // A separatorBuilder that always returns a Divider is fine
    await tester.pumpWidget(buildFrame(const Divider()));
    expect(tester.takeException(), isNull);

    // A separatorBuilder that returns null throws a FlutterError
    await tester.pumpWidget(buildFrame(null));
    expect(tester.takeException(), isInstanceOf<FlutterError>());
    expect(find.byType(ErrorWidget), findsOneWidget);
  });

  testWidgets('itemBuilder can return null', (WidgetTester tester) async {
    const List<String> listOfValues = <String>['ALPHA', 'BETA', 'GAMMA', 'DELTA'];
    const Key key = Key('list');
    const int RENDER_NULL_AT = 2; // only render the first 2 values

    Widget buildFrame() {
      return MaterialApp(
        home: Material(
          child: ListView.builder(
            key: key,
            itemBuilder: (BuildContext context, int index) {
              if (index == RENDER_NULL_AT) {
                return null;
              }
              return Text(listOfValues[index]);
            },
            itemCount: listOfValues.length,
          ),
        ),
      );
    }

    // The length of a list is itemCount or the index of the first itemBuilder
    // that returns null, whichever is smaller
    await tester.pumpWidget(buildFrame());
    expect(tester.takeException(), isNull);
    expect(find.byType(ErrorWidget), findsNothing);
    expect(find.byType(Text), findsNWidgets(RENDER_NULL_AT));
  });

  testWidgets('when itemBuilder throws, creates Error Widget', (WidgetTester tester) async {
    const List<String> listOfValues = <String>['ALPHA', 'BETA', 'GAMMA', 'DELTA'];

    Widget buildFrame(bool throwOnFirstItem) {
      return MaterialApp(
        home: Material(
          child: ListView.builder(
            itemBuilder: (BuildContext context, int index) {
              if (index == 0 && throwOnFirstItem) {
                throw Exception('itemBuilder fail');
              }
              return Text(listOfValues[index]);
            },
            itemCount: listOfValues.length,
          ),
        ),
      );
    }

    // When itemBuilder doesn't throw, no ErrorWidget
    await tester.pumpWidget(buildFrame(false));
    expect(tester.takeException(), isNull);
    final Finder finder = find.byType(ErrorWidget);
    expect(find.byType(ErrorWidget), findsNothing);

    // When it does throw, one error widget is rendered in the item's place
    await tester.pumpWidget(buildFrame(true));
    expect(tester.takeException(), isInstanceOf<Exception>());
    expect(finder, findsOneWidget);
  });

  testWidgets('when separatorBuilder throws, creates ErrorWidget', (WidgetTester tester) async {
    const List<String> listOfValues = <String>['ALPHA', 'BETA', 'GAMMA', 'DELTA'];
    const Key key = Key('list');

    Widget buildFrame(bool throwOnFirstSeparator) {
      return MaterialApp(
        home: Material(
          child: ListView.separated(
            key: key,
            itemBuilder: (BuildContext context, int index) {
              return Text(listOfValues[index]);
            },
            separatorBuilder: (BuildContext context, int index) {
              if (index == 0 && throwOnFirstSeparator) {
                throw Exception('separatorBuilder fail');
              }
              return const Divider();
            },
            itemCount: listOfValues.length,
          ),
        ),
      );
    }

    // When separatorBuilder doesn't throw, no ErrorWidget
    await tester.pumpWidget(buildFrame(false));
    expect(tester.takeException(), isNull);
    final Finder finder = find.byType(ErrorWidget);
    expect(find.byType(ErrorWidget), findsNothing);

    // When it does throw, one error widget is rendered in the separator's place
    await tester.pumpWidget(buildFrame(true));
    expect(tester.takeException(), isInstanceOf<Exception>());
    expect(finder, findsOneWidget);
  });
562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590

  testWidgets('ListView.builder asserts on negative childCount', (WidgetTester tester) async {
    expect(() => ListView.builder(
      itemBuilder: (BuildContext context, int index) {
        return const SizedBox();
      },
      itemCount: -1,
    ), throwsA(isInstanceOf<AssertionError>()));
  });

  testWidgets('ListView.builder asserts on negative semanticChildCount', (WidgetTester tester) async {
    expect(() => ListView.builder(
      itemBuilder: (BuildContext context, int index) {
        return const SizedBox();
      },
      itemCount: 1,
      semanticChildCount: -1,
    ), throwsA(isInstanceOf<AssertionError>()));
  });

  testWidgets('ListView.builder asserts on nonsensical childCount/semanticChildCount', (WidgetTester tester) async {
    expect(() => ListView.builder(
      itemBuilder: (BuildContext context, int index) {
        return const SizedBox();
      },
      itemCount: 1,
      semanticChildCount: 4,
    ), throwsA(isInstanceOf<AssertionError>()));
  });
591
}