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 | |
5 | import 'package:flutter/foundation.dart'; |
6 | import 'package:flutter/gestures.dart' show DragStartBehavior; |
7 | import 'package:flutter/material.dart'; |
8 | import 'package:flutter/rendering.dart'; |
9 | import 'package:flutter_test/flutter_test.dart'; |
10 | |
11 | import '../rendering/rendering_tester.dart' show TestClipPaintingContext; |
12 | import 'semantics_tester.dart'; |
13 | import 'states.dart'; |
14 | |
15 | void 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 | |