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: |
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 | |