page_view_test.dart 37.2 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
  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) { },
30
              children: kStates.map<Widget>((String state) => Text(state)).toList(),
31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74
            ),
          ),
        ),
      );
    }

    // 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) { },
75
              children: kStates.map<Widget>((String state) => Text(state)).toList(),
76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99
            ),
          ),
        ),
      );
    }

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

100 101 102 103 104 105
  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
106
  testWidgets('PageView control test', (WidgetTester tester) async {
107
    final List<String> log = <String>[];
Adam Barth's avatar
Adam Barth committed
108

109
    await tester.pumpWidget(Directionality(
110
      textDirection: TextDirection.ltr,
111
      child: PageView(
112
        dragStartBehavior: DragStartBehavior.down,
113
        children: kStates.map<Widget>((String state) {
114
          return GestureDetector(
115
            dragStartBehavior: DragStartBehavior.down,
116 117 118
            onTap: () {
              log.add(state);
            },
119
            child: Container(
120 121
              height: 200.0,
              color: const Color(0xFF0000FF),
122
              child: Text(state),
123 124 125 126
            ),
          );
        }).toList(),
      ),
Adam Barth's avatar
Adam Barth committed
127 128 129 130 131 132 133 134
    ));

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

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

135
    await tester.drag(find.byType(PageView), const Offset(-20.0, 0.0));
Adam Barth's avatar
Adam Barth committed
136 137 138 139 140 141
    await tester.pump();

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

142
    await tester.pumpAndSettle();
Adam Barth's avatar
Adam Barth committed
143 144 145 146

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

147
    await tester.drag(find.byType(PageView), const Offset(-401.0, 0.0));
148
    await tester.pumpAndSettle();
Adam Barth's avatar
Adam Barth committed
149 150 151 152 153 154 155 156 157

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

158
    await tester.fling(find.byType(PageView), const Offset(-200.0, 0.0), 1000.0);
159
    await tester.pumpAndSettle();
Adam Barth's avatar
Adam Barth committed
160 161 162 163 164 165

    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);
166
    await tester.pumpAndSettle();
Adam Barth's avatar
Adam Barth committed
167 168 169 170 171

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

173
  testWidgets('PageView does not squish when overscrolled', (WidgetTester tester) async {
174 175 176 177 178
    await tester.pumpWidget(MaterialApp(
      home: PageView(
        children: List<Widget>.generate(10, (int i) {
          return Container(
            key: ValueKey<int>(i),
179
            color: const Color(0xFF0000FF),
180 181 182 183 184
          );
        }),
      ),
    ));

185 186
    Size sizeOf(int i) => tester.getSize(find.byKey(ValueKey<int>(i)));
    double leftOf(int i) => tester.getTopLeft(find.byKey(ValueKey<int>(i))).dx;
187 188 189 190

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

191
    // Going into overscroll.
192
    await tester.drag(find.byType(PageView), const Offset(100.0, 0.0));
193 194
    await tester.pump();

195
    expect(leftOf(0), greaterThan(0.0));
196 197
    expect(sizeOf(0), equals(const Size(800.0, 600.0)));

198
    // Easing overscroll past overscroll limit.
199
    await tester.drag(find.byType(PageView), const Offset(-200.0, 0.0));
200 201
    await tester.pump();

202
    expect(leftOf(0), lessThan(0.0));
203
    expect(sizeOf(0), equals(const Size(800.0, 600.0)));
Dan Field's avatar
Dan Field committed
204
  }, variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.iOS,  TargetPlatform.macOS }));
205 206

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

209
    await tester.pumpWidget(Directionality(
210
      textDirection: TextDirection.ltr,
211 212
      child: Center(
        child: SizedBox(
213 214
          width: 600.0,
          height: 400.0,
215
          child: PageView(
216
            controller: controller,
217
            children: kStates.map<Widget>((String state) => Text(state)).toList(),
218
          ),
219 220 221 222 223 224
        ),
      ),
    ));

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

225
    controller.nextPage(duration: const Duration(milliseconds: 150), curve: Curves.ease);
226
    await tester.pumpAndSettle();
227 228 229

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

230
    await tester.pumpWidget(Directionality(
231
      textDirection: TextDirection.ltr,
232 233
      child: Center(
        child: SizedBox(
234 235
          width: 300.0,
          height: 400.0,
236
          child: PageView(
237
            controller: controller,
238
            children: kStates.map<Widget>((String state) => Text(state)).toList(),
239
          ),
240 241 242 243 244 245
        ),
      ),
    ));

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

246
    controller.previousPage(duration: const Duration(milliseconds: 150), curve: Curves.ease);
247
    await tester.pumpAndSettle();
248 249 250 251 252

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

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

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

268
    await tester.drag(find.byType(PageView), const Offset(-1250.0, 0.0));
269
    await tester.pumpAndSettle();
270 271 272

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

273
    await tester.pumpWidget(Directionality(
274
      textDirection: TextDirection.ltr,
275 276
      child: Center(
        child: SizedBox(
277 278
          width: 250.0,
          height: 100.0,
279 280
          child: PageView(
            children: kStates.map<Widget>((String state) => Text(state)).toList(),
281
          ),
282 283 284 285 286 287
        ),
      ),
    ));

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

288
    await tester.pumpWidget(Directionality(
289
      textDirection: TextDirection.ltr,
290 291
      child: Center(
        child: SizedBox(
292 293
          width: 450.0,
          height: 400.0,
294 295
          child: PageView(
            children: kStates.map<Widget>((String state) => Text(state)).toList(),
296
          ),
297 298 299 300 301 302
        ),
      ),
    ));

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

304
  testWidgets('PageController nextPage and previousPage return Futures that resolve', (WidgetTester tester) async {
305 306
    final PageController controller = PageController();
    await tester.pumpWidget(Directionality(
307
        textDirection: TextDirection.ltr,
308
        child: PageView(
309
          controller: controller,
310
          children: kStates.map<Widget>((String state) => Text(state)).toList(),
311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335
        ),
    ));

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

336
  testWidgets('PageView in zero-size container', (WidgetTester tester) async {
337
    await tester.pumpWidget(Directionality(
338
      textDirection: TextDirection.ltr,
339 340
      child: Center(
        child: SizedBox(
341 342
          width: 0.0,
          height: 0.0,
343 344
          child: PageView(
            children: kStates.map<Widget>((String state) => Text(state)).toList(),
345
          ),
346 347 348 349
        ),
      ),
    ));

350
    expect(find.text('Alabama', skipOffstage: false), findsOneWidget);
351

352
    await tester.pumpWidget(Directionality(
353
      textDirection: TextDirection.ltr,
354 355
      child: Center(
        child: SizedBox(
356 357
          width: 200.0,
          height: 200.0,
358 359
          child: PageView(
            children: kStates.map<Widget>((String state) => Text(state)).toList(),
360
          ),
361 362 363 364 365
        ),
      ),
    ));

    expect(find.text('Alabama'), findsOneWidget);
366
  });
367 368 369

  testWidgets('Page changes at halfway point', (WidgetTester tester) async {
    final List<int> log = <int>[];
370
    await tester.pumpWidget(Directionality(
371
      textDirection: TextDirection.ltr,
372
      child: PageView(
373
        onPageChanged: log.add,
374
        children: kStates.map<Widget>((String state) => Text(state)).toList(),
375
      ),
376 377 378 379
    ));

    expect(log, isEmpty);

380
    final TestGesture gesture =
381
        await tester.startGesture(const Offset(100.0, 100.0));
382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409
    // 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();
410
    await tester.pumpAndSettle();
411 412 413 414 415 416 417

    expect(log, isEmpty);

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

418 419
  testWidgets('Bouncing scroll physics ballistics does not overshoot', (WidgetTester tester) async {
    final List<int> log = <int>[];
420
    final PageController controller = PageController(viewportFraction: 0.9);
421

422
    Widget build(PageController controller, { Size? size }) {
423
      final Widget pageView = Directionality(
424
        textDirection: TextDirection.ltr,
425
        child: PageView(
426 427 428
          controller: controller,
          onPageChanged: log.add,
          physics: const BouncingScrollPhysics(),
429
          children: kStates.map<Widget>((String state) => Text(state)).toList(),
430 431 432 433
        ),
      );

      if (size != null) {
434
        return OverflowBox(
435 436 437 438
          minWidth: size.width,
          minHeight: size.height,
          maxWidth: size.width,
          maxHeight: size.height,
439
          child: pageView,
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
        );
      } 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);
  });

472
  testWidgets('PageView viewportFraction', (WidgetTester tester) async {
473
    PageController controller = PageController(viewportFraction: 7/8);
474 475

    Widget build(PageController controller) {
476
      return Directionality(
477
        textDirection: TextDirection.ltr,
478
        child: PageView.builder(
479 480 481
          controller: controller,
          itemCount: kStates.length,
          itemBuilder: (BuildContext context, int index) {
482
            return Container(
483
              height: 200.0,
484
              color: index.isEven
485 486
                ? const Color(0xFF0000FF)
                : const Color(0xFF00FF00),
487
              child: Text(kStates[index]),
488 489 490
            );
          },
        ),
491 492 493 494 495
      );
    }

    await tester.pumpWidget(build(controller));

496 497
    expect(tester.getTopLeft(find.text('Alabama')), const Offset(50.0, 0.0));
    expect(tester.getTopLeft(find.text('Alaska')), const Offset(750.0, 0.0));
498 499 500 501

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

502 503 504
    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));
505

506
    controller = PageController(viewportFraction: 39/40);
507 508 509

    await tester.pumpWidget(build(controller));

510 511 512
    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));
513 514
  });

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

518
    Widget build({ required bool pageSnapping }) {
519
      return Directionality(
520
        textDirection: TextDirection.ltr,
521
        child: PageView(
522 523 524
          pageSnapping: pageSnapping,
          onPageChanged: log.add,
          children:
525
              kStates.map<Widget>((String state) => Text(state)).toList(),
526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577
        ),
      );
    }

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

578
  testWidgets('PageView small viewportFraction', (WidgetTester tester) async {
579
    final PageController controller = PageController(viewportFraction: 1/8);
580 581

    Widget build(PageController controller) {
582
      return Directionality(
583
        textDirection: TextDirection.ltr,
584
        child: PageView.builder(
585 586 587
          controller: controller,
          itemCount: kStates.length,
          itemBuilder: (BuildContext context, int index) {
588
            return Container(
589
              height: 200.0,
590
              color: index.isEven
591 592
                ? const Color(0xFF0000FF)
                : const Color(0xFF00FF00),
593
              child: Text(kStates[index]),
594 595 596
            );
          },
        ),
597 598 599 600 601
      );
    }

    await tester.pumpWidget(build(controller));

602 603 604 605 606
    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));
607 608 609 610

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

611 612 613 614 615 616 617 618 619
    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));
620 621 622
  });

  testWidgets('PageView large viewportFraction', (WidgetTester tester) async {
623
    final PageController controller = PageController(viewportFraction: 5/4);
624 625

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

    await tester.pumpWidget(build(controller));

646 647
    expect(tester.getTopLeft(find.text('Alabama')), const Offset(-100.0, 0.0));
    expect(tester.getBottomRight(find.text('Alabama')), const Offset(900.0, 600.0));
648 649 650 651

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

652
    expect(tester.getTopLeft(find.text('Hawaii')), const Offset(-100.0, 0.0));
653
  });
654

655 656 657 658 659 660 661 662 663 664 665 666
  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,
667
                color: index.isEven
668 669 670 671 672 673 674 675 676 677 678 679 680 681 682 683 684 685 686 687 688
                  ? 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));
689 690
    },
  );
691

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

  testWidgets(
    'All visible pages are able to receive touch events',
    (WidgetTester tester) async {
      // Regression test for https://github.com/flutter/flutter/issues/23873.
733
      final PageController controller = PageController(viewportFraction: 1/4);
734
      late int tappedIndex;
735 736 737 738 739 740 741 742 743 744 745 746 747 748 749 750 751 752 753 754 755

      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.
756
      for (final int index in visiblePages) {
757 758 759 760 761 762 763 764 765 766 767 768
        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];
769
      for (final int index in visiblePages) {
770 771 772 773
        expect(find.text('$index'), findsOneWidget);
        await tester.tap(find.text('$index'));
        expect(tappedIndex, index);
      }
774 775
    },
  );
776

777 778 779 780 781 782 783 784 785 786 787 788 789 790 791
  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,
792
              children: kStates.map<Widget>((String state) => Text(state)).toList(),
793 794 795 796 797 798 799 800 801 802 803 804 805 806 807 808 809 810 811 812 813 814 815 816 817 818
              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();
  });

819
  testWidgets('PageView does not report page changed on overscroll', (WidgetTester tester) async {
820
    final PageController controller = PageController(
821 822
      initialPage: kStates.length - 1,
    );
823 824
    int changeIndex = 0;
    Widget build() {
825
      return Directionality(
826
        textDirection: TextDirection.ltr,
827
        child: PageView(
828
          controller: controller,
829
          children: kStates.map<Widget>((String state) => Text(state)).toList(),
830 831 832 833
          onPageChanged: (int page) {
            changeIndex = page;
          },
        ),
834 835 836 837 838 839 840 841 842 843
      );
    }

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

845
  testWidgets('PageView can restore page', (WidgetTester tester) async {
846
    final PageController controller = PageController();
847 848
    expect(
      () => controller.page,
849
      throwsA(isAssertionError.having(
850 851 852 853 854
        (AssertionError error) => error.message,
        'message',
        equals('PageController.page cannot be accessed before a PageView is built with it.'),
      )),
    );
855 856
    final PageStorageBucket bucket = PageStorageBucket();
    await tester.pumpWidget(Directionality(
857
      textDirection: TextDirection.ltr,
858
      child: PageStorage(
859
        bucket: bucket,
860
        child: PageView(
861
          key: const PageStorageKey<String>('PageView'),
862
          controller: controller,
863
          children: const <Widget>[
864 865 866
            Placeholder(),
            Placeholder(),
            Placeholder(),
867 868 869
          ],
        ),
      ),
870
    ));
871 872
    expect(controller.page, 0);
    controller.jumpToPage(2);
873
    expect(await tester.pumpAndSettle(const Duration(minutes: 1)), 2);
874 875
    expect(controller.page, 2);
    await tester.pumpWidget(
876
      PageStorage(
877
        bucket: bucket,
878
        child: Container(),
879 880
      ),
    );
881 882
    expect(
      () => controller.page,
883
      throwsA(isAssertionError.having(
884 885 886 887 888
        (AssertionError error) => error.message,
        'message',
        equals('PageController.page cannot be accessed before a PageView is built with it.'),
      )),
    );
889
    await tester.pumpWidget(Directionality(
890
      textDirection: TextDirection.ltr,
891
      child: PageStorage(
892
        bucket: bucket,
893
        child: PageView(
894
          key: const PageStorageKey<String>('PageView'),
895
          controller: controller,
896
          children: const <Widget>[
897 898 899
            Placeholder(),
            Placeholder(),
            Placeholder(),
900 901 902
          ],
        ),
      ),
903
    ));
904
    expect(controller.page, 2);
905

906 907
    final PageController controller2 = PageController(keepPage: false);
    await tester.pumpWidget(Directionality(
908
      textDirection: TextDirection.ltr,
909
      child: PageStorage(
910
        bucket: bucket,
911
        child: PageView(
912
          key: const PageStorageKey<String>('Check it again against your list and see consistency!'),
913
          controller: controller2,
914
          children: const <Widget>[
915 916 917
            Placeholder(),
            Placeholder(),
            Placeholder(),
918 919 920
          ],
        ),
      ),
921
    ));
922
    expect(controller2.page, 0);
923
  });
924 925

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

928 929
    final PageController controller = PageController();
    await tester.pumpWidget(Directionality(
930
      textDirection: TextDirection.ltr,
931
      child: PageView(
932
          controller: controller,
933 934
          children: List<Widget>.generate(3, (int i) {
            return Semantics(
935
              container: true,
936
              child: Text('Page #$i'),
937
            );
938
          }),
939 940 941 942 943 944 945 946 947 948 949 950 951 952 953 954 955 956 957 958 959 960 961 962
        ),
    ));
    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();
  });
963 964

  testWidgets('PageMetrics', (WidgetTester tester) async {
965
    final PageMetrics page = PageMetrics(
966 967 968 969 970 971 972 973 974 975 976 977 978
      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);
  });
979 980 981 982 983 984 985 986 987 988 989

  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,
990
            child: Text('Page #$i'),
991 992 993 994 995 996 997 998
          );
        }),
      ),
    ));
    // Simulate precision error.
    pageController.position.jumpTo(799.99999999999);
    expect(pageController.page, 1);
  });
999 1000 1001 1002 1003 1004 1005 1006 1007

  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,
1008
          allowImplicitScrolling: true,
1009 1010 1011
          children: List<Widget>.generate(4, (int i) {
            return Semantics(
              container: true,
1012
              child: Text('Page #$i'),
1013 1014 1015 1016 1017 1018 1019 1020 1021 1022 1023 1024 1025 1026 1027 1028 1029 1030 1031 1032 1033 1034 1035 1036 1037 1038 1039 1040
            );
          }),
        ),
    ));
    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();
  });
1041 1042 1043 1044 1045 1046 1047 1048 1049 1050 1051 1052 1053 1054 1055 1056 1057 1058 1059 1060 1061 1062 1063 1064 1065 1066

  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,
1067
          children: <Widget>[Container(height: 2000.0)],
1068 1069 1070 1071 1072 1073 1074 1075 1076
        ),
      ),
    );
    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));
  });
1077 1078 1079 1080 1081 1082 1083

  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,
1084
      child: PageView(),
1085 1086 1087 1088 1089 1090 1091 1092 1093 1094 1095 1096 1097 1098
    ));

    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);
  });
1099 1100 1101 1102 1103 1104 1105 1106 1107 1108 1109 1110 1111 1112 1113 1114 1115 1116 1117 1118 1119 1120 1121 1122 1123 1124 1125

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