| // 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 'dart:ui' as ui; |
| |
| import 'package:animations/src/modal.dart'; |
| import 'package:flutter/material.dart'; |
| import 'package:flutter_test/flutter_test.dart'; |
| |
| void main() { |
| testWidgets( |
| 'showModal builds a new route with specified barrier properties', |
| (WidgetTester tester) async { |
| await tester.pumpWidget( |
| MaterialApp( |
| home: Scaffold( |
| body: Builder(builder: (BuildContext context) { |
| return Center( |
| child: ElevatedButton( |
| onPressed: () { |
| showModal<void>( |
| context: context, |
| configuration: _TestModalConfiguration(), |
| builder: (BuildContext context) { |
| return const _FlutterLogoModal(); |
| }, |
| ); |
| }, |
| child: const Icon(Icons.add), |
| ), |
| ); |
| }), |
| ), |
| ), |
| ); |
| await tester.tap(find.byType(ElevatedButton)); |
| await tester.pumpAndSettle(); |
| |
| // New route containing _FlutterLogoModal is present. |
| expect(find.byType(_FlutterLogoModal), findsOneWidget); |
| final ModalBarrier topModalBarrier = tester.widget<ModalBarrier>( |
| find.byType(ModalBarrier).at(1), |
| ); |
| |
| // Verify new route's modal barrier properties are correct. |
| expect(topModalBarrier.color, Colors.green); |
| expect(topModalBarrier.barrierSemanticsDismissible, true); |
| expect(topModalBarrier.semanticsLabel, 'customLabel'); |
| }, |
| ); |
| |
| testWidgets( |
| 'showModal forwards animation', |
| (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: _TestModalConfiguration(), |
| builder: (BuildContext context) { |
| return _FlutterLogoModal(key: key); |
| }, |
| ); |
| }, |
| child: const Icon(Icons.add), |
| ), |
| ); |
| }), |
| ), |
| ), |
| ); |
| |
| // Start forwards animation |
| await tester.tap(find.byType(ElevatedButton)); |
| await tester.pump(); |
| |
| // Opacity duration: Linear transition throughout 300ms |
| double topFadeTransitionOpacity = _getOpacity(key, tester); |
| expect(topFadeTransitionOpacity, 0.0); |
| |
| // Halfway through forwards animation. |
| await tester.pump(const Duration(milliseconds: 150)); |
| topFadeTransitionOpacity = _getOpacity(key, tester); |
| expect(topFadeTransitionOpacity, 0.5); |
| |
| // The end of the transition. |
| await tester.pump(const Duration(milliseconds: 150)); |
| topFadeTransitionOpacity = _getOpacity(key, tester); |
| expect(topFadeTransitionOpacity, 1.0); |
| |
| await tester.pump(const Duration(milliseconds: 1)); |
| expect(find.byType(_FlutterLogoModal), findsOneWidget); |
| }, |
| ); |
| |
| testWidgets( |
| 'showModal reverse animation', |
| (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: _TestModalConfiguration(), |
| builder: (BuildContext context) { |
| return _FlutterLogoModal(key: key); |
| }, |
| ); |
| }, |
| child: const Icon(Icons.add), |
| ), |
| ); |
| }), |
| ), |
| ), |
| ); |
| |
| // Start forwards animation |
| await tester.tap(find.byType(ElevatedButton)); |
| await tester.pumpAndSettle(); |
| expect(find.byType(_FlutterLogoModal), findsOneWidget); |
| |
| await tester.tapAt(Offset.zero); |
| await tester.pump(); |
| |
| // Opacity duration: Linear transition throughout 200ms |
| double topFadeTransitionOpacity = _getOpacity(key, tester); |
| expect(topFadeTransitionOpacity, 1.0); |
| |
| // Halfway through forwards animation. |
| await tester.pump(const Duration(milliseconds: 100)); |
| topFadeTransitionOpacity = _getOpacity(key, tester); |
| expect(topFadeTransitionOpacity, 0.5); |
| |
| // The end of the transition. |
| await tester.pump(const Duration(milliseconds: 100)); |
| topFadeTransitionOpacity = _getOpacity(key, tester); |
| expect(topFadeTransitionOpacity, 0.0); |
| |
| await tester.pump(const Duration(milliseconds: 1)); |
| expect(find.byType(_FlutterLogoModal), findsNothing); |
| }, |
| ); |
| |
| testWidgets( |
| 'showModal builds a new route with specified barrier properties ' |
| 'with default configuration(FadeScaleTransitionConfiguration)', |
| (WidgetTester tester) async { |
| await tester.pumpWidget( |
| MaterialApp( |
| home: Scaffold( |
| body: Builder(builder: (BuildContext context) { |
| return Center( |
| child: ElevatedButton( |
| onPressed: () { |
| showModal<void>( |
| context: context, |
| builder: (BuildContext context) { |
| return const _FlutterLogoModal(); |
| }, |
| ); |
| }, |
| child: const Icon(Icons.add), |
| ), |
| ); |
| }), |
| ), |
| ), |
| ); |
| await tester.tap(find.byType(ElevatedButton)); |
| await tester.pumpAndSettle(); |
| |
| // New route containing _FlutterLogoModal is present. |
| expect(find.byType(_FlutterLogoModal), findsOneWidget); |
| final ModalBarrier topModalBarrier = tester.widget<ModalBarrier>( |
| find.byType(ModalBarrier).at(1), |
| ); |
| |
| // Verify new route's modal barrier properties are correct. |
| expect(topModalBarrier.color, Colors.black54); |
| expect(topModalBarrier.barrierSemanticsDismissible, true); |
| expect(topModalBarrier.semanticsLabel, 'Dismiss'); |
| }, |
| ); |
| |
| testWidgets( |
| 'showModal forwards animation ' |
| 'with default configuration(FadeScaleTransitionConfiguration)', |
| (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, |
| builder: (BuildContext context) { |
| return _FlutterLogoModal(key: key); |
| }, |
| ); |
| }, |
| child: const Icon(Icons.add), |
| ), |
| ); |
| }), |
| ), |
| ), |
| ); |
| |
| // Start forwards animation |
| 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(const Duration(milliseconds: 1)); |
| expect(find.byType(_FlutterLogoModal), findsOneWidget); |
| }, |
| ); |
| |
| testWidgets( |
| 'showModal reverse animation ' |
| 'with default configuration(FadeScaleTransitionConfiguration)', |
| (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, |
| builder: (BuildContext context) { |
| return _FlutterLogoModal(key: key); |
| }, |
| ); |
| }, |
| child: const Icon(Icons.add), |
| ), |
| ); |
| }), |
| ), |
| ), |
| ); |
| |
| // Start forwards animation |
| await tester.tap(find.byType(ElevatedButton)); |
| await tester.pumpAndSettle(); |
| expect(find.byType(_FlutterLogoModal), findsOneWidget); |
| |
| // 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( |
| '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: _TestModalConfiguration(), |
| 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: 150)); |
| 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: 100)); |
| 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( |
| 'showModal builds a new route with specified route settings', |
| (WidgetTester tester) async { |
| const RouteSettings routeSettings = RouteSettings( |
| name: 'route-name', |
| arguments: 'arguments', |
| ); |
| |
| final Widget button = Builder(builder: (BuildContext context) { |
| return Center( |
| child: ElevatedButton( |
| onPressed: () { |
| showModal<void>( |
| context: context, |
| configuration: _TestModalConfiguration(), |
| routeSettings: routeSettings, |
| builder: (BuildContext context) { |
| return const _FlutterLogoModal(); |
| }, |
| ); |
| }, |
| child: const Icon(Icons.add), |
| ), |
| ); |
| }); |
| |
| await tester.pumpWidget(_boilerplate(button)); |
| await tester.tap(find.byType(ElevatedButton)); |
| await tester.pumpAndSettle(); |
| |
| // New route containing _FlutterLogoModal is present. |
| expect(find.byType(_FlutterLogoModal), findsOneWidget); |
| |
| // Expect the last route pushed to the navigator to contain RouteSettings |
| // equal to the RouteSettings passed to showModal |
| final ModalRoute<dynamic> modalRoute = ModalRoute.of( |
| tester.element(find.byType(_FlutterLogoModal)), |
| )!; |
| expect(modalRoute.settings, routeSettings); |
| }, |
| ); |
| |
| testWidgets( |
| 'showModal builds a new route with specified image filter', |
| (WidgetTester tester) async { |
| final ui.ImageFilter filter = ui.ImageFilter.blur(sigmaX: 1, sigmaY: 1); |
| |
| final Widget button = Builder(builder: (BuildContext context) { |
| return Center( |
| child: ElevatedButton( |
| onPressed: () { |
| showModal<void>( |
| context: context, |
| configuration: _TestModalConfiguration(), |
| filter: filter, |
| builder: (BuildContext context) { |
| return const _FlutterLogoModal(); |
| }, |
| ); |
| }, |
| child: const Icon(Icons.add), |
| ), |
| ); |
| }); |
| |
| await tester.pumpWidget(_boilerplate(button)); |
| await tester.tap(find.byType(ElevatedButton)); |
| await tester.pumpAndSettle(); |
| |
| // New route containing _FlutterLogoModal is present. |
| expect(find.byType(_FlutterLogoModal), findsOneWidget); |
| final BackdropFilter backdropFilter = tester.widget<BackdropFilter>( |
| find.byType(BackdropFilter), |
| ); |
| |
| // Verify new route's backdrop filter has been applied |
| expect(backdropFilter.filter, filter); |
| }, |
| ); |
| } |
| |
| Widget _boilerplate(Widget child) => MaterialApp(home: Scaffold(body: child)); |
| |
| 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({ |
| super.key, |
| this.name, |
| }); |
| |
| 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), |
| ), |
| ), |
| ), |
| ); |
| } |
| } |
| |
| class _TestModalConfiguration extends ModalConfiguration { |
| _TestModalConfiguration() |
| : super( |
| barrierColor: Colors.green, |
| barrierDismissible: true, |
| barrierLabel: 'customLabel', |
| transitionDuration: const Duration(milliseconds: 300), |
| reverseTransitionDuration: const Duration(milliseconds: 200), |
| ); |
| |
| @override |
| Widget transitionBuilder( |
| BuildContext context, |
| Animation<double> animation, |
| Animation<double> secondaryAnimation, |
| Widget child, |
| ) { |
| return FadeTransition( |
| opacity: animation, |
| child: child, |
| ); |
| } |
| } |