page_view_test.dart 33.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
import 'package:flutter/gestures.dart' show DragStartBehavior;
6
import 'package:flutter/material.dart';
7
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';
11
import 'semantics_tester.dart';
Adam Barth's avatar
Adam Barth committed
12 13
import 'states.dart';

14
const Duration _frameDuration = Duration(milliseconds: 100);
Adam Barth's avatar
Adam Barth committed
15 16

void main() {
17 18 19 20 21 22
  testWidgets('PageController cannot return page while unattached',
      (WidgetTester tester) async {
    final PageController controller = PageController();
    expect(() => controller.page, throwsAssertionError);
  });

Adam Barth's avatar
Adam Barth committed
23
  testWidgets('PageView control test', (WidgetTester tester) async {
24
    final List<String> log = <String>[];
Adam Barth's avatar
Adam Barth committed
25

26
    await tester.pumpWidget(Directionality(
27
      textDirection: TextDirection.ltr,
28
      child: PageView(
29
        dragStartBehavior: DragStartBehavior.down,
30
        children: kStates.map<Widget>((String state) {
31
          return GestureDetector(
32
            dragStartBehavior: DragStartBehavior.down,
33 34 35
            onTap: () {
              log.add(state);
            },
36
            child: Container(
37 38
              height: 200.0,
              color: const Color(0xFF0000FF),
39
              child: Text(state),
40 41 42 43
            ),
          );
        }).toList(),
      ),
Adam Barth's avatar
Adam Barth committed
44 45 46 47 48 49 50 51
    ));

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

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

52
    await tester.drag(find.byType(PageView), const Offset(-20.0, 0.0));
Adam Barth's avatar
Adam Barth committed
53 54 55 56 57 58
    await tester.pump();

    expect(find.text('Alabama'), findsOneWidget);
    expect(find.text('Alaska'), findsOneWidget);
    expect(find.text('Arizona'), findsNothing);

59
    await tester.pumpAndSettle(_frameDuration);
Adam Barth's avatar
Adam Barth committed
60 61 62 63

    expect(find.text('Alabama'), findsOneWidget);
    expect(find.text('Alaska'), findsNothing);

64 65
    await tester.drag(find.byType(PageView), const Offset(-401.0, 0.0));
    await tester.pumpAndSettle(_frameDuration);
Adam Barth's avatar
Adam Barth committed
66 67 68 69 70 71 72 73 74

    expect(find.text('Alabama'), findsNothing);
    expect(find.text('Alaska'), findsOneWidget);
    expect(find.text('Arizona'), findsNothing);

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

75
    await tester.fling(find.byType(PageView), const Offset(-200.0, 0.0), 1000.0);
76
    await tester.pumpAndSettle(_frameDuration);
Adam Barth's avatar
Adam Barth committed
77 78 79 80 81 82

    expect(find.text('Alabama'), findsNothing);
    expect(find.text('Alaska'), findsNothing);
    expect(find.text('Arizona'), findsOneWidget);

    await tester.fling(find.byType(PageView), const Offset(200.0, 0.0), 1000.0);
83
    await tester.pumpAndSettle(_frameDuration);
Adam Barth's avatar
Adam Barth committed
84 85 86 87 88

    expect(find.text('Alabama'), findsNothing);
    expect(find.text('Alaska'), findsOneWidget);
    expect(find.text('Arizona'), findsNothing);
  });
89

90
  testWidgets('PageView does not squish when overscrolled', (WidgetTester tester) async {
91 92 93 94 95
    await tester.pumpWidget(MaterialApp(
      home: PageView(
        children: List<Widget>.generate(10, (int i) {
          return Container(
            key: ValueKey<int>(i),
96
            color: const Color(0xFF0000FF),
97 98 99 100 101
          );
        }),
      ),
    ));

102 103
    Size sizeOf(int i) => tester.getSize(find.byKey(ValueKey<int>(i)));
    double leftOf(int i) => tester.getTopLeft(find.byKey(ValueKey<int>(i))).dx;
104 105 106 107

    expect(leftOf(0), equals(0.0));
    expect(sizeOf(0), equals(const Size(800.0, 600.0)));

108
    // Going into overscroll.
109
    await tester.drag(find.byType(PageView), const Offset(100.0, 0.0));
110 111
    await tester.pump();

112
    expect(leftOf(0), greaterThan(0.0));
113 114
    expect(sizeOf(0), equals(const Size(800.0, 600.0)));

115
    // Easing overscroll past overscroll limit.
116
    await tester.drag(find.byType(PageView), const Offset(-200.0, 0.0));
117 118
    await tester.pump();

119
    expect(leftOf(0), lessThan(0.0));
120
    expect(sizeOf(0), equals(const Size(800.0, 600.0)));
Dan Field's avatar
Dan Field committed
121
  }, variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.iOS,  TargetPlatform.macOS }));
122 123

  testWidgets('PageController control test', (WidgetTester tester) async {
124
    final PageController controller = PageController(initialPage: 4);
125

126
    await tester.pumpWidget(Directionality(
127
      textDirection: TextDirection.ltr,
128 129
      child: Center(
        child: SizedBox(
130 131
          width: 600.0,
          height: 400.0,
132
          child: PageView(
133
            controller: controller,
134
            children: kStates.map<Widget>((String state) => Text(state)).toList(),
135
          ),
136 137 138 139 140 141
        ),
      ),
    ));

    expect(find.text('California'), findsOneWidget);

142 143
    controller.nextPage(duration: const Duration(milliseconds: 150), curve: Curves.ease);
    await tester.pumpAndSettle(const Duration(milliseconds: 100));
144 145 146

    expect(find.text('Colorado'), findsOneWidget);

147
    await tester.pumpWidget(Directionality(
148
      textDirection: TextDirection.ltr,
149 150
      child: Center(
        child: SizedBox(
151 152
          width: 300.0,
          height: 400.0,
153
          child: PageView(
154
            controller: controller,
155
            children: kStates.map<Widget>((String state) => Text(state)).toList(),
156
          ),
157 158 159 160 161 162
        ),
      ),
    ));

    expect(find.text('Colorado'), findsOneWidget);

163 164
    controller.previousPage(duration: const Duration(milliseconds: 150), curve: Curves.ease);
    await tester.pumpAndSettle(const Duration(milliseconds: 100));
165 166 167 168 169

    expect(find.text('California'), findsOneWidget);
  });

  testWidgets('PageController page stability', (WidgetTester tester) async {
170
    await tester.pumpWidget(Directionality(
171
      textDirection: TextDirection.ltr,
172 173
      child: Center(
        child: SizedBox(
174 175
          width: 600.0,
          height: 400.0,
176 177
          child: PageView(
            children: kStates.map<Widget>((String state) => Text(state)).toList(),
178
          ),
179
        ),
180 181 182 183 184
      ),
    ));

    expect(find.text('Alabama'), findsOneWidget);

185 186
    await tester.drag(find.byType(PageView), const Offset(-1250.0, 0.0));
    await tester.pumpAndSettle(const Duration(milliseconds: 100));
187 188 189

    expect(find.text('Arizona'), findsOneWidget);

190
    await tester.pumpWidget(Directionality(
191
      textDirection: TextDirection.ltr,
192 193
      child: Center(
        child: SizedBox(
194 195
          width: 250.0,
          height: 100.0,
196 197
          child: PageView(
            children: kStates.map<Widget>((String state) => Text(state)).toList(),
198
          ),
199 200 201 202 203 204
        ),
      ),
    ));

    expect(find.text('Arizona'), findsOneWidget);

205
    await tester.pumpWidget(Directionality(
206
      textDirection: TextDirection.ltr,
207 208
      child: Center(
        child: SizedBox(
209 210
          width: 450.0,
          height: 400.0,
211 212
          child: PageView(
            children: kStates.map<Widget>((String state) => Text(state)).toList(),
213
          ),
214 215 216 217 218 219
        ),
      ),
    ));

    expect(find.text('Arizona'), findsOneWidget);
  });
220

221
  testWidgets('PageController nextPage and previousPage return Futures that resolve', (WidgetTester tester) async {
222 223
    final PageController controller = PageController();
    await tester.pumpWidget(Directionality(
224
        textDirection: TextDirection.ltr,
225
        child: PageView(
226
          controller: controller,
227
          children: kStates.map<Widget>((String state) => Text(state)).toList(),
228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252
        ),
    ));

    bool nextPageCompleted = false;
    controller.nextPage(duration: const Duration(milliseconds: 150), curve: Curves.ease)
        .then((_) => nextPageCompleted = true);

    expect(nextPageCompleted, false);
    await tester.pump(const Duration(milliseconds: 200));
    expect(nextPageCompleted, false);
    await tester.pump(const Duration(milliseconds: 200));
    expect(nextPageCompleted, true);


    bool previousPageCompleted = false;
    controller.previousPage(duration: const Duration(milliseconds: 150), curve: Curves.ease)
        .then((_) => previousPageCompleted = true);

    expect(previousPageCompleted, false);
    await tester.pump(const Duration(milliseconds: 200));
    expect(previousPageCompleted, false);
    await tester.pump(const Duration(milliseconds: 200));
    expect(previousPageCompleted, true);
  });

253
  testWidgets('PageView in zero-size container', (WidgetTester tester) async {
254
    await tester.pumpWidget(Directionality(
255
      textDirection: TextDirection.ltr,
256 257
      child: Center(
        child: SizedBox(
258 259
          width: 0.0,
          height: 0.0,
260 261
          child: PageView(
            children: kStates.map<Widget>((String state) => Text(state)).toList(),
262
          ),
263 264 265 266
        ),
      ),
    ));

267
    expect(find.text('Alabama', skipOffstage: false), findsOneWidget);
268

269
    await tester.pumpWidget(Directionality(
270
      textDirection: TextDirection.ltr,
271 272
      child: Center(
        child: SizedBox(
273 274
          width: 200.0,
          height: 200.0,
275 276
          child: PageView(
            children: kStates.map<Widget>((String state) => Text(state)).toList(),
277
          ),
278 279 280 281 282
        ),
      ),
    ));

    expect(find.text('Alabama'), findsOneWidget);
283
  });
284 285 286

  testWidgets('Page changes at halfway point', (WidgetTester tester) async {
    final List<int> log = <int>[];
287
    await tester.pumpWidget(Directionality(
288
      textDirection: TextDirection.ltr,
289
      child: PageView(
290
        onPageChanged: log.add,
291
        children: kStates.map<Widget>((String state) => Text(state)).toList(),
292
      ),
293 294 295 296
    ));

    expect(log, isEmpty);

297
    final TestGesture gesture =
298
        await tester.startGesture(const Offset(100.0, 100.0));
299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326
    // The page view is 800.0 wide, so this move is just short of halfway.
    await gesture.moveBy(const Offset(-380.0, 0.0));

    expect(log, isEmpty);

    // We've crossed the halfway mark.
    await gesture.moveBy(const Offset(-40.0, 0.0));

    expect(log, equals(const <int>[1]));
    log.clear();

    // Moving a bit more should not generate redundant notifications.
    await gesture.moveBy(const Offset(-40.0, 0.0));

    expect(log, isEmpty);

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

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

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

    expect(log, isEmpty);

    await gesture.up();
327
    await tester.pumpAndSettle();
328 329 330 331 332 333 334

    expect(log, isEmpty);

    expect(find.text('Alabama'), findsNothing);
    expect(find.text('Alaska'), findsOneWidget);
  });

335 336
  testWidgets('Bouncing scroll physics ballistics does not overshoot', (WidgetTester tester) async {
    final List<int> log = <int>[];
337
    final PageController controller = PageController(viewportFraction: 0.9);
338

339
    Widget build(PageController controller, { Size? size }) {
340
      final Widget pageView = Directionality(
341
        textDirection: TextDirection.ltr,
342
        child: PageView(
343 344 345
          controller: controller,
          onPageChanged: log.add,
          physics: const BouncingScrollPhysics(),
346
          children: kStates.map<Widget>((String state) => Text(state)).toList(),
347 348 349 350
        ),
      );

      if (size != null) {
351
        return OverflowBox(
352 353 354 355
          minWidth: size.width,
          minHeight: size.height,
          maxWidth: size.width,
          maxHeight: size.height,
356
          child: pageView,
357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388
        );
      } else {
        return pageView;
      }
    }

    await tester.pumpWidget(build(controller));
    expect(log, isEmpty);

    // Fling right to move to a non-existent page at the beginning of the
    // PageView, and confirm that the PageView settles back on the first page.
    await tester.fling(find.byType(PageView), const Offset(100.0, 0.0), 800.0);
    await tester.pumpAndSettle();
    expect(log, isEmpty);

    expect(find.text('Alabama'), findsOneWidget);
    expect(find.text('Alaska'), findsOneWidget);
    expect(find.text('Arizona'), findsNothing);

    // Try again with a Cupertino "Plus" device size.
    await tester.pumpWidget(build(controller, size: const Size(414.0, 736.0)));
    expect(log, isEmpty);

    await tester.fling(find.byType(PageView), const Offset(100.0, 0.0), 800.0);
    await tester.pumpAndSettle();
    expect(log, isEmpty);

    expect(find.text('Alabama'), findsOneWidget);
    expect(find.text('Alaska'), findsOneWidget);
    expect(find.text('Arizona'), findsNothing);
  });

389
  testWidgets('PageView viewportFraction', (WidgetTester tester) async {
390
    PageController controller = PageController(viewportFraction: 7/8);
391 392

    Widget build(PageController controller) {
393
      return Directionality(
394
        textDirection: TextDirection.ltr,
395
        child: PageView.builder(
396 397 398
          controller: controller,
          itemCount: kStates.length,
          itemBuilder: (BuildContext context, int index) {
399
            return Container(
400
              height: 200.0,
401
              color: index.isEven
402 403
                ? const Color(0xFF0000FF)
                : const Color(0xFF00FF00),
404
              child: Text(kStates[index]),
405 406 407
            );
          },
        ),
408 409 410 411 412
      );
    }

    await tester.pumpWidget(build(controller));

413 414
    expect(tester.getTopLeft(find.text('Alabama')), const Offset(50.0, 0.0));
    expect(tester.getTopLeft(find.text('Alaska')), const Offset(750.0, 0.0));
415 416 417 418

    controller.jumpToPage(10);
    await tester.pump();

419 420 421
    expect(tester.getTopLeft(find.text('Georgia')), const Offset(-650.0, 0.0));
    expect(tester.getTopLeft(find.text('Hawaii')), const Offset(50.0, 0.0));
    expect(tester.getTopLeft(find.text('Idaho')), const Offset(750.0, 0.0));
422

423
    controller = PageController(viewportFraction: 39/40);
424 425 426

    await tester.pumpWidget(build(controller));

427 428 429
    expect(tester.getTopLeft(find.text('Georgia')), const Offset(-770.0, 0.0));
    expect(tester.getTopLeft(find.text('Hawaii')), const Offset(10.0, 0.0));
    expect(tester.getTopLeft(find.text('Idaho')), const Offset(790.0, 0.0));
430 431
  });

432 433 434
  testWidgets('Page snapping disable and reenable', (WidgetTester tester) async {
    final List<int> log = <int>[];

435
    Widget build({ required bool pageSnapping }) {
436
      return Directionality(
437
        textDirection: TextDirection.ltr,
438
        child: PageView(
439 440 441
          pageSnapping: pageSnapping,
          onPageChanged: log.add,
          children:
442
              kStates.map<Widget>((String state) => Text(state)).toList(),
443 444 445 446 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
        ),
      );
    }

    await tester.pumpWidget(build(pageSnapping: true));
    expect(log, isEmpty);

    // Drag more than halfway to the next page, to confirm the default behavior.
    TestGesture gesture = await tester.startGesture(const Offset(100.0, 100.0));
    // The page view is 800.0 wide, so this move is just beyond halfway.
    await gesture.moveBy(const Offset(-420.0, 0.0));

    expect(log, equals(const <int>[1]));
    log.clear();

    // Release the gesture, confirm that the page settles on the next.
    await gesture.up();
    await tester.pumpAndSettle();

    expect(find.text('Alabama'), findsNothing);
    expect(find.text('Alaska'), findsOneWidget);

    // Disable page snapping, and try moving halfway. Confirm it doesn't snap.
    await tester.pumpWidget(build(pageSnapping: false));
    gesture = await tester.startGesture(const Offset(100.0, 100.0));
    // Move just beyond halfway, again.
    await gesture.moveBy(const Offset(-420.0, 0.0));

    // Page notifications still get sent.
    expect(log, equals(const <int>[2]));
    log.clear();

    // Release the gesture, confirm that both pages are visible.
    await gesture.up();
    await tester.pumpAndSettle();

    expect(find.text('Alabama'), findsNothing);
    expect(find.text('Alaska'), findsOneWidget);
    expect(find.text('Arizona'), findsOneWidget);
    expect(find.text('Arkansas'), findsNothing);

    // Now re-enable snapping, confirm that we've settled on a page.
    await tester.pumpWidget(build(pageSnapping: true));
    await tester.pumpAndSettle();

    expect(log, isEmpty);

    expect(find.text('Alaska'), findsNothing);
    expect(find.text('Arizona'), findsOneWidget);
    expect(find.text('Arkansas'), findsNothing);
  });

495
  testWidgets('PageView small viewportFraction', (WidgetTester tester) async {
496
    final PageController controller = PageController(viewportFraction: 1/8);
497 498

    Widget build(PageController controller) {
499
      return Directionality(
500
        textDirection: TextDirection.ltr,
501
        child: PageView.builder(
502 503 504
          controller: controller,
          itemCount: kStates.length,
          itemBuilder: (BuildContext context, int index) {
505
            return Container(
506
              height: 200.0,
507
              color: index.isEven
508 509
                ? const Color(0xFF0000FF)
                : const Color(0xFF00FF00),
510
              child: Text(kStates[index]),
511 512 513
            );
          },
        ),
514 515 516 517 518
      );
    }

    await tester.pumpWidget(build(controller));

519 520 521 522 523
    expect(tester.getTopLeft(find.text('Alabama')), const Offset(350.0, 0.0));
    expect(tester.getTopLeft(find.text('Alaska')), const Offset(450.0, 0.0));
    expect(tester.getTopLeft(find.text('Arizona')), const Offset(550.0, 0.0));
    expect(tester.getTopLeft(find.text('Arkansas')), const Offset(650.0, 0.0));
    expect(tester.getTopLeft(find.text('California')), const Offset(750.0, 0.0));
524 525 526 527

    controller.jumpToPage(10);
    await tester.pump();

528 529 530 531 532 533 534 535 536
    expect(tester.getTopLeft(find.text('Connecticut')), const Offset(-50.0, 0.0));
    expect(tester.getTopLeft(find.text('Delaware')), const Offset(50.0, 0.0));
    expect(tester.getTopLeft(find.text('Florida')), const Offset(150.0, 0.0));
    expect(tester.getTopLeft(find.text('Georgia')), const Offset(250.0, 0.0));
    expect(tester.getTopLeft(find.text('Hawaii')), const Offset(350.0, 0.0));
    expect(tester.getTopLeft(find.text('Idaho')), const Offset(450.0, 0.0));
    expect(tester.getTopLeft(find.text('Illinois')), const Offset(550.0, 0.0));
    expect(tester.getTopLeft(find.text('Indiana')), const Offset(650.0, 0.0));
    expect(tester.getTopLeft(find.text('Iowa')), const Offset(750.0, 0.0));
537 538 539
  });

  testWidgets('PageView large viewportFraction', (WidgetTester tester) async {
540
    final PageController controller = PageController(viewportFraction: 5/4);
541 542

    Widget build(PageController controller) {
543
      return Directionality(
544
        textDirection: TextDirection.ltr,
545
        child: PageView.builder(
546 547 548
          controller: controller,
          itemCount: kStates.length,
          itemBuilder: (BuildContext context, int index) {
549
            return Container(
550
              height: 200.0,
551
              color: index.isEven
552 553
                ? const Color(0xFF0000FF)
                : const Color(0xFF00FF00),
554
              child: Text(kStates[index]),
555 556 557
            );
          },
        ),
558 559 560 561 562
      );
    }

    await tester.pumpWidget(build(controller));

563 564
    expect(tester.getTopLeft(find.text('Alabama')), const Offset(-100.0, 0.0));
    expect(tester.getBottomRight(find.text('Alabama')), const Offset(900.0, 600.0));
565 566 567 568

    controller.jumpToPage(10);
    await tester.pump();

569
    expect(tester.getTopLeft(find.text('Hawaii')), const Offset(-100.0, 0.0));
570
  });
571

572 573 574 575 576 577 578 579 580 581 582 583
  testWidgets(
    'Updating PageView large viewportFraction',
    (WidgetTester tester) async {
      Widget build(PageController controller) {
        return Directionality(
          textDirection: TextDirection.ltr,
          child: PageView.builder(
            controller: controller,
            itemCount: kStates.length,
            itemBuilder: (BuildContext context, int index) {
              return Container(
                height: 200.0,
584
                color: index.isEven
585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601 602 603 604 605
                  ? const Color(0xFF0000FF)
                  : const Color(0xFF00FF00),
                child: Text(kStates[index]),
              );
            },
          ),
        );
      }

      final PageController oldController = PageController(viewportFraction: 5/4);
      await tester.pumpWidget(build(oldController));

      expect(tester.getTopLeft(find.text('Alabama')), const Offset(-100, 0));
      expect(tester.getBottomRight(find.text('Alabama')), const Offset(900.0, 600.0));

      final PageController newController = PageController(viewportFraction: 4);
      await tester.pumpWidget(build(newController));
      newController.jumpToPage(10);
      await tester.pump();

      expect(tester.getTopLeft(find.text('Hawaii')), const Offset(-(4 - 1) * 800 / 2, 0));
606 607
    },
  );
608

609 610 611 612 613 614 615 616 617 618 619 620 621 622 623
  testWidgets(
    'PageView large viewportFraction can scroll to the last page and snap',
    (WidgetTester tester) async {
      // Regression test for https://github.com/flutter/flutter/issues/45096.
      final PageController controller = PageController(viewportFraction: 5/4);

      Widget build(PageController controller) {
        return Directionality(
          textDirection: TextDirection.ltr,
          child: PageView.builder(
            controller: controller,
            itemCount: 3,
            itemBuilder: (BuildContext context, int index) {
              return Container(
                height: 200.0,
624
                color: index.isEven
625 626 627 628 629 630 631 632 633 634 635 636 637 638 639 640 641 642
                  ? const Color(0xFF0000FF)
                  : const Color(0xFF00FF00),
                  child: Text(index.toString()),
              );
            },
          ),
        );
      }

      await tester.pumpWidget(build(controller));

      expect(tester.getCenter(find.text('0')), const Offset(400, 300));

      controller.jumpToPage(2);
      await tester.pump();
      await tester.pumpAndSettle();

      expect(tester.getCenter(find.text('2')), const Offset(400, 300));
643 644
    },
  );
645 646 647 648 649 650

  testWidgets(
    'All visible pages are able to receive touch events',
    (WidgetTester tester) async {
      // Regression test for https://github.com/flutter/flutter/issues/23873.
      final PageController controller = PageController(viewportFraction: 1/4, initialPage: 0);
651
      late int tappedIndex;
652 653 654 655 656 657 658 659 660 661 662 663 664 665 666 667 668 669 670 671 672

      Widget build() {
        return Directionality(
          textDirection: TextDirection.ltr,
          child: PageView.builder(
            controller: controller,
            itemCount: 20,
            itemBuilder: (BuildContext context, int index) {
              return GestureDetector(
                onTap: () => tappedIndex = index,
                child: SizedBox.expand(child: Text('$index')),
              );
            },
          ),
        );
      }

      Iterable<int> visiblePages = const <int> [0, 1, 2];
      await tester.pumpWidget(build());

      // The first 3 items should be visible and tappable.
673
      for (final int index in visiblePages) {
674 675 676 677 678 679 680 681 682 683 684 685
        expect(find.text(index.toString()), findsOneWidget);
        // The center of page 2's x-coordinate is 800, so we have to manually
        // offset it a bit to make sure the tap lands within the screen.
        final Offset center = tester.getCenter(find.text('$index')) - const Offset(3, 0);
        await tester.tapAt(center);
        expect(tappedIndex, index);
      }

      controller.jumpToPage(19);
      await tester.pump();
      // The last 3 items should be visible and tappable.
      visiblePages = const <int> [17, 18, 19];
686
      for (final int index in visiblePages) {
687 688 689 690
        expect(find.text('$index'), findsOneWidget);
        await tester.tap(find.text('$index'));
        expect(tappedIndex, index);
      }
691 692
    },
  );
693

694 695 696 697 698 699 700 701 702 703 704 705 706 707 708
  testWidgets('the current item remains centered on constraint change', (WidgetTester tester) async {
    // Regression test for https://github.com/flutter/flutter/issues/50505.
    final PageController controller = PageController(
      initialPage: kStates.length - 1,
      viewportFraction: 0.5,
    );

    Widget build(Size size) {
      return Directionality(
        textDirection: TextDirection.ltr,
        child: Center(
          child: SizedBox.fromSize(
            size: size,
            child: PageView(
              controller: controller,
709
              children: kStates.map<Widget>((String state) => Text(state)).toList(),
710 711 712 713 714 715 716 717 718 719 720 721 722 723 724 725 726 727 728 729 730 731 732 733 734 735
              onPageChanged: (int page) { },
            ),
          ),
        ),
      );
    }

    // Verifies that the last item is centered on screen.
    void verifyCentered() {
      expect(
        tester.getCenter(find.text(kStates.last)),
        offsetMoreOrLessEquals(const Offset(400, 300)),
      );
    }

    await tester.pumpWidget(build(const Size(300, 300)));
    await tester.pumpAndSettle();

    verifyCentered();

    await tester.pumpWidget(build(const Size(200, 300)));
    await tester.pumpAndSettle();

    verifyCentered();
  });

736
  testWidgets('PageView does not report page changed on overscroll', (WidgetTester tester) async {
737
    final PageController controller = PageController(
738 739
      initialPage: kStates.length - 1,
    );
740 741
    int changeIndex = 0;
    Widget build() {
742
      return Directionality(
743
        textDirection: TextDirection.ltr,
744
        child: PageView(
745
          controller: controller,
746
          children: kStates.map<Widget>((String state) => Text(state)).toList(),
747 748 749 750
          onPageChanged: (int page) {
            changeIndex = page;
          },
        ),
751 752 753 754 755 756 757 758 759 760
      );
    }

    await tester.pumpWidget(build());
    controller.jumpToPage(kStates.length * 2); // try to move beyond max range
    // change index should be zero, shouldn't fire onPageChanged
    expect(changeIndex, 0);
    await tester.pump();
    expect(changeIndex, 0);
  });
761

762
  testWidgets('PageView can restore page', (WidgetTester tester) async {
763
    final PageController controller = PageController();
764 765
    expect(
      () => controller.page,
766
      throwsA(isAssertionError.having(
767 768 769 770 771
        (AssertionError error) => error.message,
        'message',
        equals('PageController.page cannot be accessed before a PageView is built with it.'),
      )),
    );
772 773
    final PageStorageBucket bucket = PageStorageBucket();
    await tester.pumpWidget(Directionality(
774
      textDirection: TextDirection.ltr,
775
      child: PageStorage(
776
        bucket: bucket,
777
        child: PageView(
778
          key: const PageStorageKey<String>('PageView'),
779
          controller: controller,
780
          children: const <Widget>[
781 782 783
            Placeholder(),
            Placeholder(),
            Placeholder(),
784 785 786
          ],
        ),
      ),
787
    ));
788 789 790 791 792
    expect(controller.page, 0);
    controller.jumpToPage(2);
    expect(await tester.pumpAndSettle(const Duration(minutes: 1)), 1);
    expect(controller.page, 2);
    await tester.pumpWidget(
793
      PageStorage(
794
        bucket: bucket,
795
        child: Container(),
796 797
      ),
    );
798 799
    expect(
      () => controller.page,
800
      throwsA(isAssertionError.having(
801 802 803 804 805
        (AssertionError error) => error.message,
        'message',
        equals('PageController.page cannot be accessed before a PageView is built with it.'),
      )),
    );
806
    await tester.pumpWidget(Directionality(
807
      textDirection: TextDirection.ltr,
808
      child: PageStorage(
809
        bucket: bucket,
810
        child: PageView(
811
          key: const PageStorageKey<String>('PageView'),
812
          controller: controller,
813
          children: const <Widget>[
814 815 816
            Placeholder(),
            Placeholder(),
            Placeholder(),
817 818 819
          ],
        ),
      ),
820
    ));
821
    expect(controller.page, 2);
822

823 824
    final PageController controller2 = PageController(keepPage: false);
    await tester.pumpWidget(Directionality(
825
      textDirection: TextDirection.ltr,
826
      child: PageStorage(
827
        bucket: bucket,
828
        child: PageView(
829
          key: const PageStorageKey<String>('Check it again against your list and see consistency!'),
830
          controller: controller2,
831
          children: const <Widget>[
832 833 834
            Placeholder(),
            Placeholder(),
            Placeholder(),
835 836 837
          ],
        ),
      ),
838
    ));
839
    expect(controller2.page, 0);
840
  });
841 842

  testWidgets('PageView exposes semantics of children', (WidgetTester tester) async {
843
    final SemanticsTester semantics = SemanticsTester(tester);
844

845 846
    final PageController controller = PageController();
    await tester.pumpWidget(Directionality(
847
      textDirection: TextDirection.ltr,
848
      child: PageView(
849
          controller: controller,
850 851
          children: List<Widget>.generate(3, (int i) {
            return Semantics(
852
              container: true,
853
              child: Text('Page #$i'),
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
        ),
    ));
    expect(controller.page, 0);

    expect(semantics, includesNodeWith(label: 'Page #0'));
    expect(semantics, isNot(includesNodeWith(label: 'Page #1')));
    expect(semantics, isNot(includesNodeWith(label: 'Page #2')));

    controller.jumpToPage(1);
    await tester.pumpAndSettle();

    expect(semantics, isNot(includesNodeWith(label: 'Page #0')));
    expect(semantics, includesNodeWith(label: 'Page #1'));
    expect(semantics, isNot(includesNodeWith(label: 'Page #2')));

    controller.jumpToPage(2);
    await tester.pumpAndSettle();

    expect(semantics, isNot(includesNodeWith(label: 'Page #0')));
    expect(semantics, isNot(includesNodeWith(label: 'Page #1')));
    expect(semantics, includesNodeWith(label: 'Page #2'));

    semantics.dispose();
  });
880 881

  testWidgets('PageMetrics', (WidgetTester tester) async {
882
    final PageMetrics page = PageMetrics(
883 884 885 886 887 888 889 890 891 892 893 894 895
      minScrollExtent: 100.0,
      maxScrollExtent: 200.0,
      pixels: 150.0,
      viewportDimension: 25.0,
      axisDirection: AxisDirection.right,
      viewportFraction: 1.0,
    );
    expect(page.page, 6);
    final PageMetrics page2 = page.copyWith(
      pixels: page.pixels - 100.0,
    );
    expect(page2.page, 4.0);
  });
896 897 898 899 900 901 902 903 904 905 906

  testWidgets('Page controller can handle rounding issue', (WidgetTester tester) async {
    final PageController pageController = PageController();

    await tester.pumpWidget(Directionality(
      textDirection: TextDirection.ltr,
      child: PageView(
        controller: pageController,
        children: List<Widget>.generate(3, (int i) {
          return Semantics(
            container: true,
907
            child: Text('Page #$i'),
908 909 910 911 912 913 914 915
          );
        }),
      ),
    ));
    // Simulate precision error.
    pageController.position.jumpTo(799.99999999999);
    expect(pageController.page, 1);
  });
916 917 918 919 920 921 922 923 924

  testWidgets('PageView can participate in a11y scrolling', (WidgetTester tester) async {
    final SemanticsTester semantics = SemanticsTester(tester);

    final PageController controller = PageController();
    await tester.pumpWidget(Directionality(
      textDirection: TextDirection.ltr,
      child: PageView(
          controller: controller,
925
          allowImplicitScrolling: true,
926 927 928
          children: List<Widget>.generate(4, (int i) {
            return Semantics(
              container: true,
929
              child: Text('Page #$i'),
930 931 932 933 934 935 936 937 938 939 940 941 942 943 944 945 946 947 948 949 950 951 952 953 954 955 956 957
            );
          }),
        ),
    ));
    expect(controller.page, 0);

    expect(semantics, includesNodeWith(flags: <SemanticsFlag>[SemanticsFlag.hasImplicitScrolling]));
    expect(semantics, includesNodeWith(label: 'Page #0'));
    expect(semantics, includesNodeWith(label: 'Page #1', flags: <SemanticsFlag>[SemanticsFlag.isHidden]));
    expect(semantics, isNot(includesNodeWith(label: 'Page #2', flags: <SemanticsFlag>[SemanticsFlag.isHidden])));
    expect(semantics, isNot(includesNodeWith(label: 'Page #3', flags: <SemanticsFlag>[SemanticsFlag.isHidden])));

    controller.nextPage(duration: const Duration(milliseconds: 150), curve: Curves.ease);
    await tester.pumpAndSettle();
    expect(semantics, includesNodeWith(label: 'Page #0', flags: <SemanticsFlag>[SemanticsFlag.isHidden]));
    expect(semantics, includesNodeWith(label: 'Page #1'));
    expect(semantics, includesNodeWith(label: 'Page #2', flags: <SemanticsFlag>[SemanticsFlag.isHidden]));
    expect(semantics, isNot(includesNodeWith(label: 'Page #3', flags: <SemanticsFlag>[SemanticsFlag.isHidden])));

    controller.nextPage(duration: const Duration(milliseconds: 150), curve: Curves.ease);
    await tester.pumpAndSettle();
    expect(semantics, isNot(includesNodeWith(label: 'Page #0', flags: <SemanticsFlag>[SemanticsFlag.isHidden])));
    expect(semantics, includesNodeWith(label: 'Page #1', flags: <SemanticsFlag>[SemanticsFlag.isHidden]));
    expect(semantics, includesNodeWith(label: 'Page #2'));
    expect(semantics, includesNodeWith(label: 'Page #3', flags: <SemanticsFlag>[SemanticsFlag.isHidden]));

    semantics.dispose();
  });
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

  testWidgets('PageView respects clipBehavior', (WidgetTester tester) async {
    await tester.pumpWidget(
      Directionality(
        textDirection: TextDirection.ltr,
        child: PageView(
          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: PageView(
          clipBehavior: Clip.antiAlias,
984
          children: <Widget>[Container(height: 2000.0)],
985 986 987 988 989 990 991 992 993
        ),
      ),
    );
    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));
  });
994 995 996 997 998 999 1000 1001 1002 1003 1004 1005 1006 1007 1008 1009 1010 1011 1012 1013 1014 1015 1016 1017 1018

  testWidgets('PageView.padEnds tests', (WidgetTester tester) async {
    Finder viewportFinder() => find.byType(SliverFillViewport, skipOffstage: false);

    // PageView() defaults to true.
    await tester.pumpWidget(Directionality(
      textDirection: TextDirection.ltr,
      child: PageView(
        children: const <Widget>[],
      ),
    ));

    expect(tester.widget<SliverFillViewport>(viewportFinder()).padEnds, true);

    // PageView(padEnds: false) is propagated properly.
    await tester.pumpWidget(Directionality(
      textDirection: TextDirection.ltr,
      child: PageView(
        padEnds: false,
        children: const <Widget>[],
      ),
    ));

    expect(tester.widget<SliverFillViewport>(viewportFinder()).padEnds, false);
  });
Adam Barth's avatar
Adam Barth committed
1019
}