1// Copyright 2014 The Flutter Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
4
5import 'package:flutter/cupertino.dart';
6import 'package:flutter/foundation.dart';
7import 'package:flutter/material.dart';
8import 'package:flutter/rendering.dart';
9import 'package:flutter/services.dart';
10import 'package:flutter_test/flutter_test.dart';
11
12void main() {
13 final TestWidgetsFlutterBinding binding = TestWidgetsFlutterBinding.ensureInitialized();
14
15 testWidgets('Default PageTransitionsTheme platform', (WidgetTester tester) async {
16 await tester.pumpWidget(const MaterialApp(home: Text('home')));
17 final PageTransitionsTheme theme = Theme.of(
18 tester.element(find.text('home')),
19 ).pageTransitionsTheme;
20 expect(theme.builders, isNotNull);
21 for (final TargetPlatform platform in TargetPlatform.values) {
22 switch (platform) {
23 case TargetPlatform.android:
24 case TargetPlatform.iOS:
25 case TargetPlatform.macOS:
26 case TargetPlatform.linux:
27 case TargetPlatform.windows:
28 expect(
29 theme.builders[platform],
30 isNotNull,
31 reason: 'theme builder for $platform is null',
32 );
33 case TargetPlatform.fuchsia:
34 expect(
35 theme.builders[platform],
36 isNull,
37 reason: 'theme builder for $platform is not null',
38 );
39 }
40 }
41 });
42
43 testWidgets(
44 'Default PageTransitionsTheme builds a CupertinoPageTransition',
45 (WidgetTester tester) async {
46 final Map<String, WidgetBuilder> routes = <String, WidgetBuilder>{
47 '/': (BuildContext context) => Material(
48 child: TextButton(
49 child: const Text('push'),
50 onPressed: () {
51 Navigator.of(context).pushNamed('/b');
52 },
53 ),
54 ),
55 '/b': (BuildContext context) => const Text('page b'),
56 };
57
58 await tester.pumpWidget(MaterialApp(routes: routes));
59
60 expect(
61 Theme.of(tester.element(find.text('push'))).platform,
62 debugDefaultTargetPlatformOverride,
63 );
64 expect(find.byType(CupertinoPageTransition), findsOneWidget);
65
66 await tester.tap(find.text('push'));
67 await tester.pumpAndSettle();
68 expect(find.text('page b'), findsOneWidget);
69 expect(find.byType(CupertinoPageTransition), findsOneWidget);
70 },
71 variant: const TargetPlatformVariant(<TargetPlatform>{
72 TargetPlatform.iOS,
73 TargetPlatform.macOS,
74 }),
75 );
76
77 testWidgets(
78 'Default PageTransitionsTheme builds a _ZoomPageTransition for android',
79 (WidgetTester tester) async {
80 final Map<String, WidgetBuilder> routes = <String, WidgetBuilder>{
81 '/': (BuildContext context) => Material(
82 child: TextButton(
83 child: const Text('push'),
84 onPressed: () {
85 Navigator.of(context).pushNamed('/b');
86 },
87 ),
88 ),
89 '/b': (BuildContext context) => const Text('page b'),
90 };
91
92 await tester.pumpWidget(MaterialApp(routes: routes));
93
94 Finder findZoomPageTransition() {
95 return find.descendant(
96 of: find.byType(MaterialApp),
97 matching: find.byWidgetPredicate(
98 (Widget w) => '${w.runtimeType}' == '_ZoomPageTransition',
99 ),
100 );
101 }
102
103 expect(
104 Theme.of(tester.element(find.text('push'))).platform,
105 debugDefaultTargetPlatformOverride,
106 );
107 expect(findZoomPageTransition(), findsOneWidget);
108
109 await tester.tap(find.text('push'));
110 await tester.pumpAndSettle();
111 expect(find.text('page b'), findsOneWidget);
112 expect(findZoomPageTransition(), findsOneWidget);
113 },
114 variant: TargetPlatformVariant.only(TargetPlatform.android),
115 );
116
117 testWidgets(
118 'Default background color when FadeForwardsPageTransitionBuilder is used',
119 (WidgetTester tester) async {
120 final Map<String, WidgetBuilder> routes = <String, WidgetBuilder>{
121 '/': (BuildContext context) => Material(
122 child: TextButton(
123 child: const Text('push'),
124 onPressed: () {
125 Navigator.of(context).pushNamed('/b');
126 },
127 ),
128 ),
129 '/b': (BuildContext context) => const Text('page b'),
130 };
131
132 await tester.pumpWidget(
133 MaterialApp(
134 theme: ThemeData(
135 pageTransitionsTheme: const PageTransitionsTheme(
136 builders: <TargetPlatform, PageTransitionsBuilder>{
137 TargetPlatform.android: FadeForwardsPageTransitionsBuilder(),
138 },
139 ),
140 colorScheme: ThemeData().colorScheme.copyWith(surface: Colors.pink),
141 ),
142 routes: routes,
143 ),
144 );
145
146 Finder findFadeForwardsPageTransition() {
147 return find.descendant(
148 of: find.byType(MaterialApp),
149 matching: find.byWidgetPredicate(
150 (Widget w) => '${w.runtimeType}' == '_FadeForwardsPageTransition',
151 ),
152 );
153 }
154
155 expect(findFadeForwardsPageTransition(), findsOneWidget);
156
157 await tester.tap(find.text('push'));
158 await tester.pump(const Duration(milliseconds: 400));
159
160 final Finder coloredBoxFinder = find.byType(ColoredBox).last;
161 expect(coloredBoxFinder, findsOneWidget);
162 final ColoredBox coloredBox = tester.widget<ColoredBox>(coloredBoxFinder);
163 expect(coloredBox.color, Colors.pink);
164
165 await tester.pumpAndSettle();
166 expect(find.text('page b'), findsOneWidget);
167 expect(findFadeForwardsPageTransition(), findsOneWidget);
168 },
169 variant: TargetPlatformVariant.only(TargetPlatform.android),
170 );
171
172 testWidgets(
173 'Override background color in FadeForwardsPageTransitionBuilder',
174 (WidgetTester tester) async {
175 final Map<String, WidgetBuilder> routes = <String, WidgetBuilder>{
176 '/': (BuildContext context) => Material(
177 child: TextButton(
178 child: const Text('push'),
179 onPressed: () {
180 Navigator.of(context).pushNamed('/b');
181 },
182 ),
183 ),
184 '/b': (BuildContext context) => const Text('page b'),
185 };
186
187 await tester.pumpWidget(
188 MaterialApp(
189 theme: ThemeData(
190 pageTransitionsTheme: const PageTransitionsTheme(
191 builders: <TargetPlatform, PageTransitionsBuilder>{
192 TargetPlatform.android: FadeForwardsPageTransitionsBuilder(
193 backgroundColor: Colors.lightGreen,
194 ),
195 },
196 ),
197 colorScheme: ThemeData().colorScheme.copyWith(surface: Colors.pink),
198 ),
199 routes: routes,
200 ),
201 );
202
203 Finder findFadeForwardsPageTransition() {
204 return find.descendant(
205 of: find.byType(MaterialApp),
206 matching: find.byWidgetPredicate(
207 (Widget w) => '${w.runtimeType}' == '_FadeForwardsPageTransition',
208 ),
209 );
210 }
211
212 expect(findFadeForwardsPageTransition(), findsOneWidget);
213
214 await tester.tap(find.text('push'));
215 await tester.pump(const Duration(milliseconds: 400));
216
217 final Finder coloredBoxFinder = find.byType(ColoredBox).last;
218 expect(coloredBoxFinder, findsOneWidget);
219 final ColoredBox coloredBox = tester.widget<ColoredBox>(coloredBoxFinder);
220 expect(coloredBox.color, Colors.lightGreen);
221
222 await tester.pumpAndSettle();
223 expect(find.text('page b'), findsOneWidget);
224 expect(findFadeForwardsPageTransition(), findsOneWidget);
225 },
226 variant: TargetPlatformVariant.only(TargetPlatform.android),
227 );
228
229 group('FadeForwardsPageTransitionsBuilder transitions', () {
230 testWidgets(
231 'opacity fades out during forward secondary animation',
232 (WidgetTester tester) async {
233 final AnimationController controller = AnimationController(
234 duration: const Duration(milliseconds: 100),
235 vsync: const TestVSync(),
236 );
237 addTearDown(controller.dispose);
238 final Animation<double> animation = Tween<double>(begin: 1, end: 0).animate(controller);
239 final Animation<double> secondaryAnimation = Tween<double>(
240 begin: 0,
241 end: 1,
242 ).animate(controller);
243
244 await tester.pumpWidget(
245 Builder(
246 builder: (BuildContext context) {
247 return const FadeForwardsPageTransitionsBuilder().delegatedTransition!(
248 context,
249 animation,
250 secondaryAnimation,
251 false,
252 const SizedBox(),
253 )!;
254 },
255 ),
256 );
257
258 final RenderAnimatedOpacity? renderOpacity = tester
259 .element(find.byType(SizedBox))
260 .findAncestorRenderObjectOfType<RenderAnimatedOpacity>();
261
262 // Since secondary animation is forward, transition will be reverse between duration 0 to 0.25.
263 controller.value = 0.0;
264 await tester.pump();
265 expect(renderOpacity?.opacity.value, 1.0);
266
267 controller.value = 0.25;
268 await tester.pump();
269 expect(renderOpacity?.opacity.value, 0.0);
270 },
271 variant: TargetPlatformVariant.only(TargetPlatform.android),
272 );
273
274 testWidgets(
275 'opacity fades in during reverse secondary animaation',
276 (WidgetTester tester) async {
277 final AnimationController controller = AnimationController(
278 duration: const Duration(milliseconds: 100),
279 vsync: const TestVSync(),
280 );
281 addTearDown(controller.dispose);
282 final Animation<double> animation = Tween<double>(begin: 0, end: 1).animate(controller);
283 final Animation<double> secondaryAnimation = Tween<double>(
284 begin: 1,
285 end: 0,
286 ).animate(controller);
287
288 await tester.pumpWidget(
289 Builder(
290 builder: (BuildContext context) {
291 return const FadeForwardsPageTransitionsBuilder().delegatedTransition!(
292 context,
293 animation,
294 secondaryAnimation,
295 false,
296 const SizedBox(),
297 )!;
298 },
299 ),
300 );
301
302 final RenderAnimatedOpacity? renderOpacity = tester
303 .element(find.byType(SizedBox))
304 .findAncestorRenderObjectOfType<RenderAnimatedOpacity>();
305
306 // Since secondary animation is reverse, transition will be forward between duration 0.75 to 1.0.
307 controller.value = 0.75;
308 await tester.pump();
309 expect(renderOpacity?.opacity.value, 0.0);
310
311 controller.value = 1.0;
312 await tester.pump();
313 expect(renderOpacity?.opacity.value, 1.0);
314 },
315 variant: TargetPlatformVariant.only(TargetPlatform.android),
316 );
317 });
318
319 testWidgets(
320 'FadeForwardsPageTransitionBuilder default duration is 800ms',
321 (WidgetTester tester) async {
322 final Map<String, WidgetBuilder> routes = <String, WidgetBuilder>{
323 '/': (BuildContext context) => Material(
324 child: TextButton(
325 child: const Text('push'),
326 onPressed: () {
327 Navigator.of(context).pushNamed('/b');
328 },
329 ),
330 ),
331 '/b': (BuildContext context) => const Text('page b'),
332 };
333
334 await tester.pumpWidget(
335 MaterialApp(
336 theme: ThemeData(
337 pageTransitionsTheme: const PageTransitionsTheme(
338 builders: <TargetPlatform, PageTransitionsBuilder>{
339 TargetPlatform.android: FadeForwardsPageTransitionsBuilder(),
340 },
341 ),
342 ),
343 routes: routes,
344 ),
345 );
346
347 Finder findFadeForwardsPageTransition() {
348 return find.descendant(
349 of: find.byType(MaterialApp),
350 matching: find.byWidgetPredicate(
351 (Widget w) => '${w.runtimeType}' == '_FadeForwardsPageTransition',
352 ),
353 );
354 }
355
356 expect(findFadeForwardsPageTransition(), findsOneWidget);
357
358 await tester.tap(find.text('push'));
359 await tester.pump(const Duration(milliseconds: 799));
360 expect(find.text('page b'), findsNothing);
361 ColoredBox coloredBox = tester.widget(find.byType(ColoredBox).last);
362 expect(
363 coloredBox.color,
364 isNot(Colors.transparent),
365 ); // Color is not transparent during animation.
366
367 await tester.pump(const Duration(milliseconds: 801));
368 expect(find.text('page b'), findsOneWidget);
369 coloredBox = tester.widget(find.byType(ColoredBox).last);
370 expect(coloredBox.color, Colors.transparent); // Color is transparent during animation.
371 },
372 variant: TargetPlatformVariant.only(TargetPlatform.android),
373 );
374
375 testWidgets(
376 'CupertinoPageTransitionsBuilder default duration is 500ms',
377 (WidgetTester tester) async {
378 final Map<String, WidgetBuilder> routes = <String, WidgetBuilder>{
379 '/': (BuildContext context) => Material(
380 child: TextButton(
381 child: const Text('push'),
382 onPressed: () {
383 Navigator.of(context).pushNamed('/b');
384 },
385 ),
386 ),
387 '/b': (BuildContext context) => const Text('page b'),
388 };
389
390 await tester.pumpWidget(
391 MaterialApp(
392 theme: ThemeData(
393 pageTransitionsTheme: const PageTransitionsTheme(
394 builders: <TargetPlatform, PageTransitionsBuilder>{
395 TargetPlatform.iOS: CupertinoPageTransitionsBuilder(),
396 },
397 ),
398 ),
399 routes: routes,
400 ),
401 );
402
403 expect(find.byType(CupertinoPageTransition), findsOneWidget);
404
405 await tester.tap(find.text('push'));
406 await tester.pump();
407 await tester.pump(const Duration(milliseconds: 499));
408 expect(tester.hasRunningAnimations, isTrue);
409
410 await tester.pump(const Duration(milliseconds: 10));
411 expect(tester.hasRunningAnimations, isFalse);
412 },
413 variant: TargetPlatformVariant.only(TargetPlatform.iOS),
414 );
415
416 testWidgets(
417 'Animation duration changes accordingly when page transition builder changes',
418 (WidgetTester tester) async {
419 Widget buildApp(PageTransitionsBuilder pageTransitionBuilder) {
420 return MaterialApp(
421 theme: ThemeData(
422 pageTransitionsTheme: PageTransitionsTheme(
423 builders: <TargetPlatform, PageTransitionsBuilder>{
424 TargetPlatform.android: pageTransitionBuilder,
425 },
426 ),
427 ),
428 routes: <String, WidgetBuilder>{
429 '/': (BuildContext context) => Material(
430 child: TextButton(
431 child: const Text('push'),
432 onPressed: () {
433 Navigator.of(context).pushNamed('/b');
434 },
435 ),
436 ),
437 '/b': (BuildContext context) => Material(
438 child: Column(
439 mainAxisAlignment: MainAxisAlignment.center,
440 children: <Widget>[
441 TextButton(
442 child: const Text('pop'),
443 onPressed: () {
444 Navigator.of(context).pop();
445 },
446 ),
447 const Text('page b'),
448 ],
449 ),
450 ),
451 },
452 );
453 }
454
455 await tester.pumpWidget(buildApp(const FadeForwardsPageTransitionsBuilder()));
456
457 Finder findFadeForwardsPageTransition() {
458 return find.descendant(
459 of: find.byType(MaterialApp),
460 matching: find.byWidgetPredicate(
461 (Widget w) => '${w.runtimeType}' == '_FadeForwardsPageTransition',
462 ),
463 );
464 }
465
466 expect(findFadeForwardsPageTransition(), findsOneWidget);
467
468 await tester.tap(find.text('push'));
469 await tester.pump(const Duration(milliseconds: 799));
470 expect(find.text('page b'), findsNothing);
471 ColoredBox coloredBox = tester.widget(find.byType(ColoredBox).last);
472 expect(
473 coloredBox.color,
474 isNot(Colors.transparent),
475 ); // The color is not transparent during animation.
476
477 await tester.pump(const Duration(milliseconds: 801));
478 expect(find.text('page b'), findsOneWidget);
479 coloredBox = tester.widget(find.byType(ColoredBox).last);
480 expect(coloredBox.color, Colors.transparent); // The color is transparent during animation.
481
482 await tester.pumpWidget(buildApp(const FadeUpwardsPageTransitionsBuilder()));
483 await tester.pumpAndSettle();
484 expect(
485 find.descendant(
486 of: find.byType(MaterialApp),
487 matching: find.byWidgetPredicate(
488 (Widget w) => '${w.runtimeType}' == '_FadeUpwardsPageTransition',
489 ),
490 ),
491 findsOneWidget,
492 );
493 await tester.tap(find.text('pop'));
494 await tester.pump(const Duration(milliseconds: 299));
495 expect(find.text('page b'), findsOneWidget);
496 expect(
497 find.byType(ColoredBox),
498 findsNothing,
499 ); // ColoredBox doesn't exist in FadeUpwardsPageTransition.
500
501 await tester.pump(const Duration(milliseconds: 301));
502 expect(find.text('page b'), findsNothing);
503 expect(find.text('push'), findsOneWidget); // The first page
504 expect(find.byType(ColoredBox), findsNothing);
505 },
506 variant: TargetPlatformVariant.only(TargetPlatform.android),
507 );
508
509 testWidgets(
510 'PageTransitionsTheme override builds a _OpenUpwardsPageTransition',
511 (WidgetTester tester) async {
512 final Map<String, WidgetBuilder> routes = <String, WidgetBuilder>{
513 '/': (BuildContext context) => Material(
514 child: TextButton(
515 child: const Text('push'),
516 onPressed: () {
517 Navigator.of(context).pushNamed('/b');
518 },
519 ),
520 ),
521 '/b': (BuildContext context) => const Text('page b'),
522 };
523
524 await tester.pumpWidget(
525 MaterialApp(
526 theme: ThemeData(
527 pageTransitionsTheme: const PageTransitionsTheme(
528 builders: <TargetPlatform, PageTransitionsBuilder>{
529 TargetPlatform.android:
530 OpenUpwardsPageTransitionsBuilder(), // creates a _OpenUpwardsPageTransition
531 },
532 ),
533 ),
534 routes: routes,
535 ),
536 );
537
538 Finder findOpenUpwardsPageTransition() {
539 return find.descendant(
540 of: find.byType(MaterialApp),
541 matching: find.byWidgetPredicate(
542 (Widget w) => '${w.runtimeType}' == '_OpenUpwardsPageTransition',
543 ),
544 );
545 }
546
547 expect(
548 Theme.of(tester.element(find.text('push'))).platform,
549 debugDefaultTargetPlatformOverride,
550 );
551 expect(findOpenUpwardsPageTransition(), findsOneWidget);
552
553 await tester.tap(find.text('push'));
554 await tester.pumpAndSettle();
555 expect(find.text('page b'), findsOneWidget);
556 expect(findOpenUpwardsPageTransition(), findsOneWidget);
557 },
558 variant: TargetPlatformVariant.only(TargetPlatform.android),
559 );
560
561 testWidgets(
562 'PageTransitionsTheme override builds a CupertinoPageTransition on android',
563 (WidgetTester tester) async {
564 final Map<String, WidgetBuilder> routes = <String, WidgetBuilder>{
565 '/': (BuildContext context) => Material(
566 child: TextButton(
567 child: const Text('push'),
568 onPressed: () {
569 Navigator.of(context).pushNamed('/b');
570 },
571 ),
572 ),
573 '/b': (BuildContext context) => const Text('page b'),
574 };
575
576 await tester.pumpWidget(
577 MaterialApp(
578 theme: ThemeData(
579 pageTransitionsTheme: const PageTransitionsTheme(
580 builders: <TargetPlatform, PageTransitionsBuilder>{
581 TargetPlatform.android: CupertinoPageTransitionsBuilder(),
582 },
583 ),
584 ),
585 routes: routes,
586 ),
587 );
588
589 expect(
590 Theme.of(tester.element(find.text('push'))).platform,
591 debugDefaultTargetPlatformOverride,
592 );
593 expect(find.byType(CupertinoPageTransition), findsOneWidget);
594
595 await tester.tap(find.text('push'));
596 await tester.pumpAndSettle();
597 expect(find.text('page b'), findsOneWidget);
598 expect(find.byType(CupertinoPageTransition), findsOneWidget);
599 },
600 variant: TargetPlatformVariant.only(TargetPlatform.android),
601 );
602
603 testWidgets(
604 'CupertinoPageTransition on android does not block gestures on backswipe',
605 (WidgetTester tester) async {
606 final Map<String, WidgetBuilder> routes = <String, WidgetBuilder>{
607 '/': (BuildContext context) => Material(
608 child: TextButton(
609 child: const Text('push'),
610 onPressed: () {
611 Navigator.of(context).pushNamed('/b');
612 },
613 ),
614 ),
615 '/b': (BuildContext context) => const Text('page b'),
616 };
617
618 await tester.pumpWidget(
619 MaterialApp(
620 theme: ThemeData(
621 pageTransitionsTheme: const PageTransitionsTheme(
622 builders: <TargetPlatform, PageTransitionsBuilder>{
623 TargetPlatform.android: CupertinoPageTransitionsBuilder(),
624 },
625 ),
626 ),
627 routes: routes,
628 ),
629 );
630
631 expect(
632 Theme.of(tester.element(find.text('push'))).platform,
633 debugDefaultTargetPlatformOverride,
634 );
635 expect(find.byType(CupertinoPageTransition), findsOneWidget);
636
637 await tester.tap(find.text('push'));
638 await tester.pumpAndSettle();
639 expect(find.text('page b'), findsOneWidget);
640 expect(find.byType(CupertinoPageTransition), findsOneWidget);
641
642 await tester.pumpAndSettle(const Duration(minutes: 1));
643
644 final TestGesture gesture = await tester.startGesture(const Offset(5.0, 100.0));
645 await gesture.moveBy(const Offset(400.0, 0.0));
646 await gesture.up();
647 await tester.pump();
648
649 await tester.pumpAndSettle(const Duration(minutes: 1));
650
651 expect(find.text('push'), findsOneWidget);
652 await tester.tap(find.text('push'));
653 await tester.pumpAndSettle();
654 expect(find.text('page b'), findsOneWidget);
655 },
656 variant: TargetPlatformVariant.only(TargetPlatform.android),
657 );
658
659 testWidgets(
660 'PageTransitionsTheme override builds a _FadeUpwardsTransition',
661 (WidgetTester tester) async {
662 final Map<String, WidgetBuilder> routes = <String, WidgetBuilder>{
663 '/': (BuildContext context) => Material(
664 child: TextButton(
665 child: const Text('push'),
666 onPressed: () {
667 Navigator.of(context).pushNamed('/b');
668 },
669 ),
670 ),
671 '/b': (BuildContext context) => const Text('page b'),
672 };
673
674 await tester.pumpWidget(
675 MaterialApp(
676 theme: ThemeData(
677 pageTransitionsTheme: const PageTransitionsTheme(
678 builders: <TargetPlatform, PageTransitionsBuilder>{
679 TargetPlatform.android:
680 FadeUpwardsPageTransitionsBuilder(), // creates a _FadeUpwardsTransition
681 },
682 ),
683 ),
684 routes: routes,
685 ),
686 );
687
688 Finder findFadeUpwardsPageTransition() {
689 return find.descendant(
690 of: find.byType(MaterialApp),
691 matching: find.byWidgetPredicate(
692 (Widget w) => '${w.runtimeType}' == '_FadeUpwardsPageTransition',
693 ),
694 );
695 }
696
697 expect(
698 Theme.of(tester.element(find.text('push'))).platform,
699 debugDefaultTargetPlatformOverride,
700 );
701 expect(findFadeUpwardsPageTransition(), findsOneWidget);
702
703 await tester.tap(find.text('push'));
704 await tester.pumpAndSettle();
705 expect(find.text('page b'), findsOneWidget);
706 expect(findFadeUpwardsPageTransition(), findsOneWidget);
707 },
708 variant: TargetPlatformVariant.only(TargetPlatform.android),
709 );
710
711 Widget boilerplate({
712 required bool themeAllowSnapshotting,
713 bool secondRouteAllowSnapshotting = true,
714 }) {
715 return MaterialApp(
716 theme: ThemeData(
717 pageTransitionsTheme: PageTransitionsTheme(
718 builders: <TargetPlatform, PageTransitionsBuilder>{
719 TargetPlatform.android: ZoomPageTransitionsBuilder(
720 allowSnapshotting: themeAllowSnapshotting,
721 ),
722 },
723 ),
724 ),
725 onGenerateRoute: (RouteSettings settings) {
726 if (settings.name == '/') {
727 return MaterialPageRoute<Widget>(builder: (_) => const Material(child: Text('Page 1')));
728 }
729 return MaterialPageRoute<Widget>(
730 builder: (_) => const Material(child: Text('Page 2')),
731 allowSnapshotting: secondRouteAllowSnapshotting,
732 );
733 },
734 );
735 }
736
737 bool isTransitioningWithSnapshotting(WidgetTester tester, Finder of) {
738 final Iterable<Layer> layers = tester.layerListOf(
739 find.ancestor(of: of, matching: find.byType(SnapshotWidget)).first,
740 );
741 final bool hasOneOpacityLayer = layers.whereType<OpacityLayer>().length == 1;
742 final bool hasOneTransformLayer = layers.whereType<TransformLayer>().length == 1;
743 // When snapshotting is on, the OpacityLayer and TransformLayer will not be
744 // applied directly.
745 return !(hasOneOpacityLayer && hasOneTransformLayer);
746 }
747
748 testWidgets(
749 'ZoomPageTransitionsBuilder default route snapshotting behavior',
750 (WidgetTester tester) async {
751 await tester.pumpWidget(boilerplate(themeAllowSnapshotting: true));
752
753 final Finder page1 = find.text('Page 1');
754 final Finder page2 = find.text('Page 2');
755
756 // Transitioning from page 1 to page 2.
757 tester.state<NavigatorState>(find.byType(Navigator)).pushNamed('/2');
758 await tester.pump();
759 await tester.pump(const Duration(milliseconds: 50));
760
761 // Exiting route should be snapshotted.
762 expect(isTransitioningWithSnapshotting(tester, page1), isTrue);
763
764 // Entering route should be snapshotted.
765 expect(isTransitioningWithSnapshotting(tester, page2), isTrue);
766
767 await tester.pumpAndSettle();
768
769 // Transitioning back from page 2 to page 1.
770 tester.state<NavigatorState>(find.byType(Navigator)).pop();
771 await tester.pump();
772 await tester.pump(const Duration(milliseconds: 50));
773
774 // Exiting route should be snapshotted.
775 expect(isTransitioningWithSnapshotting(tester, page2), isTrue);
776
777 // Entering route should be snapshotted.
778 expect(isTransitioningWithSnapshotting(tester, page1), isTrue);
779 },
780 variant: TargetPlatformVariant.only(TargetPlatform.android),
781 skip: kIsWeb, // [intended] rasterization is not used on the web.
782 );
783
784 testWidgets(
785 'ZoomPageTransitionsBuilder.allowSnapshotting can disable route snapshotting',
786 (WidgetTester tester) async {
787 await tester.pumpWidget(boilerplate(themeAllowSnapshotting: false));
788
789 final Finder page1 = find.text('Page 1');
790 final Finder page2 = find.text('Page 2');
791
792 // Transitioning from page 1 to page 2.
793 tester.state<NavigatorState>(find.byType(Navigator)).pushNamed('/2');
794 await tester.pump();
795 await tester.pump(const Duration(milliseconds: 50));
796
797 // Exiting route should not be snapshotted.
798 expect(isTransitioningWithSnapshotting(tester, page1), isFalse);
799
800 // Entering route should not be snapshotted.
801 expect(isTransitioningWithSnapshotting(tester, page2), isFalse);
802
803 await tester.pumpAndSettle();
804
805 // Transitioning back from page 2 to page 1.
806 tester.state<NavigatorState>(find.byType(Navigator)).pop();
807 await tester.pump();
808 await tester.pump(const Duration(milliseconds: 50));
809
810 // Exiting route should not be snapshotted.
811 expect(isTransitioningWithSnapshotting(tester, page2), isFalse);
812
813 // Entering route should not be snapshotted.
814 expect(isTransitioningWithSnapshotting(tester, page1), isFalse);
815 },
816 variant: TargetPlatformVariant.only(TargetPlatform.android),
817 skip: kIsWeb, // [intended] rasterization is not used on the web.
818 );
819
820 testWidgets(
821 'Setting PageRoute.allowSnapshotting to false overrides ZoomPageTransitionsBuilder.allowSnapshotting = true',
822 (WidgetTester tester) async {
823 await tester.pumpWidget(
824 boilerplate(themeAllowSnapshotting: true, secondRouteAllowSnapshotting: false),
825 );
826
827 final Finder page1 = find.text('Page 1');
828 final Finder page2 = find.text('Page 2');
829
830 // Transitioning from page 1 to page 2.
831 tester.state<NavigatorState>(find.byType(Navigator)).pushNamed('/2');
832 await tester.pump();
833 await tester.pump(const Duration(milliseconds: 50));
834
835 // First route should be snapshotted.
836 expect(isTransitioningWithSnapshotting(tester, page1), isTrue);
837
838 // Second route should not be snapshotted.
839 expect(isTransitioningWithSnapshotting(tester, page2), isFalse);
840
841 await tester.pumpAndSettle();
842 },
843 variant: TargetPlatformVariant.only(TargetPlatform.android),
844 skip: kIsWeb, // [intended] rasterization is not used on the web.
845 );
846
847 testWidgets(
848 '_ZoomPageTransition only causes child widget built once',
849 (WidgetTester tester) async {
850 // Regression test for https://github.com/flutter/flutter/issues/58345
851
852 int builtCount = 0;
853
854 final Map<String, WidgetBuilder> routes = <String, WidgetBuilder>{
855 '/': (BuildContext context) => Material(
856 child: TextButton(
857 child: const Text('push'),
858 onPressed: () {
859 Navigator.of(context).pushNamed('/b');
860 },
861 ),
862 ),
863 '/b': (BuildContext context) => StatefulBuilder(
864 builder: (BuildContext context, StateSetter setState) {
865 builtCount++; // Increase [builtCount] each time the widget build
866 return TextButton(
867 child: const Text('pop'),
868 onPressed: () {
869 Navigator.pop(context);
870 },
871 );
872 },
873 ),
874 };
875
876 await tester.pumpWidget(
877 MaterialApp(
878 theme: ThemeData(
879 pageTransitionsTheme: const PageTransitionsTheme(
880 builders: <TargetPlatform, PageTransitionsBuilder>{
881 TargetPlatform.android:
882 ZoomPageTransitionsBuilder(), // creates a _ZoomPageTransition
883 },
884 ),
885 ),
886 routes: routes,
887 ),
888 );
889
890 // No matter push or pop was called, the child widget should built only once.
891 await tester.tap(find.text('push'));
892 await tester.pumpAndSettle();
893 expect(builtCount, 1);
894
895 await tester.tap(find.text('pop'));
896 await tester.pumpAndSettle();
897 expect(builtCount, 1);
898 },
899 variant: TargetPlatformVariant.only(TargetPlatform.android),
900 );
901
902 testWidgets(
903 'predictive back gestures pop the route on all platforms regardless of whether their transition handles predictive back',
904 (WidgetTester tester) async {
905 final Map<String, WidgetBuilder> routes = <String, WidgetBuilder>{
906 '/': (BuildContext context) => Material(
907 child: TextButton(
908 child: const Text('push'),
909 onPressed: () {
910 Navigator.of(context).pushNamed('/b');
911 },
912 ),
913 ),
914 '/b': (BuildContext context) => const Text('page b'),
915 };
916
917 await tester.pumpWidget(MaterialApp(routes: routes));
918
919 expect(find.text('push'), findsOneWidget);
920 expect(find.text('page b'), findsNothing);
921
922 await tester.tap(find.text('push'));
923 await tester.pumpAndSettle();
924
925 expect(find.text('push'), findsNothing);
926 expect(find.text('page b'), findsOneWidget);
927
928 // Start a system pop gesture.
929 final ByteData startMessage = const StandardMethodCodec().encodeMethodCall(
930 const MethodCall('startBackGesture', <String, dynamic>{
931 'touchOffset': <double>[5.0, 300.0],
932 'progress': 0.0,
933 'swipeEdge': 0, // left
934 }),
935 );
936 await binding.defaultBinaryMessenger.handlePlatformMessage(
937 'flutter/backgesture',
938 startMessage,
939 (ByteData? _) {},
940 );
941 await tester.pump();
942
943 expect(find.text('push'), findsNothing);
944 expect(find.text('page b'), findsOneWidget);
945
946 // Drag the system back gesture far enough to commit.
947 final ByteData updateMessage = const StandardMethodCodec().encodeMethodCall(
948 const MethodCall('updateBackGestureProgress', <String, dynamic>{
949 'x': 100.0,
950 'y': 300.0,
951 'progress': 0.35,
952 'swipeEdge': 0, // left
953 }),
954 );
955 await binding.defaultBinaryMessenger.handlePlatformMessage(
956 'flutter/backgesture',
957 updateMessage,
958 (ByteData? _) {},
959 );
960 await tester.pumpAndSettle();
961
962 expect(find.text('push'), findsNothing);
963 expect(find.text('page b'), findsOneWidget);
964
965 // Commit the system back gesture.
966 final ByteData commitMessage = const StandardMethodCodec().encodeMethodCall(
967 const MethodCall('commitBackGesture'),
968 );
969 await binding.defaultBinaryMessenger.handlePlatformMessage(
970 'flutter/backgesture',
971 commitMessage,
972 (ByteData? _) {},
973 );
974 await tester.pumpAndSettle();
975
976 expect(find.text('push'), findsOneWidget);
977 expect(find.text('page b'), findsNothing);
978 },
979 variant: TargetPlatformVariant.all(),
980 );
981
982 testWidgets(
983 'ZoomPageTransitionsBuilder uses theme color during transition effects',
984 (WidgetTester tester) async {
985 // Color that is being tested for presence.
986 const Color themeTestSurfaceColor = Color.fromARGB(255, 195, 255, 0);
987
988 final Map<String, WidgetBuilder> routes = <String, WidgetBuilder>{
989 '/': (BuildContext context) => Material(
990 child: Scaffold(
991 appBar: AppBar(title: const Text('Home Page')),
992 body: Center(
993 child: Column(
994 mainAxisAlignment: MainAxisAlignment.center,
995 children: <Widget>[
996 ElevatedButton(
997 onPressed: () {
998 Navigator.pushNamed(context, '/scaffolded');
999 },
1000 child: const Text('Route with scaffold!'),
1001 ),
1002 ElevatedButton(
1003 onPressed: () {
1004 Navigator.pushNamed(context, '/not-scaffolded');
1005 },
1006 child: const Text('Route with NO scaffold!'),
1007 ),
1008 ],
1009 ),
1010 ),
1011 ),
1012 ),
1013 '/scaffolded': (BuildContext context) => Material(
1014 child: Scaffold(
1015 appBar: AppBar(title: const Text('Scaffolded Page')),
1016 body: Center(
1017 child: ElevatedButton(
1018 onPressed: () {
1019 Navigator.pop(context);
1020 },
1021 child: const Text('Back to home route...'),
1022 ),
1023 ),
1024 ),
1025 ),
1026 '/not-scaffolded': (BuildContext context) => Material(
1027 child: Center(
1028 child: ElevatedButton(
1029 onPressed: () {
1030 Navigator.pop(context);
1031 },
1032 child: const Text('Back to home route...'),
1033 ),
1034 ),
1035 ),
1036 };
1037
1038 await tester.pumpWidget(
1039 MaterialApp(
1040 theme: ThemeData(
1041 colorScheme: ColorScheme.fromSeed(
1042 seedColor: Colors.blue,
1043 surface: themeTestSurfaceColor,
1044 ),
1045 pageTransitionsTheme: PageTransitionsTheme(
1046 builders: <TargetPlatform, PageTransitionsBuilder>{
1047 // Force all platforms to use ZoomPageTransitionsBuilder to test each one.
1048 for (final TargetPlatform platform in TargetPlatform.values)
1049 platform: const ZoomPageTransitionsBuilder(),
1050 },
1051 ),
1052 ),
1053 routes: routes,
1054 ),
1055 );
1056
1057 // Go to scaffolded page.
1058 await tester.tap(find.text('Route with scaffold!'));
1059
1060 // Pump till animation is half-way through.
1061 await tester.pump();
1062 await tester.pump(const Duration(milliseconds: 75));
1063
1064 // Verify that the render box is painting the right color for scaffolded pages.
1065 final RenderBox scaffoldedRenderBox = tester.firstRenderObject<RenderBox>(
1066 find.byType(MaterialApp),
1067 );
1068 // Expect the color to be at exactly 12.2% opacity at this time.
1069 expect(scaffoldedRenderBox, paints..rect(color: themeTestSurfaceColor.withOpacity(0.122)));
1070
1071 await tester.pumpAndSettle();
1072
1073 // Go back home and then go to non-scaffolded page.
1074 await tester.tap(find.text('Back to home route...'));
1075 await tester.pumpAndSettle();
1076 await tester.tap(find.text('Route with NO scaffold!'));
1077
1078 // Pump till animation is half-way through.
1079 await tester.pump();
1080 await tester.pump(const Duration(milliseconds: 125));
1081
1082 // Verify that the render box is painting the right color for non-scaffolded pages.
1083 final RenderBox nonScaffoldedRenderBox = tester.firstRenderObject<RenderBox>(
1084 find.byType(MaterialApp),
1085 );
1086 // Expect the color to be at exactly 59.6% opacity at this time.
1087 expect(nonScaffoldedRenderBox, paints..rect(color: themeTestSurfaceColor.withOpacity(0.596)));
1088
1089 await tester.pumpAndSettle();
1090
1091 // Verify that the transition successfully completed.
1092 expect(find.text('Back to home route...'), findsOneWidget);
1093 },
1094 variant: TargetPlatformVariant.all(),
1095 );
1096
1097 testWidgets(
1098 'ZoomPageTransitionsBuilder uses developer-provided color during transition effects if provided',
1099 (WidgetTester tester) async {
1100 // Color that is being tested for presence.
1101 const Color testSurfaceColor = Colors.red;
1102
1103 final Map<String, WidgetBuilder> routes = <String, WidgetBuilder>{
1104 '/': (BuildContext context) => Material(
1105 child: Scaffold(
1106 appBar: AppBar(title: const Text('Home Page')),
1107 body: Center(
1108 child: Column(
1109 mainAxisAlignment: MainAxisAlignment.center,
1110 children: <Widget>[
1111 ElevatedButton(
1112 onPressed: () {
1113 Navigator.pushNamed(context, '/scaffolded');
1114 },
1115 child: const Text('Route with scaffold!'),
1116 ),
1117 ElevatedButton(
1118 onPressed: () {
1119 Navigator.pushNamed(context, '/not-scaffolded');
1120 },
1121 child: const Text('Route with NO scaffold!'),
1122 ),
1123 ],
1124 ),
1125 ),
1126 ),
1127 ),
1128 '/scaffolded': (BuildContext context) => Material(
1129 child: Scaffold(
1130 appBar: AppBar(title: const Text('Scaffolded Page')),
1131 body: Center(
1132 child: ElevatedButton(
1133 onPressed: () {
1134 Navigator.pop(context);
1135 },
1136 child: const Text('Back to home route...'),
1137 ),
1138 ),
1139 ),
1140 ),
1141 '/not-scaffolded': (BuildContext context) => Material(
1142 child: Center(
1143 child: ElevatedButton(
1144 onPressed: () {
1145 Navigator.pop(context);
1146 },
1147 child: const Text('Back to home route...'),
1148 ),
1149 ),
1150 ),
1151 };
1152
1153 await tester.pumpWidget(
1154 MaterialApp(
1155 theme: ThemeData(
1156 colorScheme: ColorScheme.fromSeed(seedColor: Colors.blue, surface: Colors.blue),
1157 pageTransitionsTheme: PageTransitionsTheme(
1158 builders: <TargetPlatform, PageTransitionsBuilder>{
1159 // Force all platforms to use ZoomPageTransitionsBuilder to test each one.
1160 for (final TargetPlatform platform in TargetPlatform.values)
1161 platform: const ZoomPageTransitionsBuilder(backgroundColor: testSurfaceColor),
1162 },
1163 ),
1164 ),
1165 routes: routes,
1166 ),
1167 );
1168
1169 // Go to scaffolded page.
1170 await tester.tap(find.text('Route with scaffold!'));
1171
1172 // Pump till animation is half-way through.
1173 await tester.pump();
1174 await tester.pump(const Duration(milliseconds: 75));
1175
1176 // Verify that the render box is painting the right color for scaffolded pages.
1177 final RenderBox scaffoldedRenderBox = tester.firstRenderObject<RenderBox>(
1178 find.byType(MaterialApp),
1179 );
1180 // Expect the color to be at exactly 12.2% opacity at this time.
1181 expect(scaffoldedRenderBox, paints..rect(color: testSurfaceColor.withOpacity(0.122)));
1182
1183 await tester.pumpAndSettle();
1184
1185 // Go back home and then go to non-scaffolded page.
1186 await tester.tap(find.text('Back to home route...'));
1187 await tester.pumpAndSettle();
1188 await tester.tap(find.text('Route with NO scaffold!'));
1189
1190 // Pump till animation is half-way through.
1191 await tester.pump();
1192 await tester.pump(const Duration(milliseconds: 125));
1193
1194 // Verify that the render box is painting the right color for non-scaffolded pages.
1195 final RenderBox nonScaffoldedRenderBox = tester.firstRenderObject<RenderBox>(
1196 find.byType(MaterialApp),
1197 );
1198 // Expect the color to be at exactly 59.6% opacity at this time.
1199 expect(nonScaffoldedRenderBox, paints..rect(color: testSurfaceColor.withOpacity(0.596)));
1200
1201 await tester.pumpAndSettle();
1202
1203 // Verify that the transition successfully completed.
1204 expect(find.text('Back to home route...'), findsOneWidget);
1205 },
1206 variant: TargetPlatformVariant.all(),
1207 );
1208
1209 testWidgets(
1210 'Can interact with incoming route during FadeForwards back navigation',
1211 (WidgetTester tester) async {
1212 final Map<String, WidgetBuilder> routes = <String, WidgetBuilder>{
1213 '/': (BuildContext context) => Material(
1214 child: TextButton(
1215 child: const Text('push'),
1216 onPressed: () {
1217 Navigator.of(context).pushNamed('/b');
1218 },
1219 ),
1220 ),
1221 '/b': (BuildContext context) => Material(
1222 child: TextButton(
1223 child: const Text('go back'),
1224 onPressed: () {
1225 Navigator.of(context).pop();
1226 },
1227 ),
1228 ),
1229 };
1230
1231 await tester.pumpWidget(
1232 MaterialApp(
1233 theme: ThemeData(
1234 pageTransitionsTheme: const PageTransitionsTheme(
1235 builders: <TargetPlatform, PageTransitionsBuilder>{
1236 TargetPlatform.android: FadeForwardsPageTransitionsBuilder(),
1237 },
1238 ),
1239 colorScheme: ThemeData().colorScheme.copyWith(surface: Colors.pink),
1240 ),
1241 routes: routes,
1242 ),
1243 );
1244
1245 expect(find.text('push'), findsOneWidget);
1246 expect(find.text('go back'), findsNothing);
1247
1248 // Go to the second route. The duration of the FadeForwardsPageTransition
1249 // is 800ms.
1250 await tester.tap(find.text('push'));
1251 await tester.pump();
1252 await tester.pump(const Duration(milliseconds: 801));
1253
1254 expect(find.text('push'), findsNothing);
1255 expect(find.text('go back'), findsOneWidget);
1256
1257 // Tap to go back to the first route.
1258 await tester.tap(find.text('go back'));
1259 await tester.pump();
1260 await tester.pump(const Duration(milliseconds: 400));
1261
1262 expect(find.text('push'), findsOneWidget);
1263 expect(find.text('go back'), findsOneWidget);
1264
1265 // In the middle of the transition, tap to go back to the second route.
1266 await tester.tap(find.text('push'));
1267
1268 await tester.pump();
1269 await tester.pump(const Duration(milliseconds: 401));
1270
1271 expect(find.text('push'), findsOneWidget);
1272 expect(find.text('go back'), findsOneWidget);
1273
1274 await tester.pump();
1275 await tester.pump(const Duration(milliseconds: 400));
1276
1277 expect(find.text('push'), findsNothing);
1278 expect(find.text('go back'), findsOneWidget);
1279 },
1280 variant: TargetPlatformVariant.only(TargetPlatform.android),
1281 );
1282}
1283