blob: c38b30618a05187cf8a1c63bf82751899acf8eeb [file] [log] [blame]
// 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,
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,
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,
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,
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,
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),
),
),
),
);
}
}