blob: aa07e505d6bc12fd4afc7c56de1985949dc457fa [file] [log] [blame]
// Copyright 2014 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:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
void main() {
testWidgets('PageTransitionsBuilder buildTransitions method is called correctly', (
WidgetTester tester,
) async {
var buildTransitionsCalled = false;
PageRoute<dynamic>? capturedRoute;
BuildContext? capturedContext;
Animation<double>? capturedAnimation;
Animation<double>? capturedSecondaryAnimation;
Widget? capturedChild;
final builderWithCapture = _TestPageTransitionsBuilder(
onBuildTransitions:
<T>(
PageRoute<T> route,
BuildContext context,
Animation<double> animation,
Animation<double> secondaryAnimation,
Widget child,
) {
buildTransitionsCalled = true;
capturedRoute = route;
capturedContext = context;
capturedAnimation = animation;
capturedSecondaryAnimation = secondaryAnimation;
capturedChild = child;
return SlideTransition(
position: Tween<Offset>(
begin: const Offset(1.0, 0.0),
end: Offset.zero,
).animate(animation),
child: child,
);
},
);
final routes = <String, WidgetBuilder>{
'/': (BuildContext context) => Material(
child: TextButton(
child: const Text('push'),
onPressed: () {
Navigator.of(context).pushNamed('/test');
},
),
),
'/test': (BuildContext context) => const Material(child: Text('test page')),
};
await tester.pumpWidget(
MaterialApp(
theme: ThemeData(
pageTransitionsTheme: PageTransitionsTheme(
builders: <TargetPlatform, PageTransitionsBuilder>{
TargetPlatform.android: builderWithCapture,
},
),
),
routes: routes,
),
);
// Trigger navigation
await tester.tap(find.text('push'));
await tester.pump();
// Verify buildTransitions was called with correct parameters
expect(buildTransitionsCalled, isTrue);
expect(capturedRoute, isNotNull);
expect(capturedContext, isNotNull);
expect(capturedAnimation, isNotNull);
expect(capturedSecondaryAnimation, isNotNull);
expect(capturedChild, isNotNull);
expect(capturedRoute!.settings.name, '/');
});
testWidgets('PageTransitionsBuilder works with custom Navigator and PageRoute', (
WidgetTester tester,
) async {
final customTransitionsBuilder = _TestPageTransitionsBuilder(
onBuildTransitions:
<T>(
PageRoute<T> route,
BuildContext context,
Animation<double> animation,
Animation<double> secondaryAnimation,
Widget child,
) {
return FadeTransition(
opacity: animation,
child: ScaleTransition(
scale: animation.drive(
Tween<double>(begin: 0.5, end: 1.0).chain(CurveTween(curve: Curves.easeInOut)),
),
child: child,
),
);
},
);
await tester.pumpWidget(
Directionality(
textDirection: TextDirection.ltr,
child: Navigator(
onGenerateRoute: (RouteSettings settings) {
return _CustomPageRoute<void>(
settings: settings,
transitionsBuilder: customTransitionsBuilder,
builder: (BuildContext context) {
if (settings.name == '/') {
return Center(
child: GestureDetector(
onTap: () {
Navigator.of(context).pushNamed('/second');
},
child: Container(
width: 200,
height: 50,
color: const Color(0xFF2196F3),
child: const Center(
child: Text('Navigate', style: TextStyle(color: Color(0xFFFFFFFF))),
),
),
),
);
}
return const ColoredBox(
color: Color(0xFF4CAF50),
child: Center(
child: Text(
'Second Page',
style: TextStyle(color: Color(0xFFFFFFFF), fontSize: 24),
),
),
);
},
);
},
),
),
);
expect(find.text('Navigate'), findsOneWidget);
expect(find.text('Second Page'), findsNothing);
await tester.tap(find.text('Navigate'));
await tester.pump();
expect(find.text('Navigate'), findsOneWidget);
expect(find.text('Second Page'), findsOneWidget);
await tester.pump(const Duration(milliseconds: 50));
final FadeTransition fadeTransition = tester.widget<FadeTransition>(
find.byType(FadeTransition).first,
);
expect(fadeTransition.opacity.value, greaterThan(0.0));
expect(fadeTransition.opacity.value, lessThanOrEqualTo(1.0));
final ScaleTransition scaleTransition = tester.widget<ScaleTransition>(
find.byType(ScaleTransition).first,
);
expect(scaleTransition.scale.value, greaterThanOrEqualTo(0.5));
expect(scaleTransition.scale.value, lessThanOrEqualTo(1.0));
await tester.pumpAndSettle();
expect(find.text('Navigate'), findsNothing);
expect(find.text('Second Page'), findsOneWidget);
});
testWidgets('FadeUpwardsPageTransitionsBuilder test', (WidgetTester tester) async {
await tester.pumpWidget(
Directionality(
textDirection: TextDirection.ltr,
child: Navigator(
onGenerateRoute: (RouteSettings settings) {
return _CustomPageRoute<void>(
settings: settings,
transitionsBuilder: const FadeUpwardsPageTransitionsBuilder(),
builder: (BuildContext context) {
if (settings.name == '/') {
return ColoredBox(
color: const Color(0xFF2196F3),
child: Center(
child: GestureDetector(
onTap: () {
Navigator.of(context).pushNamed('/second');
},
child: const Text(
'Page 1',
style: TextStyle(color: Color(0xFFFFFFFF), fontSize: 24),
),
),
),
);
}
return const ColoredBox(
color: Color(0xFF4CAF50),
child: Center(
child: Text('Page 2', style: TextStyle(color: Color(0xFFFFFFFF), fontSize: 24)),
),
);
},
);
},
),
),
);
final Offset widget1TopLeft = tester.getTopLeft(find.text('Page 1'));
await tester.tap(find.text('Page 1'));
await tester.pump();
await tester.pump(const Duration(milliseconds: 1));
FadeTransition widget2Opacity = tester
.element(find.text('Page 2'))
.findAncestorWidgetOfExactType<FadeTransition>()!;
Offset widget2TopLeft = tester.getTopLeft(find.text('Page 2'));
expect(widget1TopLeft.dx == widget2TopLeft.dx, true);
expect(widget1TopLeft.dy < widget2TopLeft.dy, true);
expect(widget2Opacity.opacity.value < 0.01, true);
await tester.pump(const Duration(milliseconds: 300));
expect(find.text('Page 1'), findsNothing);
expect(find.text('Page 2'), isOnstage);
tester.state<NavigatorState>(find.byType(Navigator)).pop();
await tester.pump();
await tester.pump(const Duration(milliseconds: 1));
widget2Opacity = tester
.element(find.text('Page 2'))
.findAncestorWidgetOfExactType<FadeTransition>()!;
widget2TopLeft = tester.getTopLeft(find.text('Page 2'));
expect(widget1TopLeft.dy < widget2TopLeft.dy, true);
expect(widget2Opacity.opacity.value < 1.0, true);
await tester.pump(const Duration(milliseconds: 300));
expect(find.text('Page 1'), isOnstage);
expect(find.text('Page 2'), findsNothing);
});
testWidgets(
'FadeUpwardsPageTransitionsBuilder test with Material PageTransitionTheme',
(WidgetTester tester) async {
await tester.pumpWidget(
MaterialApp(
home: const Material(child: Text('Page 1')),
theme: ThemeData(
pageTransitionsTheme: const PageTransitionsTheme(
builders: <TargetPlatform, PageTransitionsBuilder>{
TargetPlatform.android: FadeUpwardsPageTransitionsBuilder(),
},
),
),
routes: <String, WidgetBuilder>{
'/next': (BuildContext context) {
return const Material(child: Text('Page 2'));
},
},
),
);
final Offset widget1TopLeft = tester.getTopLeft(find.text('Page 1'));
tester.state<NavigatorState>(find.byType(Navigator)).pushNamed('/next');
await tester.pump();
await tester.pump(const Duration(milliseconds: 1));
FadeTransition widget2Opacity = tester
.element(find.text('Page 2'))
.findAncestorWidgetOfExactType<FadeTransition>()!;
Offset widget2TopLeft = tester.getTopLeft(find.text('Page 2'));
final Size widget2Size = tester.getSize(find.text('Page 2'));
// Android transition is vertical only.
expect(widget1TopLeft.dx == widget2TopLeft.dx, true);
// Page 1 is above page 2 mid-transition.
expect(widget1TopLeft.dy < widget2TopLeft.dy, true);
// Animation begins 3/4 of the way up the page.
expect(widget2TopLeft.dy < widget2Size.height / 4.0, true);
// Animation starts with page 2 being near transparent.
expect(widget2Opacity.opacity.value < 0.01, true);
await tester.pump(const Duration(milliseconds: 300));
// Page 2 covers page 1.
expect(find.text('Page 1'), findsNothing);
expect(find.text('Page 2'), isOnstage);
tester.state<NavigatorState>(find.byType(Navigator)).pop();
await tester.pump();
await tester.pump(const Duration(milliseconds: 1));
widget2Opacity = tester
.element(find.text('Page 2'))
.findAncestorWidgetOfExactType<FadeTransition>()!;
widget2TopLeft = tester.getTopLeft(find.text('Page 2'));
// Page 2 starts to move down.
expect(widget1TopLeft.dy < widget2TopLeft.dy, true);
// Page 2 starts to lose opacity.
expect(widget2Opacity.opacity.value < 1.0, true);
await tester.pump(const Duration(milliseconds: 300));
expect(find.text('Page 1'), isOnstage);
expect(find.text('Page 2'), findsNothing);
},
variant: TargetPlatformVariant.only(TargetPlatform.android),
);
testWidgets(
'PageTransitionsTheme override builds a _OpenUpwardsPageTransition',
(WidgetTester tester) async {
final routes = <String, WidgetBuilder>{
'/': (BuildContext context) => Material(
child: TextButton(
child: const Text('push'),
onPressed: () {
Navigator.of(context).pushNamed('/b');
},
),
),
'/b': (BuildContext context) => const Text('page b'),
};
await tester.pumpWidget(
MaterialApp(
theme: ThemeData(
pageTransitionsTheme: const PageTransitionsTheme(
builders: <TargetPlatform, PageTransitionsBuilder>{
TargetPlatform.android:
OpenUpwardsPageTransitionsBuilder(), // creates a _OpenUpwardsPageTransition
},
),
),
routes: routes,
),
);
Finder findOpenUpwardsPageTransition() {
return find.descendant(
of: find.byType(MaterialApp),
matching: find.byWidgetPredicate(
(Widget w) => '${w.runtimeType}' == '_OpenUpwardsPageTransition',
),
);
}
expect(
Theme.of(tester.element(find.text('push'))).platform,
debugDefaultTargetPlatformOverride,
);
expect(findOpenUpwardsPageTransition(), findsOneWidget);
await tester.tap(find.text('push'));
await tester.pumpAndSettle();
expect(find.text('page b'), findsOneWidget);
expect(findOpenUpwardsPageTransition(), findsOneWidget);
},
variant: TargetPlatformVariant.only(TargetPlatform.android),
);
testWidgets('OpenUpwardsPageTransitionsBuilder test', (WidgetTester tester) async {
await tester.pumpWidget(
Directionality(
textDirection: TextDirection.ltr,
child: Navigator(
onGenerateRoute: (RouteSettings settings) {
return _CustomPageRoute<void>(
settings: settings,
transitionsBuilder: const OpenUpwardsPageTransitionsBuilder(),
builder: (BuildContext context) {
if (settings.name == '/') {
return ColoredBox(
color: const Color(0xFF2196F3),
child: Center(
child: GestureDetector(
onTap: () {
Navigator.of(context).pushNamed('/second');
},
child: const Text(
'Page 1',
style: TextStyle(color: Color(0xFFFFFFFF), fontSize: 24),
),
),
),
);
}
return const ColoredBox(
color: Color(0xFF4CAF50),
child: Center(
child: Text('Page 2', style: TextStyle(color: Color(0xFFFFFFFF), fontSize: 24)),
),
);
},
);
},
),
),
);
final Offset widget1TopLeft = tester.getTopLeft(find.text('Page 1'));
await tester.tap(find.text('Page 1'));
await tester.pump();
await tester.pump(const Duration(milliseconds: 1));
expect(find.text('Page 1'), findsOneWidget);
expect(find.text('Page 2'), findsOneWidget);
final Offset widget2TopLeft = tester.getTopLeft(find.text('Page 2'));
expect(widget1TopLeft.dx, widget2TopLeft.dx);
expect(widget1TopLeft.dy <= widget2TopLeft.dy, true);
await tester.pump(const Duration(milliseconds: 300));
// After animation, only Page 2 should be visible.
expect(find.text('Page 1'), findsNothing);
expect(find.text('Page 2'), isOnstage);
tester.state<NavigatorState>(find.byType(Navigator)).pop();
await tester.pump();
await tester.pump(const Duration(milliseconds: 1));
expect(find.text('Page 1'), findsOneWidget);
expect(find.text('Page 2'), findsOneWidget);
await tester.pump(const Duration(milliseconds: 300));
// After reverse animation, only Page 1 should be visible.
expect(find.text('Page 1'), isOnstage);
expect(find.text('Page 2'), findsNothing);
});
testWidgets(
'OpenUpwardsPageTransitionsBuilder test with Material PageTransitionTheme',
(WidgetTester tester) async {
await tester.pumpWidget(
MaterialApp(
home: const Material(child: Text('Page 1')),
theme: ThemeData(
pageTransitionsTheme: const PageTransitionsTheme(
builders: <TargetPlatform, PageTransitionsBuilder>{
TargetPlatform.android: OpenUpwardsPageTransitionsBuilder(),
},
),
),
routes: <String, WidgetBuilder>{
'/next': (BuildContext context) {
return const Material(child: Text('Page 2'));
},
},
),
);
final Offset widget1TopLeft = tester.getTopLeft(find.text('Page 1'));
tester.state<NavigatorState>(find.byType(Navigator)).pushNamed('/next');
await tester.pump();
await tester.pump(const Duration(milliseconds: 1));
expect(find.text('Page 1'), findsOneWidget);
expect(find.text('Page 2'), findsOneWidget);
final Offset widget2TopLeft = tester.getTopLeft(find.text('Page 2'));
expect(widget1TopLeft.dx, widget2TopLeft.dx);
expect(widget1TopLeft.dy < widget2TopLeft.dy, true);
await tester.pump(const Duration(milliseconds: 300));
// Page 2 covers page 1.
expect(find.text('Page 1'), findsNothing);
expect(find.text('Page 2'), isOnstage);
tester.state<NavigatorState>(find.byType(Navigator)).pop();
await tester.pump();
await tester.pump(const Duration(milliseconds: 1));
expect(find.text('Page 1'), findsOneWidget);
expect(find.text('Page 2'), findsOneWidget);
await tester.pump(const Duration(milliseconds: 300));
// Back to page 1.
expect(find.text('Page 1'), isOnstage);
expect(find.text('Page 2'), findsNothing);
},
variant: TargetPlatformVariant.only(TargetPlatform.android),
);
}
class _CustomPageRoute<T> extends PageRoute<T> {
_CustomPageRoute({required this.builder, required this.transitionsBuilder, super.settings});
final WidgetBuilder builder;
final PageTransitionsBuilder transitionsBuilder;
@override
Duration get transitionDuration => const Duration(milliseconds: 300);
@override
bool get maintainState => true;
@override
Color? get barrierColor => null;
@override
String? get barrierLabel => null;
@override
Widget buildPage(
BuildContext context,
Animation<double> animation,
Animation<double> secondaryAnimation,
) {
return builder(context);
}
@override
Widget buildTransitions(
BuildContext context,
Animation<double> animation,
Animation<double> secondaryAnimation,
Widget child,
) {
return transitionsBuilder.buildTransitions<T>(
this,
context,
animation,
secondaryAnimation,
child,
);
}
}
class _TestPageTransitionsBuilder extends PageTransitionsBuilder {
const _TestPageTransitionsBuilder({required this.onBuildTransitions});
final Widget Function<T>(
PageRoute<T> route,
BuildContext context,
Animation<double> animation,
Animation<double> secondaryAnimation,
Widget child,
)
onBuildTransitions;
@override
Widget buildTransitions<T>(
PageRoute<T> route,
BuildContext context,
Animation<double> animation,
Animation<double> secondaryAnimation,
Widget child,
) {
return onBuildTransitions(route, context, animation, secondaryAnimation, child);
}
}