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

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 350 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
          child: pageView,
          minWidth: size.width,
          minHeight: size.height,
          maxWidth: size.width,
          maxHeight: size.height,
        );
      } 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 395
              height: 200.0,
              color: index % 2 == 0
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 429
  testWidgets('Page snapping disable and reenable', (WidgetTester tester) async {
    final List<int> log = <int>[];

    Widget build({ 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 501
              height: 200.0,
              color: index % 2 == 0
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 545
              height: 200.0,
              color: index % 2 == 0
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 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601
  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,
                color: index % 2 == 0
                  ? 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));
  });

602 603 604 605 606 607 608 609 610 611 612 613 614 615 616 617 618 619 620 621 622 623 624 625 626 627 628 629 630 631 632 633 634 635 636 637 638 639 640 641 642 643 644 645 646 647 648 649 650 651 652 653 654 655 656 657 658 659 660 661 662 663 664
  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,
                color: index % 2 == 0
                  ? 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));
  });

  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);
      int tappedIndex;

      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.
665
      for (final int index in visiblePages) {
666 667 668 669 670 671 672 673 674 675 676 677
        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];
678
      for (final int index in visiblePages) {
679 680 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 703 704 705 706 707 708 709 710 711 712 713 714 715 716 717 718 719 720 721 722 723 724 725 726
  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(
              children: kStates.map<Widget>((String state) => Text(state)).toList(),
              controller: controller,
              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();
  });

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

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

754
  testWidgets('PageView can restore page', (WidgetTester tester) async {
755
    final PageController controller = PageController();
756 757 758 759 760 761 762 763 764
    try {
      controller.page;
      fail('Accessing page before attaching should fail.');
    } on AssertionError catch (e) {
      expect(
        e.message,
        'PageController.page cannot be accessed before a PageView is built with it.',
      );
    }
765 766
    final PageStorageBucket bucket = PageStorageBucket();
    await tester.pumpWidget(Directionality(
767
      textDirection: TextDirection.ltr,
768
      child: PageStorage(
769
        bucket: bucket,
770
        child: PageView(
771
          key: const PageStorageKey<String>('PageView'),
772
          controller: controller,
773
          children: const <Widget>[
774 775 776
            Placeholder(),
            Placeholder(),
            Placeholder(),
777 778 779
          ],
        ),
      ),
780
    ));
781 782 783 784 785
    expect(controller.page, 0);
    controller.jumpToPage(2);
    expect(await tester.pumpAndSettle(const Duration(minutes: 1)), 1);
    expect(controller.page, 2);
    await tester.pumpWidget(
786
      PageStorage(
787
        bucket: bucket,
788
        child: Container(),
789 790
      ),
    );
791 792 793 794 795 796 797 798 799
    try {
      controller.page;
      fail('Accessing page after detaching all PageViews should fail.');
    } on AssertionError catch (e) {
      expect(
        e.message,
        '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 846
          children: List<Widget>.generate(3, (int i) {
            return Semantics(
              child: Text('Page #$i'),
847 848
              container: true,
            );
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 901 902 903 904 905 906 907 908 909

  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(
            child: Text('Page #$i'),
            container: true,
          );
        }),
      ),
    ));
    // Simulate precision error.
    pageController.position.jumpTo(799.99999999999);
    expect(pageController.page, 1);
  });
910 911 912 913 914 915 916 917 918 919 920 921 922 923 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

  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,
          children: List<Widget>.generate(4, (int i) {
            return Semantics(
              child: Text('Page #$i'),
              container: true,
            );
          }),
          allowImplicitScrolling: true,
        ),
    ));
    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();
  });
Adam Barth's avatar
Adam Barth committed
952
}