| // Copyright 2013 The Flutter Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| import 'package:animations/src/fade_scale_transition.dart'; |
| import 'package:animations/src/modal.dart'; |
| import 'package:flutter/material.dart'; |
| import 'package:flutter_test/flutter_test.dart'; |
| |
| void main() { |
| testWidgets( |
| 'FadeScaleTransitionConfiguration builds a new route', |
| (WidgetTester tester) async { |
| await tester.pumpWidget( |
| MaterialApp( |
| home: Scaffold( |
| body: Builder(builder: (BuildContext context) { |
| return Center( |
| child: ElevatedButton( |
| onPressed: () { |
| showModal<void>( |
| context: context, |
| configuration: const FadeScaleTransitionConfiguration(), |
| builder: (BuildContext context) { |
| return const _FlutterLogoModal(); |
| }, |
| ); |
| }, |
| child: const Icon(Icons.add), |
| ), |
| ); |
| }), |
| ), |
| ), |
| ); |
| await tester.tap(find.byType(ElevatedButton)); |
| await tester.pumpAndSettle(); |
| expect(find.byType(_FlutterLogoModal), findsOneWidget); |
| }, |
| ); |
| |
| testWidgets( |
| 'FadeScaleTransitionConfiguration runs forward', |
| (WidgetTester tester) async { |
| final GlobalKey key = GlobalKey(); |
| await tester.pumpWidget( |
| MaterialApp( |
| home: Scaffold( |
| body: Builder(builder: (BuildContext context) { |
| return Center( |
| child: ElevatedButton( |
| onPressed: () { |
| showModal<void>( |
| context: context, |
| configuration: const FadeScaleTransitionConfiguration(), |
| builder: (BuildContext context) { |
| return _FlutterLogoModal(key: key); |
| }, |
| ); |
| }, |
| child: const Icon(Icons.add), |
| ), |
| ); |
| }), |
| ), |
| ), |
| ); |
| await tester.tap(find.byType(ElevatedButton)); |
| await tester.pump(); |
| // Opacity duration: First 30% of 150ms, linear transition |
| double topFadeTransitionOpacity = _getOpacity(key, tester); |
| double topScale = _getScale(key, tester); |
| expect(topFadeTransitionOpacity, 0.0); |
| expect(topScale, 0.80); |
| |
| // 3/10 * 150ms = 45ms (total opacity animation duration) |
| // 1/2 * 45ms = ~23ms elapsed for halfway point of opacity |
| // animation |
| await tester.pump(const Duration(milliseconds: 23)); |
| topFadeTransitionOpacity = _getOpacity(key, tester); |
| expect(topFadeTransitionOpacity, closeTo(0.5, 0.05)); |
| topScale = _getScale(key, tester); |
| expect(topScale, greaterThan(0.80)); |
| expect(topScale, lessThan(1.0)); |
| |
| // End of opacity animation |
| await tester.pump(const Duration(milliseconds: 22)); |
| topFadeTransitionOpacity = _getOpacity(key, tester); |
| expect(topFadeTransitionOpacity, 1.0); |
| topScale = _getScale(key, tester); |
| expect(topScale, greaterThan(0.80)); |
| expect(topScale, lessThan(1.0)); |
| |
| // 100ms into the animation |
| await tester.pump(const Duration(milliseconds: 55)); |
| topScale = _getScale(key, tester); |
| expect(topScale, greaterThan(0.80)); |
| expect(topScale, lessThan(1.0)); |
| |
| // Get to the end of the animation |
| await tester.pump(const Duration(milliseconds: 50)); |
| topScale = _getScale(key, tester); |
| expect(topScale, 1.0); |
| |
| await tester.pump(); |
| expect(find.byType(_FlutterLogoModal), findsOneWidget); |
| }, |
| ); |
| |
| testWidgets( |
| 'FadeScaleTransitionConfiguration runs forward', |
| (WidgetTester tester) async { |
| final GlobalKey key = GlobalKey(); |
| await tester.pumpWidget( |
| MaterialApp( |
| home: Scaffold( |
| body: Builder(builder: (BuildContext context) { |
| return Center( |
| child: ElevatedButton( |
| onPressed: () { |
| showModal<void>( |
| context: context, |
| configuration: const FadeScaleTransitionConfiguration(), |
| builder: (BuildContext context) { |
| return _FlutterLogoModal(key: key); |
| }, |
| ); |
| }, |
| child: const Icon(Icons.add), |
| ), |
| ); |
| }), |
| ), |
| ), |
| ); |
| // Show the incoming modal and let it animate in fully. |
| await tester.tap(find.byType(ElevatedButton)); |
| await tester.pumpAndSettle(); |
| |
| // Tap on modal barrier to start reverse animation. |
| await tester.tapAt(Offset.zero); |
| await tester.pump(); |
| |
| // Opacity duration: Linear transition throughout 75ms |
| // No scale animations on exit transition. |
| double topFadeTransitionOpacity = _getOpacity(key, tester); |
| double topScale = _getScale(key, tester); |
| expect(topFadeTransitionOpacity, 1.0); |
| expect(topScale, 1.0); |
| |
| await tester.pump(const Duration(milliseconds: 25)); |
| topFadeTransitionOpacity = _getOpacity(key, tester); |
| topScale = _getScale(key, tester); |
| expect(topFadeTransitionOpacity, closeTo(0.66, 0.05)); |
| expect(topScale, 1.0); |
| |
| await tester.pump(const Duration(milliseconds: 25)); |
| topFadeTransitionOpacity = _getOpacity(key, tester); |
| topScale = _getScale(key, tester); |
| expect(topFadeTransitionOpacity, closeTo(0.33, 0.05)); |
| expect(topScale, 1.0); |
| |
| // End of opacity animation |
| await tester.pump(const Duration(milliseconds: 25)); |
| topFadeTransitionOpacity = _getOpacity(key, tester); |
| expect(topFadeTransitionOpacity, 0.0); |
| topScale = _getScale(key, tester); |
| expect(topScale, 1.0); |
| |
| await tester.pump(const Duration(milliseconds: 1)); |
| expect(find.byType(_FlutterLogoModal), findsNothing); |
| }, |
| ); |
| |
| testWidgets( |
| 'FadeScaleTransitionConfiguration does not jump when interrupted', |
| (WidgetTester tester) async { |
| final GlobalKey key = GlobalKey(); |
| await tester.pumpWidget( |
| MaterialApp( |
| home: Scaffold( |
| body: Builder(builder: (BuildContext context) { |
| return Center( |
| child: ElevatedButton( |
| onPressed: () { |
| showModal<void>( |
| context: context, |
| configuration: const FadeScaleTransitionConfiguration(), |
| builder: (BuildContext context) { |
| return _FlutterLogoModal(key: key); |
| }, |
| ); |
| }, |
| child: const Icon(Icons.add), |
| ), |
| ); |
| }), |
| ), |
| ), |
| ); |
| |
| await tester.tap(find.byType(ElevatedButton)); |
| await tester.pump(); |
| // Opacity duration: First 30% of 150ms, linear transition |
| double topFadeTransitionOpacity = _getOpacity(key, tester); |
| double topScale = _getScale(key, tester); |
| expect(topFadeTransitionOpacity, 0.0); |
| expect(topScale, 0.80); |
| |
| // 3/10 * 150ms = 45ms (total opacity animation duration) |
| // End of opacity animation |
| await tester.pump(const Duration(milliseconds: 45)); |
| topFadeTransitionOpacity = _getOpacity(key, tester); |
| expect(topFadeTransitionOpacity, 1.0); |
| topScale = _getScale(key, tester); |
| expect(topScale, greaterThan(0.80)); |
| expect(topScale, lessThan(1.0)); |
| |
| // 100ms into the animation |
| await tester.pump(const Duration(milliseconds: 55)); |
| topFadeTransitionOpacity = _getOpacity(key, tester); |
| expect(topFadeTransitionOpacity, 1.0); |
| topScale = _getScale(key, tester); |
| expect(topScale, greaterThan(0.80)); |
| expect(topScale, lessThan(1.0)); |
| |
| // Start the reverse transition by interrupting the forwards |
| // transition. |
| await tester.tapAt(Offset.zero); |
| await tester.pump(); |
| // Opacity and scale values should remain the same after |
| // the reverse animation starts. |
| expect(_getOpacity(key, tester), topFadeTransitionOpacity); |
| expect(_getScale(key, tester), topScale); |
| |
| // Should animate in reverse with 2/3 * 75ms = 50ms |
| // using the enter transition's animation pattern |
| // instead of the exit animation pattern. |
| |
| // Calculation for the time when the linear fade |
| // transition should start if running backwards: |
| // 3/10 * 75ms = 22.5ms |
| // To get the 22.5ms timestamp, run backwards for: |
| // 50ms - 22.5ms = ~27.5ms |
| await tester.pump(const Duration(milliseconds: 27)); |
| topFadeTransitionOpacity = _getOpacity(key, tester); |
| expect(topFadeTransitionOpacity, 1.0); |
| topScale = _getScale(key, tester); |
| expect(topScale, greaterThan(0.80)); |
| expect(topScale, lessThan(1.0)); |
| |
| // Halfway through fade animation |
| await tester.pump(const Duration(milliseconds: 12)); |
| topFadeTransitionOpacity = _getOpacity(key, tester); |
| expect(topFadeTransitionOpacity, closeTo(0.5, 0.05)); |
| topScale = _getScale(key, tester); |
| expect(topScale, greaterThan(0.80)); |
| expect(topScale, lessThan(1.0)); |
| |
| // Complete the rest of the animation |
| await tester.pump(const Duration(milliseconds: 11)); |
| topFadeTransitionOpacity = _getOpacity(key, tester); |
| expect(topFadeTransitionOpacity, 0.0); |
| topScale = _getScale(key, tester); |
| expect(topScale, 0.8); |
| |
| await tester.pump(const Duration(milliseconds: 1)); |
| expect(find.byType(_FlutterLogoModal), findsNothing); |
| }, |
| ); |
| |
| testWidgets( |
| 'State is not lost when transitioning', |
| (WidgetTester tester) async { |
| final GlobalKey bottomKey = GlobalKey(); |
| final GlobalKey topKey = GlobalKey(); |
| |
| await tester.pumpWidget( |
| MaterialApp( |
| home: Scaffold( |
| body: Builder(builder: (BuildContext context) { |
| return Center( |
| child: Column( |
| children: <Widget>[ |
| ElevatedButton( |
| onPressed: () { |
| showModal<void>( |
| context: context, |
| configuration: |
| const FadeScaleTransitionConfiguration(), |
| builder: (BuildContext context) { |
| return _FlutterLogoModal( |
| key: topKey, |
| name: 'top route', |
| ); |
| }, |
| ); |
| }, |
| child: const Icon(Icons.add), |
| ), |
| _FlutterLogoModal( |
| key: bottomKey, |
| name: 'bottom route', |
| ), |
| ], |
| ), |
| ); |
| }), |
| ), |
| ), |
| ); |
| |
| // The bottom route's state should already exist. |
| final _FlutterLogoModalState bottomState = tester.state( |
| find.byKey(bottomKey), |
| ); |
| expect(bottomState.widget.name, 'bottom route'); |
| |
| // Start the enter transition of the modal route. |
| await tester.tap(find.byType(ElevatedButton)); |
| await tester.pump(); |
| await tester.pump(); |
| |
| // The bottom route's state should be retained at the start of the |
| // transition. |
| expect( |
| tester.state(find.byKey(bottomKey)), |
| bottomState, |
| ); |
| // The top route's state should be created. |
| final _FlutterLogoModalState topState = tester.state( |
| find.byKey(topKey), |
| ); |
| expect(topState.widget.name, 'top route'); |
| |
| // Halfway point of forwards animation. |
| await tester.pump(const Duration(milliseconds: 75)); |
| expect( |
| tester.state(find.byKey(bottomKey)), |
| bottomState, |
| ); |
| expect( |
| tester.state(find.byKey(topKey)), |
| topState, |
| ); |
| |
| // End the transition and see if top and bottom routes' |
| // states persist. |
| await tester.pumpAndSettle(); |
| expect( |
| tester.state(find.byKey( |
| bottomKey, |
| skipOffstage: false, |
| )), |
| bottomState, |
| ); |
| expect( |
| tester.state(find.byKey(topKey)), |
| topState, |
| ); |
| |
| // Start the reverse animation. Both top and bottom |
| // routes' states should persist. |
| await tester.tapAt(Offset.zero); |
| await tester.pump(); |
| expect( |
| tester.state(find.byKey(bottomKey)), |
| bottomState, |
| ); |
| expect( |
| tester.state(find.byKey(topKey)), |
| topState, |
| ); |
| |
| // Halfway point of the exit transition. |
| await tester.pump(const Duration(milliseconds: 38)); |
| expect( |
| tester.state(find.byKey(bottomKey)), |
| bottomState, |
| ); |
| expect( |
| tester.state(find.byKey(topKey)), |
| topState, |
| ); |
| |
| // End the exit transition. The bottom route's state should |
| // persist, whereas the top route's state should no longer |
| // be present. |
| await tester.pumpAndSettle(); |
| expect( |
| tester.state(find.byKey(bottomKey)), |
| bottomState, |
| ); |
| expect(find.byKey(topKey), findsNothing); |
| }, |
| ); |
| |
| testWidgets( |
| 'should preserve state', |
| (WidgetTester tester) async { |
| final AnimationController controller = AnimationController( |
| vsync: const TestVSync(), |
| duration: const Duration(milliseconds: 300), |
| ); |
| |
| await tester.pumpWidget( |
| MaterialApp( |
| home: Scaffold( |
| body: Center( |
| child: FadeScaleTransition( |
| animation: controller, |
| child: const _FlutterLogoModal(), |
| ), |
| ), |
| ), |
| ), |
| ); |
| |
| final State<StatefulWidget> state = tester.state( |
| find.byType(_FlutterLogoModal), |
| ); |
| expect(state, isNotNull); |
| |
| controller.forward(); |
| await tester.pump(); |
| expect(state, same(tester.state(find.byType(_FlutterLogoModal)))); |
| await tester.pump(const Duration(milliseconds: 150)); |
| expect(state, same(tester.state(find.byType(_FlutterLogoModal)))); |
| await tester.pumpAndSettle(); |
| expect(state, same(tester.state(find.byType(_FlutterLogoModal)))); |
| |
| controller.reverse(); |
| await tester.pump(); |
| expect(state, same(tester.state(find.byType(_FlutterLogoModal)))); |
| await tester.pump(const Duration(milliseconds: 150)); |
| expect(state, same(tester.state(find.byType(_FlutterLogoModal)))); |
| await tester.pumpAndSettle(); |
| expect(state, same(tester.state(find.byType(_FlutterLogoModal)))); |
| |
| controller.forward(); |
| await tester.pump(); |
| expect(state, same(tester.state(find.byType(_FlutterLogoModal)))); |
| await tester.pump(const Duration(milliseconds: 150)); |
| expect(state, same(tester.state(find.byType(_FlutterLogoModal)))); |
| await tester.pumpAndSettle(); |
| expect(state, same(tester.state(find.byType(_FlutterLogoModal)))); |
| }, |
| ); |
| } |
| |
| double _getOpacity(GlobalKey key, WidgetTester tester) { |
| final Finder finder = find.ancestor( |
| of: find.byKey(key), |
| matching: find.byType(FadeTransition), |
| ); |
| return tester.widgetList(finder).fold<double>(1.0, (double a, Widget widget) { |
| final FadeTransition transition = widget as FadeTransition; |
| return a * transition.opacity.value; |
| }); |
| } |
| |
| double _getScale(GlobalKey key, WidgetTester tester) { |
| final Finder finder = find.ancestor( |
| of: find.byKey(key), |
| matching: find.byType(ScaleTransition), |
| ); |
| return tester.widgetList(finder).fold<double>(1.0, (double a, Widget widget) { |
| final ScaleTransition transition = widget as ScaleTransition; |
| return a * transition.scale.value; |
| }); |
| } |
| |
| class _FlutterLogoModal extends StatefulWidget { |
| const _FlutterLogoModal({ |
| Key? key, |
| this.name, |
| }) : super(key: key); |
| |
| final String? name; |
| |
| @override |
| _FlutterLogoModalState createState() => _FlutterLogoModalState(); |
| } |
| |
| class _FlutterLogoModalState extends State<_FlutterLogoModal> { |
| @override |
| Widget build(BuildContext context) { |
| return const Center( |
| child: SizedBox( |
| width: 250, |
| height: 250, |
| child: Material( |
| child: Center( |
| child: FlutterLogo(size: 250), |
| ), |
| ), |
| ), |
| ); |
| } |
| } |