page_view_test.dart 33.4 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 17

void main() {
  testWidgets('PageView control test', (WidgetTester tester) async {
18
    final List<String> log = <String>[];
Adam Barth's avatar
Adam Barth committed
19

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

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

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

46
    await tester.drag(find.byType(PageView), const Offset(-20.0, 0.0));
Adam Barth's avatar
Adam Barth committed
47 48 49 50 51 52
    await tester.pump();

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

53
    await tester.pumpAndSettle(_frameDuration);
Adam Barth's avatar
Adam Barth committed
54 55 56 57

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

58 59
    await tester.drag(find.byType(PageView), const Offset(-401.0, 0.0));
    await tester.pumpAndSettle(_frameDuration);
Adam Barth's avatar
Adam Barth committed
60 61 62 63 64 65 66 67 68

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

69
    await tester.fling(find.byType(PageView), const Offset(-200.0, 0.0), 1000.0);
70
    await tester.pumpAndSettle(_frameDuration);
Adam Barth's avatar
Adam Barth committed
71 72 73 74 75 76

    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);
77
    await tester.pumpAndSettle(_frameDuration);
Adam Barth's avatar
Adam Barth committed
78 79 80 81 82

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

84
  testWidgets('PageView does not squish when overscrolled', (WidgetTester tester) async {
85 86 87 88 89
    await tester.pumpWidget(MaterialApp(
      home: PageView(
        children: List<Widget>.generate(10, (int i) {
          return Container(
            key: ValueKey<int>(i),
90
            color: const Color(0xFF0000FF),
91 92 93 94 95
          );
        }),
      ),
    ));

96 97
    Size sizeOf(int i) => tester.getSize(find.byKey(ValueKey<int>(i)));
    double leftOf(int i) => tester.getTopLeft(find.byKey(ValueKey<int>(i))).dx;
98 99 100 101

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

102
    // Going into overscroll.
103
    await tester.drag(find.byType(PageView), const Offset(100.0, 0.0));
104 105
    await tester.pump();

106
    expect(leftOf(0), greaterThan(0.0));
107 108
    expect(sizeOf(0), equals(const Size(800.0, 600.0)));

109
    // Easing overscroll past overscroll limit.
110
    await tester.drag(find.byType(PageView), const Offset(-200.0, 0.0));
111 112
    await tester.pump();

113
    expect(leftOf(0), lessThan(0.0));
114
    expect(sizeOf(0), equals(const Size(800.0, 600.0)));
Dan Field's avatar
Dan Field committed
115
  }, variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.iOS,  TargetPlatform.macOS }));
116 117

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

120
    await tester.pumpWidget(Directionality(
121
      textDirection: TextDirection.ltr,
122 123
      child: Center(
        child: SizedBox(
124 125
          width: 600.0,
          height: 400.0,
126
          child: PageView(
127
            controller: controller,
128
            children: kStates.map<Widget>((String state) => Text(state)).toList(),
129
          ),
130 131 132 133 134 135
        ),
      ),
    ));

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

136 137
    controller.nextPage(duration: const Duration(milliseconds: 150), curve: Curves.ease);
    await tester.pumpAndSettle(const Duration(milliseconds: 100));
138 139 140

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

141
    await tester.pumpWidget(Directionality(
142
      textDirection: TextDirection.ltr,
143 144
      child: Center(
        child: SizedBox(
145 146
          width: 300.0,
          height: 400.0,
147
          child: PageView(
148
            controller: controller,
149
            children: kStates.map<Widget>((String state) => Text(state)).toList(),
150
          ),
151 152 153 154 155 156
        ),
      ),
    ));

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

157 158
    controller.previousPage(duration: const Duration(milliseconds: 150), curve: Curves.ease);
    await tester.pumpAndSettle(const Duration(milliseconds: 100));
159 160 161 162 163

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

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

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

179 180
    await tester.drag(find.byType(PageView), const Offset(-1250.0, 0.0));
    await tester.pumpAndSettle(const Duration(milliseconds: 100));
181 182 183

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

184
    await tester.pumpWidget(Directionality(
185
      textDirection: TextDirection.ltr,
186 187
      child: Center(
        child: SizedBox(
188 189
          width: 250.0,
          height: 100.0,
190 191
          child: PageView(
            children: kStates.map<Widget>((String state) => Text(state)).toList(),
192
          ),
193 194 195 196 197 198
        ),
      ),
    ));

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

199
    await tester.pumpWidget(Directionality(
200
      textDirection: TextDirection.ltr,
201 202
      child: Center(
        child: SizedBox(
203 204
          width: 450.0,
          height: 400.0,
205 206
          child: PageView(
            children: kStates.map<Widget>((String state) => Text(state)).toList(),
207
          ),
208 209 210 211 212 213
        ),
      ),
    ));

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

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

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

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

261
    expect(find.text('Alabama', skipOffstage: false), findsOneWidget);
262

263
    await tester.pumpWidget(Directionality(
264
      textDirection: TextDirection.ltr,
265 266
      child: Center(
        child: SizedBox(
267 268
          width: 200.0,
          height: 200.0,
269 270
          child: PageView(
            children: kStates.map<Widget>((String state) => Text(state)).toList(),
271
          ),
272 273 274 275 276
        ),
      ),
    ));

    expect(find.text('Alabama'), findsOneWidget);
277
  });
278 279 280

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

    expect(log, isEmpty);

291
    final TestGesture gesture =
292
        await tester.startGesture(const Offset(100.0, 100.0));
293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320
    // 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();
321
    await tester.pumpAndSettle();
322 323 324 325 326 327 328

    expect(log, isEmpty);

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

329 330
  testWidgets('Bouncing scroll physics ballistics does not overshoot', (WidgetTester tester) async {
    final List<int> log = <int>[];
331
    final PageController controller = PageController(viewportFraction: 0.9);
332

333
    Widget build(PageController controller, { Size? size }) {
334
      final Widget pageView = Directionality(
335
        textDirection: TextDirection.ltr,
336
        child: PageView(
337 338 339
          controller: controller,
          onPageChanged: log.add,
          physics: const BouncingScrollPhysics(),
340
          children: kStates.map<Widget>((String state) => Text(state)).toList(),
341 342 343 344
        ),
      );

      if (size != null) {
345
        return OverflowBox(
346 347 348 349
          minWidth: size.width,
          minHeight: size.height,
          maxWidth: size.width,
          maxHeight: size.height,
350
          child: pageView,
351 352 353 354 355 356 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
        );
      } 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);
  });

383
  testWidgets('PageView viewportFraction', (WidgetTester tester) async {
384
    PageController controller = PageController(viewportFraction: 7/8);
385 386

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

    await tester.pumpWidget(build(controller));

407 408
    expect(tester.getTopLeft(find.text('Alabama')), const Offset(50.0, 0.0));
    expect(tester.getTopLeft(find.text('Alaska')), const Offset(750.0, 0.0));
409 410 411 412

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

413 414 415
    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));
416

417
    controller = PageController(viewportFraction: 39/40);
418 419 420

    await tester.pumpWidget(build(controller));

421 422 423
    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));
424 425
  });

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

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

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

489
  testWidgets('PageView small viewportFraction', (WidgetTester tester) async {
490
    final PageController controller = PageController(viewportFraction: 1/8);
491 492

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

    await tester.pumpWidget(build(controller));

513 514 515 516 517
    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));
518 519 520 521

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

522 523 524 525 526 527 528 529 530
    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));
531 532 533
  });

  testWidgets('PageView large viewportFraction', (WidgetTester tester) async {
534
    final PageController controller = PageController(viewportFraction: 5/4);
535 536

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

    await tester.pumpWidget(build(controller));

557 558
    expect(tester.getTopLeft(find.text('Alabama')), const Offset(-100.0, 0.0));
    expect(tester.getBottomRight(find.text('Alabama')), const Offset(900.0, 600.0));
559 560 561 562

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

563
    expect(tester.getTopLeft(find.text('Hawaii')), const Offset(-100.0, 0.0));
564
  });
565

566 567 568 569 570 571 572 573 574 575 576 577
  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,
578
                color: index.isEven
579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599
                  ? 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));
600 601
    },
  );
602

603 604 605 606 607 608 609 610 611 612 613 614 615 616 617
  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,
618
                color: index.isEven
619 620 621 622 623 624 625 626 627 628 629 630 631 632 633 634 635 636
                  ? 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));
637 638
    },
  );
639 640 641 642 643 644

  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);
645
      late int tappedIndex;
646 647 648 649 650 651 652 653 654 655 656 657 658 659 660 661 662 663 664 665 666

      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.
667
      for (final int index in visiblePages) {
668 669 670 671 672 673 674 675 676 677 678 679
        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];
680
      for (final int index in visiblePages) {
681 682 683 684
        expect(find.text('$index'), findsOneWidget);
        await tester.tap(find.text('$index'));
        expect(tappedIndex, index);
      }
685 686
    },
  );
687

688 689 690 691 692 693 694 695 696 697 698 699 700 701 702
  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,
703
              children: kStates.map<Widget>((String state) => Text(state)).toList(),
704 705 706 707 708 709 710 711 712 713 714 715 716 717 718 719 720 721 722 723 724 725 726 727 728 729
              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();
  });

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

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

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

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

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

839 840
    final PageController controller = PageController();
    await tester.pumpWidget(Directionality(
841
      textDirection: TextDirection.ltr,
842
      child: PageView(
843
          controller: controller,
844 845
          children: List<Widget>.generate(3, (int i) {
            return Semantics(
846
              container: true,
847
              child: Text('Page #$i'),
848
            );
849
          }),
850 851 852 853 854 855 856 857 858 859 860 861 862 863 864 865 866 867 868 869 870 871 872 873
        ),
    ));
    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();
  });
874 875

  testWidgets('PageMetrics', (WidgetTester tester) async {
876
    final PageMetrics page = PageMetrics(
877 878 879 880 881 882 883 884 885 886 887 888 889
      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);
  });
890 891 892 893 894 895 896 897 898 899 900

  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,
901
            child: Text('Page #$i'),
902 903 904 905 906 907 908 909
          );
        }),
      ),
    ));
    // Simulate precision error.
    pageController.position.jumpTo(799.99999999999);
    expect(pageController.page, 1);
  });
910 911 912 913 914 915 916 917 918

  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,
919
          allowImplicitScrolling: true,
920 921 922
          children: List<Widget>.generate(4, (int i) {
            return Semantics(
              container: true,
923
              child: Text('Page #$i'),
924 925 926 927 928 929 930 931 932 933 934 935 936 937 938 939 940 941 942 943 944 945 946 947 948 949 950 951
            );
          }),
        ),
    ));
    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();
  });
952 953 954 955 956 957 958 959 960 961 962 963 964 965 966 967 968 969 970 971 972 973 974 975 976 977

  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,
978
          children: <Widget>[Container(height: 2000.0)],
979 980 981 982 983 984 985 986 987
        ),
      ),
    );
    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));
  });
988 989 990 991 992 993 994 995 996 997 998 999 1000 1001 1002 1003 1004 1005 1006 1007 1008 1009 1010 1011 1012

  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
1013
}