1// Copyright 2014 The Flutter Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
4
5import 'package:flutter/foundation.dart';
6import 'package:flutter/gestures.dart' show DragStartBehavior;
7import 'package:flutter/material.dart';
8import 'package:flutter/rendering.dart';
9import 'package:flutter_test/flutter_test.dart';
10
11import '../rendering/rendering_tester.dart' show TestClipPaintingContext;
12import 'semantics_tester.dart';
13import 'states.dart';
14
15void main() {
16 // Regression test for https://github.com/flutter/flutter/issues/100451
17 testWidgets('PageView.builder respects findChildIndexCallback', (WidgetTester tester) async {
18 bool finderCalled = false;
19 int itemCount = 7;
20 late StateSetter stateSetter;
21
22 await tester.pumpWidget(
23 Directionality(
24 textDirection: TextDirection.ltr,
25 child: StatefulBuilder(
26 builder: (BuildContext context, StateSetter setState) {
27 stateSetter = setState;
28 return PageView.builder(
29 itemCount: itemCount,
30 itemBuilder: (BuildContext _, int index) =>
31 Container(key: Key('$index'), height: 2000.0),
32 findChildIndexCallback: (Key key) {
33 finderCalled = true;
34 return null;
35 },
36 );
37 },
38 ),
39 ),
40 );
41 expect(finderCalled, false);
42
43 // Trigger update.
44 stateSetter(() => itemCount = 77);
45 await tester.pump();
46
47 expect(finderCalled, true);
48 });
49
50 testWidgets('PageView resize from zero-size viewport should not lose state', (
51 WidgetTester tester,
52 ) async {
53 // Regression test for https://github.com/flutter/flutter/issues/88956
54 final PageController controller = PageController(initialPage: 1);
55 addTearDown(controller.dispose);
56
57 Widget build(Size size) {
58 return Directionality(
59 textDirection: TextDirection.ltr,
60 child: Center(
61 child: SizedBox.fromSize(
62 size: size,
63 child: PageView(
64 controller: controller,
65 onPageChanged: (int page) {},
66 children: kStates.map<Widget>((String state) => Text(state)).toList(),
67 ),
68 ),
69 ),
70 );
71 }
72
73 // The pageView have a zero viewport, so nothing display.
74 await tester.pumpWidget(build(Size.zero));
75 expect(find.text('Alabama'), findsNothing);
76 expect(find.text('Alabama', skipOffstage: false), findsOneWidget);
77
78 // Resize from zero viewport to non-zero, the controller's initialPage 1 will display.
79 await tester.pumpWidget(build(const Size(200.0, 200.0)));
80 expect(find.text('Alaska'), findsOneWidget);
81
82 // Jump to page 'Iowa'.
83 controller.jumpToPage(kStates.indexOf('Iowa'));
84 await tester.pump();
85 expect(find.text('Iowa'), findsOneWidget);
86
87 // Resize to zero viewport again, nothing display.
88 await tester.pumpWidget(build(Size.zero));
89 expect(find.text('Iowa'), findsNothing);
90
91 // Resize from zero to non-zero, the pageView should not lose state, so the page 'Iowa' show again.
92 await tester.pumpWidget(build(const Size(200.0, 200.0)));
93 expect(find.text('Iowa'), findsOneWidget);
94 });
95
96 testWidgets('Change the page through the controller when zero-size viewport', (
97 WidgetTester tester,
98 ) async {
99 // Regression test for https://github.com/flutter/flutter/issues/88956
100 final PageController controller = PageController(initialPage: 1);
101 addTearDown(controller.dispose);
102
103 Widget build(Size size) {
104 return Directionality(
105 textDirection: TextDirection.ltr,
106 child: Center(
107 child: SizedBox.fromSize(
108 size: size,
109 child: PageView(
110 controller: controller,
111 onPageChanged: (int page) {},
112 children: kStates.map<Widget>((String state) => Text(state)).toList(),
113 ),
114 ),
115 ),
116 );
117 }
118
119 // The pageView have a zero viewport, so nothing display.
120 await tester.pumpWidget(build(Size.zero));
121 expect(find.text('Alabama'), findsNothing);
122 expect(find.text('Alabama', skipOffstage: false), findsOneWidget);
123
124 // Change the page through the page controller when zero viewport
125 controller.animateToPage(
126 kStates.indexOf('Iowa'),
127 duration: kTabScrollDuration,
128 curve: Curves.ease,
129 );
130 expect(controller.page, kStates.indexOf('Iowa'));
131
132 controller.jumpToPage(kStates.indexOf('Illinois'));
133 expect(controller.page, kStates.indexOf('Illinois'));
134
135 // Resize from zero viewport to non-zero, the latest state should not lost.
136 await tester.pumpWidget(build(const Size(200.0, 200.0)));
137 expect(controller.page, kStates.indexOf('Illinois'));
138 expect(find.text('Illinois'), findsOneWidget);
139 });
140
141 testWidgets('_PagePosition.applyViewportDimension should not throw', (WidgetTester tester) async {
142 // Regression test for https://github.com/flutter/flutter/issues/101007
143 final PageController controller = PageController(initialPage: 1);
144 addTearDown(controller.dispose);
145
146 // Set the starting viewportDimension to 0.0
147 await tester.binding.setSurfaceSize(Size.zero);
148 final MediaQueryData mediaQueryData = MediaQueryData.fromView(tester.view);
149
150 Widget build(Size size) {
151 return MediaQuery(
152 data: mediaQueryData.copyWith(size: size),
153 child: Directionality(
154 textDirection: TextDirection.ltr,
155 child: Center(
156 child: SizedBox.expand(
157 child: PageView(
158 controller: controller,
159 onPageChanged: (int page) {},
160 children: kStates.map<Widget>((String state) => Text(state)).toList(),
161 ),
162 ),
163 ),
164 ),
165 );
166 }
167
168 await tester.pumpWidget(build(Size.zero));
169 const Size surfaceSize = Size(500, 400);
170 await tester.binding.setSurfaceSize(surfaceSize);
171 await tester.pumpWidget(build(surfaceSize));
172
173 expect(tester.takeException(), isNull);
174
175 // Reset TestWidgetsFlutterBinding surfaceSize
176 await tester.binding.setSurfaceSize(null);
177 });
178
179 testWidgets('PageController cannot return page while unattached', (WidgetTester tester) async {
180 final PageController controller = PageController();
181 addTearDown(controller.dispose);
182 expect(() => controller.page, throwsAssertionError);
183 });
184
185 testWidgets('PageView control test', (WidgetTester tester) async {
186 final List<String> log = <String>[];
187
188 await tester.pumpWidget(
189 Directionality(
190 textDirection: TextDirection.ltr,
191 child: PageView(
192 dragStartBehavior: DragStartBehavior.down,
193 children: kStates.map<Widget>((String state) {
194 return GestureDetector(
195 dragStartBehavior: DragStartBehavior.down,
196 onTap: () {
197 log.add(state);
198 },
199 child: Container(height: 200.0, color: const Color(0xFF0000FF), child: Text(state)),
200 );
201 }).toList(),
202 ),
203 ),
204 );
205
206 await tester.tap(find.text('Alabama'));
207 expect(log, equals(<String>['Alabama']));
208 log.clear();
209
210 expect(find.text('Alaska'), findsNothing);
211
212 await tester.drag(find.byType(PageView), const Offset(-20.0, 0.0));
213 await tester.pump();
214
215 expect(find.text('Alabama'), findsOneWidget);
216 expect(find.text('Alaska'), findsOneWidget);
217 expect(find.text('Arizona'), findsNothing);
218
219 await tester.pumpAndSettle();
220
221 expect(find.text('Alabama'), findsOneWidget);
222 expect(find.text('Alaska'), findsNothing);
223
224 await tester.drag(find.byType(PageView), const Offset(-401.0, 0.0));
225 await tester.pumpAndSettle();
226
227 expect(find.text('Alabama'), findsNothing);
228 expect(find.text('Alaska'), findsOneWidget);
229 expect(find.text('Arizona'), findsNothing);
230
231 await tester.tap(find.text('Alaska'));
232 expect(log, equals(<String>['Alaska']));
233 log.clear();
234
235 await tester.fling(find.byType(PageView), const Offset(-200.0, 0.0), 1000.0);
236 await tester.pumpAndSettle();
237
238 expect(find.text('Alabama'), findsNothing);
239 expect(find.text('Alaska'), findsNothing);
240 expect(find.text('Arizona'), findsOneWidget);
241
242 await tester.fling(find.byType(PageView), const Offset(200.0, 0.0), 1000.0);
243 await tester.pumpAndSettle();
244
245 expect(find.text('Alabama'), findsNothing);
246 expect(find.text('Alaska'), findsOneWidget);
247 expect(find.text('Arizona'), findsNothing);
248 });
249
250 testWidgets(
251 'PageView does not squish when overscrolled',
252 (WidgetTester tester) async {
253 await tester.pumpWidget(
254 MaterialApp(
255 home: PageView(
256 children: List<Widget>.generate(10, (int i) {
257 return Container(key: ValueKey<int>(i), color: const Color(0xFF0000FF));
258 }),
259 ),
260 ),
261 );
262
263 Size sizeOf(int i) => tester.getSize(find.byKey(ValueKey<int>(i)));
264 double leftOf(int i) => tester.getTopLeft(find.byKey(ValueKey<int>(i))).dx;
265
266 expect(leftOf(0), equals(0.0));
267 expect(sizeOf(0), equals(const Size(800.0, 600.0)));
268
269 // Going into overscroll.
270 await tester.drag(find.byType(PageView), const Offset(100.0, 0.0));
271 await tester.pump();
272
273 expect(leftOf(0), greaterThan(0.0));
274 expect(sizeOf(0), equals(const Size(800.0, 600.0)));
275
276 // Easing overscroll past overscroll limit.
277 if (debugDefaultTargetPlatformOverride == TargetPlatform.macOS) {
278 await tester.drag(find.byType(PageView), const Offset(-500.0, 0.0));
279 } else {
280 await tester.drag(find.byType(PageView), const Offset(-200.0, 0.0));
281 }
282 await tester.pump();
283
284 expect(leftOf(0), lessThan(0.0));
285 expect(sizeOf(0), equals(const Size(800.0, 600.0)));
286 },
287 variant: const TargetPlatformVariant(<TargetPlatform>{
288 TargetPlatform.iOS,
289 TargetPlatform.macOS,
290 }),
291 );
292
293 testWidgets('PageController control test', (WidgetTester tester) async {
294 final PageController controller = PageController(initialPage: 4);
295 addTearDown(controller.dispose);
296
297 await tester.pumpWidget(
298 Directionality(
299 textDirection: TextDirection.ltr,
300 child: Center(
301 child: SizedBox(
302 width: 600.0,
303 height: 400.0,
304 child: PageView(
305 controller: controller,
306 children: kStates.map<Widget>((String state) => Text(state)).toList(),
307 ),
308 ),
309 ),
310 ),
311 );
312
313 expect(find.text('California'), findsOneWidget);
314
315 controller.nextPage(duration: const Duration(milliseconds: 150), curve: Curves.ease);
316 await tester.pumpAndSettle();
317
318 expect(find.text('Colorado'), findsOneWidget);
319
320 await tester.pumpWidget(
321 Directionality(
322 textDirection: TextDirection.ltr,
323 child: Center(
324 child: SizedBox(
325 width: 300.0,
326 height: 400.0,
327 child: PageView(
328 controller: controller,
329 children: kStates.map<Widget>((String state) => Text(state)).toList(),
330 ),
331 ),
332 ),
333 ),
334 );
335
336 expect(find.text('Colorado'), findsOneWidget);
337
338 controller.previousPage(duration: const Duration(milliseconds: 150), curve: Curves.ease);
339 await tester.pumpAndSettle();
340
341 expect(find.text('California'), findsOneWidget);
342 });
343
344 testWidgets('PageController page stability', (WidgetTester tester) async {
345 await tester.pumpWidget(
346 Directionality(
347 textDirection: TextDirection.ltr,
348 child: Center(
349 child: SizedBox(
350 width: 600.0,
351 height: 400.0,
352 child: PageView(children: kStates.map<Widget>((String state) => Text(state)).toList()),
353 ),
354 ),
355 ),
356 );
357
358 expect(find.text('Alabama'), findsOneWidget);
359
360 await tester.drag(find.byType(PageView), const Offset(-1250.0, 0.0));
361 await tester.pumpAndSettle();
362
363 expect(find.text('Arizona'), findsOneWidget);
364
365 await tester.pumpWidget(
366 Directionality(
367 textDirection: TextDirection.ltr,
368 child: Center(
369 child: SizedBox(
370 width: 250.0,
371 height: 100.0,
372 child: PageView(children: kStates.map<Widget>((String state) => Text(state)).toList()),
373 ),
374 ),
375 ),
376 );
377
378 expect(find.text('Arizona'), findsOneWidget);
379
380 await tester.pumpWidget(
381 Directionality(
382 textDirection: TextDirection.ltr,
383 child: Center(
384 child: SizedBox(
385 width: 450.0,
386 height: 400.0,
387 child: PageView(children: kStates.map<Widget>((String state) => Text(state)).toList()),
388 ),
389 ),
390 ),
391 );
392
393 expect(find.text('Arizona'), findsOneWidget);
394 });
395
396 testWidgets('PageController nextPage and previousPage return Futures that resolve', (
397 WidgetTester tester,
398 ) async {
399 final PageController controller = PageController();
400 addTearDown(controller.dispose);
401
402 await tester.pumpWidget(
403 Directionality(
404 textDirection: TextDirection.ltr,
405 child: PageView(
406 controller: controller,
407 children: kStates.map<Widget>((String state) => Text(state)).toList(),
408 ),
409 ),
410 );
411
412 bool nextPageCompleted = false;
413 controller
414 .nextPage(duration: const Duration(milliseconds: 150), curve: Curves.ease)
415 .then((_) => nextPageCompleted = true);
416
417 expect(nextPageCompleted, false);
418 await tester.pump(const Duration(milliseconds: 200));
419 expect(nextPageCompleted, false);
420 await tester.pump(const Duration(milliseconds: 200));
421 expect(nextPageCompleted, true);
422
423 bool previousPageCompleted = false;
424 controller
425 .previousPage(duration: const Duration(milliseconds: 150), curve: Curves.ease)
426 .then((_) => previousPageCompleted = true);
427
428 expect(previousPageCompleted, false);
429 await tester.pump(const Duration(milliseconds: 200));
430 expect(previousPageCompleted, false);
431 await tester.pump(const Duration(milliseconds: 200));
432 expect(previousPageCompleted, true);
433 });
434
435 testWidgets('PageView in zero-size container', (WidgetTester tester) async {
436 await tester.pumpWidget(
437 Directionality(
438 textDirection: TextDirection.ltr,
439 child: Center(
440 child: SizedBox.shrink(
441 child: PageView(children: kStates.map<Widget>((String state) => Text(state)).toList()),
442 ),
443 ),
444 ),
445 );
446
447 expect(find.text('Alabama', skipOffstage: false), findsOneWidget);
448
449 await tester.pumpWidget(
450 Directionality(
451 textDirection: TextDirection.ltr,
452 child: Center(
453 child: SizedBox(
454 width: 200.0,
455 height: 200.0,
456 child: PageView(children: kStates.map<Widget>((String state) => Text(state)).toList()),
457 ),
458 ),
459 ),
460 );
461
462 expect(find.text('Alabama'), findsOneWidget);
463 });
464
465 testWidgets('Page changes at halfway point', (WidgetTester tester) async {
466 final List<int> log = <int>[];
467 await tester.pumpWidget(
468 Directionality(
469 textDirection: TextDirection.ltr,
470 child: PageView(
471 onPageChanged: log.add,
472 children: kStates.map<Widget>((String state) => Text(state)).toList(),
473 ),
474 ),
475 );
476
477 expect(log, isEmpty);
478
479 final TestGesture gesture = await tester.startGesture(const Offset(100.0, 100.0));
480 // The page view is 800.0 wide, so this move is just short of halfway.
481 await gesture.moveBy(const Offset(-380.0, 0.0));
482
483 expect(log, isEmpty);
484
485 // We've crossed the halfway mark.
486 await gesture.moveBy(const Offset(-40.0, 0.0));
487
488 expect(log, equals(const <int>[1]));
489 log.clear();
490
491 // Moving a bit more should not generate redundant notifications.
492 await gesture.moveBy(const Offset(-40.0, 0.0));
493
494 expect(log, isEmpty);
495
496 await gesture.moveBy(const Offset(-40.0, 0.0));
497 await tester.pump();
498
499 await gesture.moveBy(const Offset(-40.0, 0.0));
500 await tester.pump();
501
502 await gesture.moveBy(const Offset(-40.0, 0.0));
503 await tester.pump();
504
505 expect(log, isEmpty);
506
507 await gesture.up();
508 await tester.pumpAndSettle();
509
510 expect(log, isEmpty);
511
512 expect(find.text('Alabama'), findsNothing);
513 expect(find.text('Alaska'), findsOneWidget);
514 });
515
516 testWidgets('Bouncing scroll physics ballistics does not overshoot', (WidgetTester tester) async {
517 final List<int> log = <int>[];
518 final PageController controller = PageController(viewportFraction: 0.9);
519 addTearDown(controller.dispose);
520
521 Widget build(PageController controller, {Size? size}) {
522 final Widget pageView = Directionality(
523 textDirection: TextDirection.ltr,
524 child: PageView(
525 controller: controller,
526 onPageChanged: log.add,
527 physics: const BouncingScrollPhysics(),
528 children: kStates.map<Widget>((String state) => Text(state)).toList(),
529 ),
530 );
531
532 if (size != null) {
533 return OverflowBox(
534 minWidth: size.width,
535 minHeight: size.height,
536 maxWidth: size.width,
537 maxHeight: size.height,
538 child: pageView,
539 );
540 } else {
541 return pageView;
542 }
543 }
544
545 await tester.pumpWidget(build(controller));
546 expect(log, isEmpty);
547
548 // Fling right to move to a non-existent page at the beginning of the
549 // PageView, and confirm that the PageView settles back on the first page.
550 await tester.fling(find.byType(PageView), const Offset(100.0, 0.0), 800.0);
551 await tester.pumpAndSettle();
552 expect(log, isEmpty);
553
554 expect(find.text('Alabama'), findsOneWidget);
555 expect(find.text('Alaska'), findsOneWidget);
556 expect(find.text('Arizona'), findsNothing);
557
558 // Try again with a Cupertino "Plus" device size.
559 await tester.pumpWidget(build(controller, size: const Size(414.0, 736.0)));
560 expect(log, isEmpty);
561
562 await tester.fling(find.byType(PageView), const Offset(100.0, 0.0), 800.0);
563 await tester.pumpAndSettle();
564 expect(log, isEmpty);
565
566 expect(find.text('Alabama'), findsOneWidget);
567 expect(find.text('Alaska'), findsOneWidget);
568 expect(find.text('Arizona'), findsNothing);
569 });
570
571 testWidgets('PageView viewportFraction', (WidgetTester tester) async {
572 PageController controller = PageController(viewportFraction: 7 / 8);
573 addTearDown(controller.dispose);
574
575 Widget build(PageController controller) {
576 return Directionality(
577 textDirection: TextDirection.ltr,
578 child: PageView.builder(
579 controller: controller,
580 itemCount: kStates.length,
581 itemBuilder: (BuildContext context, int index) {
582 return Container(
583 height: 200.0,
584 color: index.isEven ? const Color(0xFF0000FF) : const Color(0xFF00FF00),
585 child: Text(kStates[index]),
586 );
587 },
588 ),
589 );
590 }
591
592 await tester.pumpWidget(build(controller));
593
594 expect(tester.getTopLeft(find.text('Alabama')), const Offset(50.0, 0.0));
595 expect(tester.getTopLeft(find.text('Alaska')), const Offset(750.0, 0.0));
596
597 controller.jumpToPage(10);
598 await tester.pump();
599
600 expect(tester.getTopLeft(find.text('Georgia')), const Offset(-650.0, 0.0));
601 expect(tester.getTopLeft(find.text('Hawaii')), const Offset(50.0, 0.0));
602 expect(tester.getTopLeft(find.text('Idaho')), const Offset(750.0, 0.0));
603
604 controller = PageController(viewportFraction: 39 / 40);
605 addTearDown(controller.dispose);
606
607 await tester.pumpWidget(build(controller));
608
609 expect(tester.getTopLeft(find.text('Georgia')), const Offset(-770.0, 0.0));
610 expect(tester.getTopLeft(find.text('Hawaii')), const Offset(10.0, 0.0));
611 expect(tester.getTopLeft(find.text('Idaho')), const Offset(790.0, 0.0));
612 });
613
614 testWidgets('Page snapping disable and reenable', (WidgetTester tester) async {
615 final List<int> log = <int>[];
616
617 Widget build({required bool pageSnapping}) {
618 return Directionality(
619 textDirection: TextDirection.ltr,
620 child: PageView(
621 pageSnapping: pageSnapping,
622 onPageChanged: log.add,
623 children: kStates.map<Widget>((String state) => Text(state)).toList(),
624 ),
625 );
626 }
627
628 await tester.pumpWidget(build(pageSnapping: true));
629 expect(log, isEmpty);
630
631 // Drag more than halfway to the next page, to confirm the default behavior.
632 TestGesture gesture = await tester.startGesture(const Offset(100.0, 100.0));
633 // The page view is 800.0 wide, so this move is just beyond halfway.
634 await gesture.moveBy(const Offset(-420.0, 0.0));
635
636 expect(log, equals(const <int>[1]));
637 log.clear();
638
639 // Release the gesture, confirm that the page settles on the next.
640 await gesture.up();
641 await tester.pumpAndSettle();
642
643 expect(find.text('Alabama'), findsNothing);
644 expect(find.text('Alaska'), findsOneWidget);
645
646 // Disable page snapping, and try moving halfway. Confirm it doesn't snap.
647 await tester.pumpWidget(build(pageSnapping: false));
648 gesture = await tester.startGesture(const Offset(100.0, 100.0));
649 // Move just beyond halfway, again.
650 await gesture.moveBy(const Offset(-420.0, 0.0));
651
652 // Page notifications still get sent.
653 expect(log, equals(const <int>[2]));
654 log.clear();
655
656 // Release the gesture, confirm that both pages are visible.
657 await gesture.up();
658 await tester.pumpAndSettle();
659
660 expect(find.text('Alabama'), findsNothing);
661 expect(find.text('Alaska'), findsOneWidget);
662 expect(find.text('Arizona'), findsOneWidget);
663 expect(find.text('Arkansas'), findsNothing);
664
665 // Now re-enable snapping, confirm that we've settled on a page.
666 await tester.pumpWidget(build(pageSnapping: true));
667 await tester.pumpAndSettle();
668
669 expect(log, isEmpty);
670
671 expect(find.text('Alaska'), findsNothing);
672 expect(find.text('Arizona'), findsOneWidget);
673 expect(find.text('Arkansas'), findsNothing);
674 });
675
676 testWidgets('PageView small viewportFraction', (WidgetTester tester) async {
677 final PageController controller = PageController(viewportFraction: 1 / 8);
678 addTearDown(controller.dispose);
679
680 Widget build(PageController controller) {
681 return Directionality(
682 textDirection: TextDirection.ltr,
683 child: PageView.builder(
684 controller: controller,
685 itemCount: kStates.length,
686 itemBuilder: (BuildContext context, int index) {
687 return Container(
688 height: 200.0,
689 color: index.isEven ? const Color(0xFF0000FF) : const Color(0xFF00FF00),
690 child: Text(kStates[index]),
691 );
692 },
693 ),
694 );
695 }
696
697 await tester.pumpWidget(build(controller));
698
699 expect(tester.getTopLeft(find.text('Alabama')), const Offset(350.0, 0.0));
700 expect(tester.getTopLeft(find.text('Alaska')), const Offset(450.0, 0.0));
701 expect(tester.getTopLeft(find.text('Arizona')), const Offset(550.0, 0.0));
702 expect(tester.getTopLeft(find.text('Arkansas')), const Offset(650.0, 0.0));
703 expect(tester.getTopLeft(find.text('California')), const Offset(750.0, 0.0));
704
705 controller.jumpToPage(10);
706 await tester.pump();
707
708 expect(tester.getTopLeft(find.text('Connecticut')), const Offset(-50.0, 0.0));
709 expect(tester.getTopLeft(find.text('Delaware')), const Offset(50.0, 0.0));
710 expect(tester.getTopLeft(find.text('Florida')), const Offset(150.0, 0.0));
711 expect(tester.getTopLeft(find.text('Georgia')), const Offset(250.0, 0.0));
712 expect(tester.getTopLeft(find.text('Hawaii')), const Offset(350.0, 0.0));
713 expect(tester.getTopLeft(find.text('Idaho')), const Offset(450.0, 0.0));
714 expect(tester.getTopLeft(find.text('Illinois')), const Offset(550.0, 0.0));
715 expect(tester.getTopLeft(find.text('Indiana')), const Offset(650.0, 0.0));
716 expect(tester.getTopLeft(find.text('Iowa')), const Offset(750.0, 0.0));
717 });
718
719 testWidgets('PageView large viewportFraction', (WidgetTester tester) async {
720 final PageController controller = PageController(viewportFraction: 5 / 4);
721 addTearDown(controller.dispose);
722
723 Widget build(PageController controller) {
724 return Directionality(
725 textDirection: TextDirection.ltr,
726 child: PageView.builder(
727 controller: controller,
728 itemCount: kStates.length,
729 itemBuilder: (BuildContext context, int index) {
730 return Container(
731 height: 200.0,
732 color: index.isEven ? const Color(0xFF0000FF) : const Color(0xFF00FF00),
733 child: Text(kStates[index]),
734 );
735 },
736 ),
737 );
738 }
739
740 await tester.pumpWidget(build(controller));
741
742 expect(tester.getTopLeft(find.text('Alabama')), const Offset(-100.0, 0.0));
743 expect(tester.getBottomRight(find.text('Alabama')), const Offset(900.0, 600.0));
744
745 controller.jumpToPage(10);
746 await tester.pump();
747
748 expect(tester.getTopLeft(find.text('Hawaii')), const Offset(-100.0, 0.0));
749 });
750
751 testWidgets('Updating PageView large viewportFraction', (WidgetTester tester) async {
752 Widget build(PageController controller) {
753 return Directionality(
754 textDirection: TextDirection.ltr,
755 child: PageView.builder(
756 controller: controller,
757 itemCount: kStates.length,
758 itemBuilder: (BuildContext context, int index) {
759 return Container(
760 height: 200.0,
761 color: index.isEven ? const Color(0xFF0000FF) : const Color(0xFF00FF00),
762 child: Text(kStates[index]),
763 );
764 },
765 ),
766 );
767 }
768
769 final PageController oldController = PageController(viewportFraction: 5 / 4);
770 addTearDown(oldController.dispose);
771 await tester.pumpWidget(build(oldController));
772
773 expect(tester.getTopLeft(find.text('Alabama')), const Offset(-100, 0));
774 expect(tester.getBottomRight(find.text('Alabama')), const Offset(900.0, 600.0));
775
776 final PageController newController = PageController(viewportFraction: 4);
777 addTearDown(newController.dispose);
778 await tester.pumpWidget(build(newController));
779 newController.jumpToPage(10);
780 await tester.pump();
781
782 expect(tester.getTopLeft(find.text('Hawaii')), const Offset(-(4 - 1) * 800 / 2, 0));
783 });
784
785 testWidgets('PageView large viewportFraction can scroll to the last page and snap', (
786 WidgetTester tester,
787 ) async {
788 // Regression test for https://github.com/flutter/flutter/issues/45096.
789 final PageController controller = PageController(viewportFraction: 5 / 4);
790 addTearDown(controller.dispose);
791
792 Widget build(PageController controller) {
793 return Directionality(
794 textDirection: TextDirection.ltr,
795 child: PageView.builder(
796 controller: controller,
797 itemCount: 3,
798 itemBuilder: (BuildContext context, int index) {
799 return Container(
800 height: 200.0,
801 color: index.isEven ? const Color(0xFF0000FF) : const Color(0xFF00FF00),
802 child: Text(index.toString()),
803 );
804 },
805 ),
806 );
807 }
808
809 await tester.pumpWidget(build(controller));
810
811 expect(tester.getCenter(find.text('0')), const Offset(400, 300));
812
813 controller.jumpToPage(2);
814 await tester.pump();
815 await tester.pumpAndSettle();
816
817 expect(tester.getCenter(find.text('2')), const Offset(400, 300));
818 });
819
820 testWidgets('All visible pages are able to receive touch events', (WidgetTester tester) async {
821 // Regression test for https://github.com/flutter/flutter/issues/23873.
822 final PageController controller = PageController(viewportFraction: 1 / 4);
823 addTearDown(controller.dispose);
824 late int tappedIndex;
825
826 Widget build() {
827 return Directionality(
828 textDirection: TextDirection.ltr,
829 child: PageView.builder(
830 controller: controller,
831 itemCount: 20,
832 itemBuilder: (BuildContext context, int index) {
833 return GestureDetector(
834 onTap: () => tappedIndex = index,
835 child: SizedBox.expand(child: Text('$index')),
836 );
837 },
838 ),
839 );
840 }
841
842 Iterable<int> visiblePages = const <int>[0, 1, 2];
843 await tester.pumpWidget(build());
844
845 // The first 3 items should be visible and tappable.
846 for (final int index in visiblePages) {
847 expect(find.text(index.toString()), findsOneWidget);
848 // The center of page 2's x-coordinate is 800, so we have to manually
849 // offset it a bit to make sure the tap lands within the screen.
850 final Offset center = tester.getCenter(find.text('$index')) - const Offset(3, 0);
851 await tester.tapAt(center);
852 expect(tappedIndex, index);
853 }
854
855 controller.jumpToPage(19);
856 await tester.pump();
857 // The last 3 items should be visible and tappable.
858 visiblePages = const <int>[17, 18, 19];
859 for (final int index in visiblePages) {
860 expect(find.text('$index'), findsOneWidget);
861 await tester.tap(find.text('$index'));
862 expect(tappedIndex, index);
863 }
864 });
865
866 testWidgets('the current item remains centered on constraint change', (
867 WidgetTester tester,
868 ) async {
869 // Regression test for https://github.com/flutter/flutter/issues/50505.
870 final PageController controller = PageController(
871 initialPage: kStates.length - 1,
872 viewportFraction: 0.5,
873 );
874 addTearDown(controller.dispose);
875
876 Widget build(Size size) {
877 return Directionality(
878 textDirection: TextDirection.ltr,
879 child: Center(
880 child: SizedBox.fromSize(
881 size: size,
882 child: PageView(
883 controller: controller,
884 children: kStates.map<Widget>((String state) => Text(state)).toList(),
885 onPageChanged: (int page) {},
886 ),
887 ),
888 ),
889 );
890 }
891
892 // Verifies that the last item is centered on screen.
893 void verifyCentered() {
894 expect(
895 tester.getCenter(find.text(kStates.last)),
896 offsetMoreOrLessEquals(const Offset(400, 300)),
897 );
898 }
899
900 await tester.pumpWidget(build(const Size(300, 300)));
901 await tester.pumpAndSettle();
902
903 verifyCentered();
904
905 await tester.pumpWidget(build(const Size(200, 300)));
906 await tester.pumpAndSettle();
907
908 verifyCentered();
909 });
910
911 testWidgets('PageView does not report page changed on overscroll', (WidgetTester tester) async {
912 final PageController controller = PageController(initialPage: kStates.length - 1);
913 addTearDown(controller.dispose);
914 int changeIndex = 0;
915 Widget build() {
916 return Directionality(
917 textDirection: TextDirection.ltr,
918 child: PageView(
919 controller: controller,
920 children: kStates.map<Widget>((String state) => Text(state)).toList(),
921 onPageChanged: (int page) {
922 changeIndex = page;
923 },
924 ),
925 );
926 }
927
928 await tester.pumpWidget(build());
929 controller.jumpToPage(kStates.length * 2); // try to move beyond max range
930 // change index should be zero, shouldn't fire onPageChanged
931 expect(changeIndex, 0);
932 await tester.pump();
933 expect(changeIndex, 0);
934 });
935
936 testWidgets('PageView can restore page', (WidgetTester tester) async {
937 final PageController controller = PageController();
938 addTearDown(controller.dispose);
939 expect(
940 () => controller.page,
941 throwsA(
942 isAssertionError.having(
943 (AssertionError error) => error.message,
944 'message',
945 equals('PageController.page cannot be accessed before a PageView is built with it.'),
946 ),
947 ),
948 );
949 final PageStorageBucket bucket = PageStorageBucket();
950 await tester.pumpWidget(
951 Directionality(
952 textDirection: TextDirection.ltr,
953 child: PageStorage(
954 bucket: bucket,
955 child: PageView(
956 key: const PageStorageKey<String>('PageView'),
957 controller: controller,
958 children: const <Widget>[Placeholder(), Placeholder(), Placeholder()],
959 ),
960 ),
961 ),
962 );
963 expect(controller.page, 0);
964 controller.jumpToPage(2);
965 expect(await tester.pumpAndSettle(const Duration(minutes: 1)), 2);
966 expect(controller.page, 2);
967 await tester.pumpWidget(PageStorage(bucket: bucket, child: Container()));
968 expect(
969 () => controller.page,
970 throwsA(
971 isAssertionError.having(
972 (AssertionError error) => error.message,
973 'message',
974 equals('PageController.page cannot be accessed before a PageView is built with it.'),
975 ),
976 ),
977 );
978 await tester.pumpWidget(
979 Directionality(
980 textDirection: TextDirection.ltr,
981 child: PageStorage(
982 bucket: bucket,
983 child: PageView(
984 key: const PageStorageKey<String>('PageView'),
985 controller: controller,
986 children: const <Widget>[Placeholder(), Placeholder(), Placeholder()],
987 ),
988 ),
989 ),
990 );
991 expect(controller.page, 2);
992
993 final PageController controller2 = PageController(keepPage: false);
994 addTearDown(controller2.dispose);
995 await tester.pumpWidget(
996 Directionality(
997 textDirection: TextDirection.ltr,
998 child: PageStorage(
999 bucket: bucket,
1000 child: PageView(
1001 key: const PageStorageKey<String>(
1002 'Check it again against your list and see consistency!',
1003 ),
1004 controller: controller2,
1005 children: const <Widget>[Placeholder(), Placeholder(), Placeholder()],
1006 ),
1007 ),
1008 ),
1009 );
1010 expect(controller2.page, 0);
1011 });
1012
1013 testWidgets('PageView exposes semantics of children', (WidgetTester tester) async {
1014 final SemanticsTester semantics = SemanticsTester(tester);
1015
1016 final PageController controller = PageController();
1017 addTearDown(controller.dispose);
1018 await tester.pumpWidget(
1019 Directionality(
1020 textDirection: TextDirection.ltr,
1021 child: PageView(
1022 controller: controller,
1023 children: List<Widget>.generate(3, (int i) {
1024 return Semantics(container: true, child: Text('Page #$i'));
1025 }),
1026 ),
1027 ),
1028 );
1029 expect(controller.page, 0);
1030
1031 expect(semantics, includesNodeWith(label: 'Page #0'));
1032 expect(semantics, isNot(includesNodeWith(label: 'Page #1')));
1033 expect(semantics, isNot(includesNodeWith(label: 'Page #2')));
1034
1035 controller.jumpToPage(1);
1036 await tester.pumpAndSettle();
1037
1038 expect(semantics, isNot(includesNodeWith(label: 'Page #0')));
1039 expect(semantics, includesNodeWith(label: 'Page #1'));
1040 expect(semantics, isNot(includesNodeWith(label: 'Page #2')));
1041
1042 controller.jumpToPage(2);
1043 await tester.pumpAndSettle();
1044
1045 expect(semantics, isNot(includesNodeWith(label: 'Page #0')));
1046 expect(semantics, isNot(includesNodeWith(label: 'Page #1')));
1047 expect(semantics, includesNodeWith(label: 'Page #2'));
1048
1049 semantics.dispose();
1050 });
1051
1052 testWidgets('PageMetrics', (WidgetTester tester) async {
1053 final PageMetrics page = PageMetrics(
1054 minScrollExtent: 100.0,
1055 maxScrollExtent: 200.0,
1056 pixels: 150.0,
1057 viewportDimension: 25.0,
1058 axisDirection: AxisDirection.right,
1059 viewportFraction: 1.0,
1060 devicePixelRatio: tester.view.devicePixelRatio,
1061 );
1062 expect(page.page, 6);
1063 final PageMetrics page2 = page.copyWith(pixels: page.pixels - 100.0);
1064 expect(page2.page, 4.0);
1065 });
1066
1067 testWidgets('Page controller can handle rounding issue', (WidgetTester tester) async {
1068 final PageController pageController = PageController();
1069 addTearDown(pageController.dispose);
1070
1071 await tester.pumpWidget(
1072 Directionality(
1073 textDirection: TextDirection.ltr,
1074 child: PageView(
1075 controller: pageController,
1076 children: List<Widget>.generate(3, (int i) {
1077 return Semantics(container: true, child: Text('Page #$i'));
1078 }),
1079 ),
1080 ),
1081 );
1082 // Simulate precision error.
1083 pageController.position.jumpTo(799.99999999999);
1084 expect(pageController.page, 1);
1085 });
1086
1087 testWidgets('PageView can participate in a11y scrolling', (WidgetTester tester) async {
1088 final SemanticsTester semantics = SemanticsTester(tester);
1089
1090 final PageController controller = PageController();
1091 addTearDown(controller.dispose);
1092 await tester.pumpWidget(
1093 Directionality(
1094 textDirection: TextDirection.ltr,
1095 child: PageView(
1096 controller: controller,
1097 allowImplicitScrolling: true,
1098 children: List<Widget>.generate(4, (int i) {
1099 return Semantics(container: true, child: Text('Page #$i'));
1100 }),
1101 ),
1102 ),
1103 );
1104 expect(controller.page, 0);
1105
1106 expect(semantics, includesNodeWith(flags: <SemanticsFlag>[SemanticsFlag.hasImplicitScrolling]));
1107 expect(semantics, includesNodeWith(label: 'Page #0'));
1108 expect(
1109 semantics,
1110 includesNodeWith(label: 'Page #1', flags: <SemanticsFlag>[SemanticsFlag.isHidden]),
1111 );
1112 expect(
1113 semantics,
1114 isNot(includesNodeWith(label: 'Page #2', flags: <SemanticsFlag>[SemanticsFlag.isHidden])),
1115 );
1116 expect(
1117 semantics,
1118 isNot(includesNodeWith(label: 'Page #3', flags: <SemanticsFlag>[SemanticsFlag.isHidden])),
1119 );
1120
1121 controller.nextPage(duration: const Duration(milliseconds: 150), curve: Curves.ease);
1122 await tester.pumpAndSettle();
1123 expect(
1124 semantics,
1125 includesNodeWith(label: 'Page #0', flags: <SemanticsFlag>[SemanticsFlag.isHidden]),
1126 );
1127 expect(semantics, includesNodeWith(label: 'Page #1'));
1128 expect(
1129 semantics,
1130 includesNodeWith(label: 'Page #2', flags: <SemanticsFlag>[SemanticsFlag.isHidden]),
1131 );
1132 expect(
1133 semantics,
1134 isNot(includesNodeWith(label: 'Page #3', flags: <SemanticsFlag>[SemanticsFlag.isHidden])),
1135 );
1136
1137 controller.nextPage(duration: const Duration(milliseconds: 150), curve: Curves.ease);
1138 await tester.pumpAndSettle();
1139 expect(
1140 semantics,
1141 isNot(includesNodeWith(label: 'Page #0', flags: <SemanticsFlag>[SemanticsFlag.isHidden])),
1142 );
1143 expect(
1144 semantics,
1145 includesNodeWith(label: 'Page #1', flags: <SemanticsFlag>[SemanticsFlag.isHidden]),
1146 );
1147 expect(semantics, includesNodeWith(label: 'Page #2'));
1148 expect(
1149 semantics,
1150 includesNodeWith(label: 'Page #3', flags: <SemanticsFlag>[SemanticsFlag.isHidden]),
1151 );
1152
1153 semantics.dispose();
1154 });
1155
1156 testWidgets('PageView respects clipBehavior', (WidgetTester tester) async {
1157 await tester.pumpWidget(
1158 Directionality(
1159 textDirection: TextDirection.ltr,
1160 child: PageView(children: <Widget>[Container(height: 2000.0)]),
1161 ),
1162 );
1163
1164 // 1st, check that the render object has received the default clip behavior.
1165 final RenderViewport renderObject = tester.allRenderObjects.whereType<RenderViewport>().first;
1166 expect(renderObject.clipBehavior, equals(Clip.hardEdge));
1167
1168 // 2nd, check that the painting context has received the default clip behavior.
1169 final TestClipPaintingContext context = TestClipPaintingContext();
1170 renderObject.paint(context, Offset.zero);
1171 expect(context.clipBehavior, equals(Clip.hardEdge));
1172
1173 // 3rd, pump a new widget to check that the render object can update its clip behavior.
1174 await tester.pumpWidget(
1175 Directionality(
1176 textDirection: TextDirection.ltr,
1177 child: PageView(
1178 clipBehavior: Clip.antiAlias,
1179 children: <Widget>[Container(height: 2000.0)],
1180 ),
1181 ),
1182 );
1183 expect(renderObject.clipBehavior, equals(Clip.antiAlias));
1184
1185 // 4th, check that a non-default clip behavior can be sent to the painting context.
1186 renderObject.paint(context, Offset.zero);
1187 expect(context.clipBehavior, equals(Clip.antiAlias));
1188 });
1189
1190 testWidgets('PageView.padEnds tests', (WidgetTester tester) async {
1191 Finder viewportFinder() => find.byType(SliverFillViewport, skipOffstage: false);
1192
1193 // PageView() defaults to true.
1194 await tester.pumpWidget(Directionality(textDirection: TextDirection.ltr, child: PageView()));
1195
1196 expect(tester.widget<SliverFillViewport>(viewportFinder()).padEnds, true);
1197
1198 // PageView(padEnds: false) is propagated properly.
1199 await tester.pumpWidget(
1200 Directionality(textDirection: TextDirection.ltr, child: PageView(padEnds: false)),
1201 );
1202
1203 expect(tester.widget<SliverFillViewport>(viewportFinder()).padEnds, false);
1204 });
1205
1206 testWidgets('PageView - precision error inside RenderSliverFixedExtentBoxAdaptor', (
1207 WidgetTester tester,
1208 ) async {
1209 // Regression test for https://github.com/flutter/flutter/issues/95101
1210 final PageController controller = PageController(initialPage: 152);
1211 addTearDown(controller.dispose);
1212
1213 await tester.pumpWidget(
1214 Center(
1215 child: SizedBox(
1216 width: 392.72727272727275,
1217 child: Directionality(
1218 textDirection: TextDirection.ltr,
1219 child: PageView.builder(
1220 controller: controller,
1221 itemCount: 366,
1222 itemBuilder: (BuildContext context, int index) {
1223 return const SizedBox();
1224 },
1225 ),
1226 ),
1227 ),
1228 ),
1229 );
1230
1231 controller.jumpToPage(365);
1232 await tester.pump();
1233 expect(tester.takeException(), isNull);
1234 });
1235
1236 testWidgets('PageView content should not be stretched on precision error', (
1237 WidgetTester tester,
1238 ) async {
1239 // Regression test for https://github.com/flutter/flutter/issues/126561.
1240 final PageController controller = PageController();
1241 addTearDown(controller.dispose);
1242
1243 const double pixel6EmulatorWidth = 411.42857142857144;
1244
1245 await tester.pumpWidget(
1246 MaterialApp(
1247 home: Center(
1248 child: SizedBox(
1249 width: pixel6EmulatorWidth,
1250 child: PageView(
1251 controller: controller,
1252 physics: const PageScrollPhysics().applyTo(const ClampingScrollPhysics()),
1253 children: const <Widget>[
1254 Center(child: Text('First Page')),
1255 Center(child: Text('Second Page')),
1256 Center(child: Text('Third Page')),
1257 ],
1258 ),
1259 ),
1260 ),
1261 ),
1262 );
1263
1264 controller.animateToPage(2, duration: const Duration(milliseconds: 300), curve: Curves.ease);
1265 await tester.pumpAndSettle();
1266
1267 final Finder transformFinder = find.descendant(
1268 of: find.byType(PageView),
1269 matching: find.byType(Transform),
1270 );
1271 expect(transformFinder, findsOneWidget);
1272
1273 // Get the Transform widget that stretches the PageView.
1274 final Transform transform = tester.firstWidget<Transform>(
1275 find.descendant(of: find.byType(PageView), matching: find.byType(Transform)),
1276 );
1277
1278 // Check the stretch factor in the first element of the transform matrix.
1279 expect(transform.transform.storage.first, 1.0);
1280 });
1281
1282 testWidgets('PageController onAttach, onDetach', (WidgetTester tester) async {
1283 int attach = 0;
1284 int detach = 0;
1285 final PageController controller = PageController(
1286 onAttach: (_) {
1287 attach++;
1288 },
1289 onDetach: (_) {
1290 detach++;
1291 },
1292 );
1293 addTearDown(controller.dispose);
1294
1295 await tester.pumpWidget(
1296 MaterialApp(
1297 home: Center(
1298 child: PageView(
1299 controller: controller,
1300 physics: const PageScrollPhysics().applyTo(const ClampingScrollPhysics()),
1301 children: const <Widget>[
1302 Center(child: Text('First Page')),
1303 Center(child: Text('Second Page')),
1304 Center(child: Text('Third Page')),
1305 ],
1306 ),
1307 ),
1308 ),
1309 );
1310 await tester.pumpAndSettle();
1311
1312 expect(attach, 1);
1313 expect(detach, 0);
1314
1315 await tester.pumpWidget(Container());
1316 await tester.pumpAndSettle();
1317
1318 expect(attach, 1);
1319 expect(detach, 1);
1320 });
1321
1322 group('$PageView handles change of controller', () {
1323 final GlobalKey key = GlobalKey();
1324
1325 Widget createPageView(PageController? controller) {
1326 return MaterialApp(
1327 home: Scaffold(
1328 body: PageView(
1329 key: key,
1330 controller: controller,
1331 children: const <Widget>[
1332 Center(child: Text('0')),
1333 Center(child: Text('1')),
1334 Center(child: Text('2')),
1335 ],
1336 ),
1337 ),
1338 );
1339 }
1340
1341 Future<void> testPageViewWithController(
1342 PageController controller,
1343 WidgetTester tester,
1344 bool controls,
1345 ) async {
1346 int currentVisiblePage() {
1347 return int.parse(tester.widgetList(find.byType(Text)).whereType<Text>().first.data!);
1348 }
1349
1350 final int initialPageInView = currentVisiblePage();
1351
1352 for (int i = 0; i < 3; i++) {
1353 if (controls) {
1354 controller.jumpToPage(i);
1355 await tester.pumpAndSettle();
1356 expect(currentVisiblePage(), i);
1357 } else {
1358 expect(() => controller.jumpToPage(i), throwsAssertionError);
1359 expect(currentVisiblePage(), initialPageInView);
1360 }
1361 }
1362 }
1363
1364 testWidgets('null to value', (WidgetTester tester) async {
1365 final PageController controller = PageController();
1366 addTearDown(controller.dispose);
1367 await tester.pumpWidget(createPageView(null));
1368 await tester.pumpWidget(createPageView(controller));
1369 await testPageViewWithController(controller, tester, true);
1370 });
1371
1372 testWidgets('value to value', (WidgetTester tester) async {
1373 final PageController controller1 = PageController();
1374 addTearDown(controller1.dispose);
1375 final PageController controller2 = PageController();
1376 addTearDown(controller2.dispose);
1377 await tester.pumpWidget(createPageView(controller1));
1378 await testPageViewWithController(controller1, tester, true);
1379 await tester.pumpWidget(createPageView(controller2));
1380 await testPageViewWithController(controller1, tester, false);
1381 await testPageViewWithController(controller2, tester, true);
1382 });
1383
1384 testWidgets('value to null', (WidgetTester tester) async {
1385 final PageController controller = PageController();
1386 addTearDown(controller.dispose);
1387 await tester.pumpWidget(createPageView(controller));
1388 await testPageViewWithController(controller, tester, true);
1389 await tester.pumpWidget(createPageView(null));
1390 await testPageViewWithController(controller, tester, false);
1391 });
1392
1393 testWidgets('null to null', (WidgetTester tester) async {
1394 await tester.pumpWidget(createPageView(null));
1395 await tester.pumpWidget(createPageView(null));
1396 });
1397 });
1398
1399 group('Asserts in jumpToPage and animateToPage methods works properly', () {
1400 Widget createPageView([PageController? controller]) {
1401 return MaterialApp(
1402 home: Scaffold(
1403 body: PageView(
1404 controller: controller,
1405 children: <Widget>[
1406 Container(color: Colors.red),
1407 Container(color: Colors.green),
1408 Container(color: Colors.blue),
1409 ],
1410 ),
1411 ),
1412 );
1413 }
1414
1415 group('One pageController is attached to multiple PageViews', () {
1416 Widget createMultiplePageViews(PageController controller) {
1417 return MaterialApp(
1418 home: Scaffold(
1419 body: Column(
1420 children: <Widget>[
1421 Expanded(
1422 child: PageView(
1423 controller: controller,
1424 children: <Widget>[
1425 Container(color: Colors.red),
1426 Container(color: Colors.green),
1427 Container(color: Colors.blue),
1428 ],
1429 ),
1430 ),
1431 Expanded(
1432 child: PageView(
1433 controller: controller,
1434 children: <Widget>[
1435 Container(color: Colors.orange),
1436 Container(color: Colors.purple),
1437 Container(color: Colors.yellow),
1438 ],
1439 ),
1440 ),
1441 ],
1442 ),
1443 ),
1444 );
1445 }
1446
1447 testWidgets(
1448 'animateToPage assertion is working properly when pageController is attached to multiple PageViews',
1449 (WidgetTester tester) async {
1450 final PageController controller = PageController();
1451 addTearDown(controller.dispose);
1452 await tester.pumpWidget(createMultiplePageViews(controller));
1453
1454 expect(
1455 () => controller.animateToPage(
1456 2,
1457 duration: const Duration(milliseconds: 300),
1458 curve: Curves.ease,
1459 ),
1460 throwsA(
1461 isAssertionError.having(
1462 (AssertionError error) => error.message,
1463 'message',
1464 equals(
1465 'Multiple PageViews are attached to '
1466 'the same PageController.',
1467 ),
1468 ),
1469 ),
1470 );
1471 },
1472 );
1473
1474 testWidgets(
1475 'jumpToPage assertion is working properly when pageController is attached to multiple PageViews',
1476 (WidgetTester tester) async {
1477 final PageController controller = PageController();
1478 addTearDown(controller.dispose);
1479 await tester.pumpWidget(createMultiplePageViews(controller));
1480
1481 expect(
1482 () => controller.jumpToPage(2),
1483 throwsA(
1484 isAssertionError.having(
1485 (AssertionError error) => error.message,
1486 'message',
1487 equals(
1488 'Multiple PageViews are attached to '
1489 'the same PageController.',
1490 ),
1491 ),
1492 ),
1493 );
1494 },
1495 );
1496 });
1497
1498 group('PageController is attached or is not attached to PageView', () {
1499 testWidgets('Assert behavior of animateToPage works properly', (WidgetTester tester) async {
1500 final PageController controller = PageController();
1501 addTearDown(controller.dispose);
1502
1503 // pageController is not attached to PageView
1504 await tester.pumpWidget(createPageView());
1505 expect(
1506 () => controller.animateToPage(
1507 2,
1508 duration: const Duration(milliseconds: 300),
1509 curve: Curves.ease,
1510 ),
1511 throwsA(
1512 isAssertionError.having(
1513 (AssertionError error) => error.message,
1514 'message',
1515 equals('PageController is not attached to a PageView.'),
1516 ),
1517 ),
1518 );
1519
1520 // pageController is attached to PageView
1521 await tester.pumpWidget(createPageView(controller));
1522 expect(
1523 () => controller.animateToPage(
1524 2,
1525 duration: const Duration(milliseconds: 300),
1526 curve: Curves.ease,
1527 ),
1528 returnsNormally,
1529 );
1530 });
1531
1532 testWidgets('Assert behavior of jumpToPage works properly', (WidgetTester tester) async {
1533 final PageController controller = PageController();
1534 addTearDown(controller.dispose);
1535
1536 // pageController is not attached to PageView
1537 await tester.pumpWidget(createPageView());
1538 expect(
1539 () => controller.jumpToPage(2),
1540 throwsA(
1541 isAssertionError.having(
1542 (AssertionError error) => error.message,
1543 'message',
1544 equals('PageController is not attached to a PageView.'),
1545 ),
1546 ),
1547 );
1548
1549 // pageController is attached to PageView
1550 await tester.pumpWidget(createPageView(controller));
1551 expect(() => controller.jumpToPage(2), returnsNormally);
1552 });
1553 });
1554 });
1555
1556 testWidgets(
1557 'Get the page value before the content dimension is determined,do not throw an assertion and return null',
1558 (WidgetTester tester) async {
1559 // Regression test for https://github.com/flutter/flutter/issues/146986.
1560 final PageController controller = PageController();
1561 late String currentPage;
1562 addTearDown(controller.dispose);
1563 await tester.pumpWidget(
1564 MaterialApp(
1565 home: Material(
1566 child: StatefulBuilder(
1567 builder: (BuildContext context, StateSetter setState) {
1568 return Scaffold(
1569 body: PageView(
1570 controller: controller,
1571 children: <Widget>[
1572 Builder(
1573 builder: (BuildContext context) {
1574 currentPage = controller.page == null ? 'null' : 'not empty';
1575 return Center(child: Text(currentPage));
1576 },
1577 ),
1578 ],
1579 ),
1580 floatingActionButton: FloatingActionButton(
1581 onPressed: () {
1582 setState(() {});
1583 },
1584 ),
1585 );
1586 },
1587 ),
1588 ),
1589 ),
1590 );
1591 expect(find.text('null'), findsOneWidget);
1592 expect(find.text('not empty'), findsNothing);
1593 expect(currentPage, 'null');
1594
1595 await tester.tap(find.byType(FloatingActionButton));
1596 await tester.pump();
1597 currentPage = controller.page == null ? 'null' : 'not empty';
1598 expect(find.text('not empty'), findsOneWidget);
1599 expect(find.text('null'), findsNothing);
1600 expect(currentPage, 'not empty');
1601 },
1602 );
1603
1604 testWidgets('Does not crash when calling jumpToPage before layout', (WidgetTester tester) async {
1605 // Regression test for https://github.com/flutter/flutter/issues/86222.
1606 final PageController controller = PageController();
1607 addTearDown(controller.dispose);
1608
1609 await tester.pumpWidget(
1610 MaterialApp(
1611 home: Scaffold(
1612 body: Navigator(
1613 onDidRemovePage: (Page<Object?> page) {},
1614 pages: <Page<void>>[
1615 MaterialPage<void>(
1616 child: Scaffold(
1617 body: PageView(
1618 controller: controller,
1619 children: const <Widget>[
1620 Scaffold(body: Text('One')),
1621 Scaffold(body: Text('Two')),
1622 ],
1623 ),
1624 ),
1625 ),
1626 const MaterialPage<void>(child: Scaffold()),
1627 ],
1628 ),
1629 ),
1630 ),
1631 );
1632
1633 controller.jumpToPage(1);
1634 expect(tester.takeException(), null);
1635 });
1636
1637 testWidgets('Does not crash when calling animateToPage before layout', (
1638 WidgetTester tester,
1639 ) async {
1640 // Regression test for https://github.com/flutter/flutter/issues/86222.
1641 final PageController controller = PageController();
1642 addTearDown(controller.dispose);
1643
1644 await tester.pumpWidget(
1645 MaterialApp(
1646 home: Scaffold(
1647 body: Navigator(
1648 onDidRemovePage: (Page<Object?> page) {},
1649 pages: <Page<void>>[
1650 MaterialPage<void>(
1651 child: Scaffold(
1652 body: PageView(
1653 controller: controller,
1654 children: const <Widget>[
1655 Scaffold(body: Text('One')),
1656 Scaffold(body: Text('Two')),
1657 ],
1658 ),
1659 ),
1660 ),
1661 const MaterialPage<void>(child: Scaffold()),
1662 ],
1663 ),
1664 ),
1665 ),
1666 );
1667
1668 controller.animateToPage(1, duration: const Duration(milliseconds: 50), curve: Curves.bounceIn);
1669 expect(tester.takeException(), null);
1670 });
1671}
1672