page_view_test.dart 38.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
// 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' show TestClipPaintingContext;
11
import 'semantics_tester.dart';
Adam Barth's avatar
Adam Barth committed
12 13 14
import 'states.dart';

void main() {
15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50
  // Regression test for https://github.com/flutter/flutter/issues/100451
  testWidgets('PageView.builder respects findChildIndexCallback', (WidgetTester tester) async {
    bool finderCalled = false;
    int itemCount = 7;
    late StateSetter stateSetter;

    await tester.pumpWidget(
      Directionality(
        textDirection: TextDirection.ltr,
        child: StatefulBuilder(
          builder: (BuildContext context, StateSetter setState) {
            stateSetter = setState;
            return PageView.builder(
              itemCount: itemCount,
              itemBuilder: (BuildContext _, int index) => Container(
                key: Key('$index'),
                height: 2000.0,
              ),
              findChildIndexCallback: (Key key) {
                finderCalled = true;
                return null;
              },
            );
          },
        ),
      )
    );
    expect(finderCalled, false);

    // Trigger update.
    stateSetter(() => itemCount = 77);
    await tester.pump();

    expect(finderCalled, true);
  });

51 52 53 54 55 56 57 58 59 60 61 62 63 64 65
  testWidgets('PageView resize from zero-size viewport should not lose state', (WidgetTester tester) async {
    // Regression test for https://github.com/flutter/flutter/issues/88956
    final PageController controller = PageController(
      initialPage: 1,
    );

    Widget build(Size size) {
      return Directionality(
        textDirection: TextDirection.ltr,
        child: Center(
          child: SizedBox.fromSize(
            size: size,
            child: PageView(
              controller: controller,
              onPageChanged: (int page) { },
66
              children: kStates.map<Widget>((String state) => Text(state)).toList(),
67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110
            ),
          ),
        ),
      );
    }

    // The pageView have a zero viewport, so nothing display.
    await tester.pumpWidget(build(Size.zero));
    expect(find.text('Alabama'), findsNothing);
    expect(find.text('Alabama', skipOffstage: false), findsOneWidget);

    // Resize from zero viewport to non-zero, the controller's initialPage 1 will display.
    await tester.pumpWidget(build(const Size(200.0, 200.0)));
    expect(find.text('Alaska'), findsOneWidget);

    // Jump to page 'Iowa'.
    controller.jumpToPage(kStates.indexOf('Iowa'));
    await tester.pump();
    expect(find.text('Iowa'), findsOneWidget);

    // Resize to zero viewport again, nothing display.
    await tester.pumpWidget(build(Size.zero));
    expect(find.text('Iowa'), findsNothing);

    // Resize from zero to non-zero, the pageView should not lose state, so the page 'Iowa' show again.
    await tester.pumpWidget(build(const Size(200.0, 200.0)));
    expect(find.text('Iowa'), findsOneWidget);
  });

  testWidgets('Change the page through the controller when zero-size viewport', (WidgetTester tester) async {
    // Regression test for https://github.com/flutter/flutter/issues/88956
    final PageController controller = PageController(
      initialPage: 1,
    );

    Widget build(Size size) {
      return Directionality(
        textDirection: TextDirection.ltr,
        child: Center(
          child: SizedBox.fromSize(
            size: size,
            child: PageView(
              controller: controller,
              onPageChanged: (int page) { },
111
              children: kStates.map<Widget>((String state) => Text(state)).toList(),
112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135
            ),
          ),
        ),
      );
    }

    // The pageView have a zero viewport, so nothing display.
    await tester.pumpWidget(build(Size.zero));
    expect(find.text('Alabama'), findsNothing);
    expect(find.text('Alabama', skipOffstage: false), findsOneWidget);

    // Change the page through the page controller when zero viewport
    controller.animateToPage(kStates.indexOf('Iowa'), duration: kTabScrollDuration, curve: Curves.ease);
    expect(controller.page, kStates.indexOf('Iowa'));

    controller.jumpToPage(kStates.indexOf('Illinois'));
    expect(controller.page, kStates.indexOf('Illinois'));

    // Resize from zero viewport to non-zero, the latest state should not lost.
    await tester.pumpWidget(build(const Size(200.0, 200.0)));
    expect(controller.page, kStates.indexOf('Illinois'));
    expect(find.text('Illinois'), findsOneWidget);
  });

136 137 138 139 140 141
  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
142
  testWidgets('PageView control test', (WidgetTester tester) async {
143
    final List<String> log = <String>[];
Adam Barth's avatar
Adam Barth committed
144

145
    await tester.pumpWidget(Directionality(
146
      textDirection: TextDirection.ltr,
147
      child: PageView(
148
        dragStartBehavior: DragStartBehavior.down,
149
        children: kStates.map<Widget>((String state) {
150
          return GestureDetector(
151
            dragStartBehavior: DragStartBehavior.down,
152 153 154
            onTap: () {
              log.add(state);
            },
155
            child: Container(
156 157
              height: 200.0,
              color: const Color(0xFF0000FF),
158
              child: Text(state),
159 160 161 162
            ),
          );
        }).toList(),
      ),
Adam Barth's avatar
Adam Barth committed
163 164 165 166 167 168 169 170
    ));

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

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

171
    await tester.drag(find.byType(PageView), const Offset(-20.0, 0.0));
Adam Barth's avatar
Adam Barth committed
172 173 174 175 176 177
    await tester.pump();

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

178
    await tester.pumpAndSettle();
Adam Barth's avatar
Adam Barth committed
179 180 181 182

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

183
    await tester.drag(find.byType(PageView), const Offset(-401.0, 0.0));
184
    await tester.pumpAndSettle();
Adam Barth's avatar
Adam Barth committed
185 186 187 188 189 190 191 192 193

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

194
    await tester.fling(find.byType(PageView), const Offset(-200.0, 0.0), 1000.0);
195
    await tester.pumpAndSettle();
Adam Barth's avatar
Adam Barth committed
196 197 198 199 200 201

    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);
202
    await tester.pumpAndSettle();
Adam Barth's avatar
Adam Barth committed
203 204 205 206 207

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

209
  testWidgets('PageView does not squish when overscrolled', (WidgetTester tester) async {
210 211 212 213 214
    await tester.pumpWidget(MaterialApp(
      home: PageView(
        children: List<Widget>.generate(10, (int i) {
          return Container(
            key: ValueKey<int>(i),
215
            color: const Color(0xFF0000FF),
216 217 218 219 220
          );
        }),
      ),
    ));

221 222
    Size sizeOf(int i) => tester.getSize(find.byKey(ValueKey<int>(i)));
    double leftOf(int i) => tester.getTopLeft(find.byKey(ValueKey<int>(i))).dx;
223 224 225 226

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

227
    // Going into overscroll.
228
    await tester.drag(find.byType(PageView), const Offset(100.0, 0.0));
229 230
    await tester.pump();

231
    expect(leftOf(0), greaterThan(0.0));
232 233
    expect(sizeOf(0), equals(const Size(800.0, 600.0)));

234
    // Easing overscroll past overscroll limit.
235
    await tester.drag(find.byType(PageView), const Offset(-200.0, 0.0));
236 237
    await tester.pump();

238
    expect(leftOf(0), lessThan(0.0));
239
    expect(sizeOf(0), equals(const Size(800.0, 600.0)));
Dan Field's avatar
Dan Field committed
240
  }, variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.iOS,  TargetPlatform.macOS }));
241 242

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

245
    await tester.pumpWidget(Directionality(
246
      textDirection: TextDirection.ltr,
247 248
      child: Center(
        child: SizedBox(
249 250
          width: 600.0,
          height: 400.0,
251
          child: PageView(
252
            controller: controller,
253
            children: kStates.map<Widget>((String state) => Text(state)).toList(),
254
          ),
255 256 257 258 259 260
        ),
      ),
    ));

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

261
    controller.nextPage(duration: const Duration(milliseconds: 150), curve: Curves.ease);
262
    await tester.pumpAndSettle();
263 264 265

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

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

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

282
    controller.previousPage(duration: const Duration(milliseconds: 150), curve: Curves.ease);
283
    await tester.pumpAndSettle();
284 285 286 287 288

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

  testWidgets('PageController page stability', (WidgetTester tester) async {
289
    await tester.pumpWidget(Directionality(
290
      textDirection: TextDirection.ltr,
291 292
      child: Center(
        child: SizedBox(
293 294
          width: 600.0,
          height: 400.0,
295 296
          child: PageView(
            children: kStates.map<Widget>((String state) => Text(state)).toList(),
297
          ),
298
        ),
299 300 301 302 303
      ),
    ));

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

304
    await tester.drag(find.byType(PageView), const Offset(-1250.0, 0.0));
305
    await tester.pumpAndSettle();
306 307 308

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

309
    await tester.pumpWidget(Directionality(
310
      textDirection: TextDirection.ltr,
311 312
      child: Center(
        child: SizedBox(
313 314
          width: 250.0,
          height: 100.0,
315 316
          child: PageView(
            children: kStates.map<Widget>((String state) => Text(state)).toList(),
317
          ),
318 319 320 321 322 323
        ),
      ),
    ));

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

324
    await tester.pumpWidget(Directionality(
325
      textDirection: TextDirection.ltr,
326 327
      child: Center(
        child: SizedBox(
328 329
          width: 450.0,
          height: 400.0,
330 331
          child: PageView(
            children: kStates.map<Widget>((String state) => Text(state)).toList(),
332
          ),
333 334 335 336 337 338
        ),
      ),
    ));

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

340
  testWidgets('PageController nextPage and previousPage return Futures that resolve', (WidgetTester tester) async {
341 342
    final PageController controller = PageController();
    await tester.pumpWidget(Directionality(
343
        textDirection: TextDirection.ltr,
344
        child: PageView(
345
          controller: controller,
346
          children: kStates.map<Widget>((String state) => Text(state)).toList(),
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
        ),
    ));

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

372
  testWidgets('PageView in zero-size container', (WidgetTester tester) async {
373
    await tester.pumpWidget(Directionality(
374
      textDirection: TextDirection.ltr,
375 376
      child: Center(
        child: SizedBox(
377 378
          width: 0.0,
          height: 0.0,
379 380
          child: PageView(
            children: kStates.map<Widget>((String state) => Text(state)).toList(),
381
          ),
382 383 384 385
        ),
      ),
    ));

386
    expect(find.text('Alabama', skipOffstage: false), findsOneWidget);
387

388
    await tester.pumpWidget(Directionality(
389
      textDirection: TextDirection.ltr,
390 391
      child: Center(
        child: SizedBox(
392 393
          width: 200.0,
          height: 200.0,
394 395
          child: PageView(
            children: kStates.map<Widget>((String state) => Text(state)).toList(),
396
          ),
397 398 399 400 401
        ),
      ),
    ));

    expect(find.text('Alabama'), findsOneWidget);
402
  });
403 404 405

  testWidgets('Page changes at halfway point', (WidgetTester tester) async {
    final List<int> log = <int>[];
406
    await tester.pumpWidget(Directionality(
407
      textDirection: TextDirection.ltr,
408
      child: PageView(
409
        onPageChanged: log.add,
410
        children: kStates.map<Widget>((String state) => Text(state)).toList(),
411
      ),
412 413 414 415
    ));

    expect(log, isEmpty);

416
    final TestGesture gesture =
417
        await tester.startGesture(const Offset(100.0, 100.0));
418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445
    // 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();
446
    await tester.pumpAndSettle();
447 448 449 450 451 452 453

    expect(log, isEmpty);

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

454 455
  testWidgets('Bouncing scroll physics ballistics does not overshoot', (WidgetTester tester) async {
    final List<int> log = <int>[];
456
    final PageController controller = PageController(viewportFraction: 0.9);
457

458
    Widget build(PageController controller, { Size? size }) {
459
      final Widget pageView = Directionality(
460
        textDirection: TextDirection.ltr,
461
        child: PageView(
462 463 464
          controller: controller,
          onPageChanged: log.add,
          physics: const BouncingScrollPhysics(),
465
          children: kStates.map<Widget>((String state) => Text(state)).toList(),
466 467 468 469
        ),
      );

      if (size != null) {
470
        return OverflowBox(
471 472 473 474
          minWidth: size.width,
          minHeight: size.height,
          maxWidth: size.width,
          maxHeight: size.height,
475
          child: pageView,
476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507
        );
      } 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);
  });

508
  testWidgets('PageView viewportFraction', (WidgetTester tester) async {
509
    PageController controller = PageController(viewportFraction: 7/8);
510 511

    Widget build(PageController controller) {
512
      return Directionality(
513
        textDirection: TextDirection.ltr,
514
        child: PageView.builder(
515 516 517
          controller: controller,
          itemCount: kStates.length,
          itemBuilder: (BuildContext context, int index) {
518
            return Container(
519
              height: 200.0,
520
              color: index.isEven
521 522
                ? const Color(0xFF0000FF)
                : const Color(0xFF00FF00),
523
              child: Text(kStates[index]),
524 525 526
            );
          },
        ),
527 528 529 530 531
      );
    }

    await tester.pumpWidget(build(controller));

532 533
    expect(tester.getTopLeft(find.text('Alabama')), const Offset(50.0, 0.0));
    expect(tester.getTopLeft(find.text('Alaska')), const Offset(750.0, 0.0));
534 535 536 537

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

538 539 540
    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));
541

542
    controller = PageController(viewportFraction: 39/40);
543 544 545

    await tester.pumpWidget(build(controller));

546 547 548
    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));
549 550
  });

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

554
    Widget build({ required bool pageSnapping }) {
555
      return Directionality(
556
        textDirection: TextDirection.ltr,
557
        child: PageView(
558 559 560
          pageSnapping: pageSnapping,
          onPageChanged: log.add,
          children:
561
              kStates.map<Widget>((String state) => Text(state)).toList(),
562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601 602 603 604 605 606 607 608 609 610 611 612 613
        ),
      );
    }

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

614
  testWidgets('PageView small viewportFraction', (WidgetTester tester) async {
615
    final PageController controller = PageController(viewportFraction: 1/8);
616 617

    Widget build(PageController controller) {
618
      return Directionality(
619
        textDirection: TextDirection.ltr,
620
        child: PageView.builder(
621 622 623
          controller: controller,
          itemCount: kStates.length,
          itemBuilder: (BuildContext context, int index) {
624
            return Container(
625
              height: 200.0,
626
              color: index.isEven
627 628
                ? const Color(0xFF0000FF)
                : const Color(0xFF00FF00),
629
              child: Text(kStates[index]),
630 631 632
            );
          },
        ),
633 634 635 636 637
      );
    }

    await tester.pumpWidget(build(controller));

638 639 640 641 642
    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));
643 644 645 646

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

647 648 649 650 651 652 653 654 655
    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));
656 657 658
  });

  testWidgets('PageView large viewportFraction', (WidgetTester tester) async {
659
    final PageController controller = PageController(viewportFraction: 5/4);
660 661

    Widget build(PageController controller) {
662
      return Directionality(
663
        textDirection: TextDirection.ltr,
664
        child: PageView.builder(
665 666 667
          controller: controller,
          itemCount: kStates.length,
          itemBuilder: (BuildContext context, int index) {
668
            return Container(
669
              height: 200.0,
670
              color: index.isEven
671 672
                ? const Color(0xFF0000FF)
                : const Color(0xFF00FF00),
673
              child: Text(kStates[index]),
674 675 676
            );
          },
        ),
677 678 679 680 681
      );
    }

    await tester.pumpWidget(build(controller));

682 683
    expect(tester.getTopLeft(find.text('Alabama')), const Offset(-100.0, 0.0));
    expect(tester.getBottomRight(find.text('Alabama')), const Offset(900.0, 600.0));
684 685 686 687

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

688
    expect(tester.getTopLeft(find.text('Hawaii')), const Offset(-100.0, 0.0));
689
  });
690

691 692 693 694 695 696 697 698 699 700 701 702
  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,
703
                color: index.isEven
704 705 706 707 708 709 710 711 712 713 714 715 716 717 718 719 720 721 722 723 724
                  ? 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));
725 726
    },
  );
727

728 729 730 731 732 733 734 735 736 737 738 739 740 741 742
  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,
743
                color: index.isEven
744 745 746 747 748 749 750 751 752 753 754 755 756 757 758 759 760 761
                  ? 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));
762 763
    },
  );
764 765 766 767 768

  testWidgets(
    'All visible pages are able to receive touch events',
    (WidgetTester tester) async {
      // Regression test for https://github.com/flutter/flutter/issues/23873.
769
      final PageController controller = PageController(viewportFraction: 1/4);
770
      late int tappedIndex;
771 772 773 774 775 776 777 778 779 780 781 782 783 784 785 786 787 788 789 790 791

      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.
792
      for (final int index in visiblePages) {
793 794 795 796 797 798 799 800 801 802 803 804
        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];
805
      for (final int index in visiblePages) {
806 807 808 809
        expect(find.text('$index'), findsOneWidget);
        await tester.tap(find.text('$index'));
        expect(tappedIndex, index);
      }
810 811
    },
  );
812

813 814 815 816 817 818 819 820 821 822 823 824 825 826 827
  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,
828
              children: kStates.map<Widget>((String state) => Text(state)).toList(),
829 830 831 832 833 834 835 836 837 838 839 840 841 842 843 844 845 846 847 848 849 850 851 852 853 854
              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();
  });

855
  testWidgets('PageView does not report page changed on overscroll', (WidgetTester tester) async {
856
    final PageController controller = PageController(
857 858
      initialPage: kStates.length - 1,
    );
859 860
    int changeIndex = 0;
    Widget build() {
861
      return Directionality(
862
        textDirection: TextDirection.ltr,
863
        child: PageView(
864
          controller: controller,
865
          children: kStates.map<Widget>((String state) => Text(state)).toList(),
866 867 868 869
          onPageChanged: (int page) {
            changeIndex = page;
          },
        ),
870 871 872 873 874 875 876 877 878 879
      );
    }

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

881
  testWidgets('PageView can restore page', (WidgetTester tester) async {
882
    final PageController controller = PageController();
883 884
    expect(
      () => controller.page,
885
      throwsA(isAssertionError.having(
886 887 888 889 890
        (AssertionError error) => error.message,
        'message',
        equals('PageController.page cannot be accessed before a PageView is built with it.'),
      )),
    );
891 892
    final PageStorageBucket bucket = PageStorageBucket();
    await tester.pumpWidget(Directionality(
893
      textDirection: TextDirection.ltr,
894
      child: PageStorage(
895
        bucket: bucket,
896
        child: PageView(
897
          key: const PageStorageKey<String>('PageView'),
898
          controller: controller,
899
          children: const <Widget>[
900 901 902
            Placeholder(),
            Placeholder(),
            Placeholder(),
903 904 905
          ],
        ),
      ),
906
    ));
907 908
    expect(controller.page, 0);
    controller.jumpToPage(2);
909
    expect(await tester.pumpAndSettle(const Duration(minutes: 1)), 2);
910 911
    expect(controller.page, 2);
    await tester.pumpWidget(
912
      PageStorage(
913
        bucket: bucket,
914
        child: Container(),
915 916
      ),
    );
917 918
    expect(
      () => controller.page,
919
      throwsA(isAssertionError.having(
920 921 922 923 924
        (AssertionError error) => error.message,
        'message',
        equals('PageController.page cannot be accessed before a PageView is built with it.'),
      )),
    );
925
    await tester.pumpWidget(Directionality(
926
      textDirection: TextDirection.ltr,
927
      child: PageStorage(
928
        bucket: bucket,
929
        child: PageView(
930
          key: const PageStorageKey<String>('PageView'),
931
          controller: controller,
932
          children: const <Widget>[
933 934 935
            Placeholder(),
            Placeholder(),
            Placeholder(),
936 937 938
          ],
        ),
      ),
939
    ));
940
    expect(controller.page, 2);
941

942 943
    final PageController controller2 = PageController(keepPage: false);
    await tester.pumpWidget(Directionality(
944
      textDirection: TextDirection.ltr,
945
      child: PageStorage(
946
        bucket: bucket,
947
        child: PageView(
948
          key: const PageStorageKey<String>('Check it again against your list and see consistency!'),
949
          controller: controller2,
950
          children: const <Widget>[
951 952 953
            Placeholder(),
            Placeholder(),
            Placeholder(),
954 955 956
          ],
        ),
      ),
957
    ));
958
    expect(controller2.page, 0);
959
  });
960 961

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

964 965
    final PageController controller = PageController();
    await tester.pumpWidget(Directionality(
966
      textDirection: TextDirection.ltr,
967
      child: PageView(
968
          controller: controller,
969 970
          children: List<Widget>.generate(3, (int i) {
            return Semantics(
971
              container: true,
972
              child: Text('Page #$i'),
973
            );
974
          }),
975 976 977 978 979 980 981 982 983 984 985 986 987 988 989 990 991 992 993 994 995 996 997 998
        ),
    ));
    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();
  });
999 1000

  testWidgets('PageMetrics', (WidgetTester tester) async {
1001
    final PageMetrics page = PageMetrics(
1002 1003 1004 1005 1006 1007 1008 1009 1010 1011 1012 1013 1014
      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);
  });
1015 1016 1017 1018 1019 1020 1021 1022 1023 1024 1025

  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,
1026
            child: Text('Page #$i'),
1027 1028 1029 1030 1031 1032 1033 1034
          );
        }),
      ),
    ));
    // Simulate precision error.
    pageController.position.jumpTo(799.99999999999);
    expect(pageController.page, 1);
  });
1035 1036 1037 1038 1039 1040 1041 1042 1043

  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,
1044
          allowImplicitScrolling: true,
1045 1046 1047
          children: List<Widget>.generate(4, (int i) {
            return Semantics(
              container: true,
1048
              child: Text('Page #$i'),
1049 1050 1051 1052 1053 1054 1055 1056 1057 1058 1059 1060 1061 1062 1063 1064 1065 1066 1067 1068 1069 1070 1071 1072 1073 1074 1075 1076
            );
          }),
        ),
    ));
    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();
  });
1077 1078 1079 1080 1081 1082 1083 1084 1085 1086 1087 1088 1089 1090 1091 1092 1093 1094 1095 1096 1097 1098 1099 1100 1101 1102

  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,
1103
          children: <Widget>[Container(height: 2000.0)],
1104 1105 1106 1107 1108 1109 1110 1111 1112
        ),
      ),
    );
    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));
  });
1113 1114 1115 1116 1117 1118 1119

  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,
1120
      child: PageView(),
1121 1122 1123 1124 1125 1126 1127 1128 1129 1130 1131 1132 1133 1134
    ));

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

    // PageView(padEnds: false) is propagated properly.
    await tester.pumpWidget(Directionality(
      textDirection: TextDirection.ltr,
      child: PageView(
        padEnds: false,
      ),
    ));

    expect(tester.widget<SliverFillViewport>(viewportFinder()).padEnds, false);
  });
1135 1136 1137 1138 1139 1140 1141 1142 1143 1144 1145 1146 1147 1148 1149 1150 1151 1152 1153 1154 1155 1156 1157 1158 1159 1160 1161

  testWidgets('PageView - precision error inside RenderSliverFixedExtentBoxAdaptor', (WidgetTester tester) async {
    // Regression test for https://github.com/flutter/flutter/issues/95101

    final PageController controller = PageController(initialPage: 152);
    await tester.pumpWidget(
      Center(
        child: SizedBox(
          width: 392.72727272727275,
          child: Directionality(
            textDirection: TextDirection.ltr,
            child: PageView.builder(
              controller: controller,
              itemCount: 366,
              itemBuilder: (BuildContext context, int index) {
                return const SizedBox();
              },
            ),
          ),
        ),
      ),
    );

    controller.jumpToPage(365);
    await tester.pump();
    expect(tester.takeException(), isNull);
  });
Adam Barth's avatar
Adam Barth committed
1162
}