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

Provided by KDAB

Privacy Policy
Learn more about Flutter for embedded and desktop on industrialflutter.com