blob: 686961e860a77b11ecac53b385e469e100eab3cf [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 'dart:collection';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter/widgets.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:mockito/mockito.dart';
final List<String> results = <String>[];
Set<TestRoute> routes = HashSet<TestRoute>();
class TestRoute extends Route<String> with LocalHistoryRoute<String> {
TestRoute(this.name);
final String name;
@override
List<OverlayEntry> get overlayEntries => _entries;
final List<OverlayEntry> _entries = <OverlayEntry>[];
void log(String s) {
results.add('$name: $s');
}
@override
void install() {
log('install');
final OverlayEntry entry = OverlayEntry(
builder: (BuildContext context) => Container(),
opaque: true,
);
_entries.add(entry);
routes.add(this);
super.install();
}
@override
TickerFuture didPush() {
log('didPush');
return super.didPush();
}
@override
void didAdd() {
log('didAdd');
super.didAdd();
}
@override
void didReplace(Route<dynamic> oldRoute) {
expect(oldRoute, isA<TestRoute>());
final TestRoute castRoute = oldRoute as TestRoute;
log('didReplace ${castRoute.name}');
super.didReplace(castRoute);
}
@override
bool didPop(String result) {
log('didPop $result');
bool returnValue;
if (returnValue = super.didPop(result))
navigator.finalizeRoute(this);
return returnValue;
}
@override
void didPopNext(Route<dynamic> nextRoute) {
expect(nextRoute, isA<TestRoute>());
final TestRoute castRoute = nextRoute as TestRoute;
log('didPopNext ${castRoute.name}');
super.didPopNext(castRoute);
}
@override
void didChangeNext(Route<dynamic> nextRoute) {
expect(nextRoute, anyOf(isNull, isA<TestRoute>()));
final TestRoute castRoute = nextRoute as TestRoute;
log('didChangeNext ${castRoute?.name}');
super.didChangeNext(castRoute);
}
@override
void dispose() {
log('dispose');
_entries.clear();
routes.remove(this);
super.dispose();
}
}
Future<void> runNavigatorTest(
WidgetTester tester,
NavigatorState host,
VoidCallback test,
List<String> expectations,
) async {
expect(host, isNotNull);
test();
expect(results, equals(expectations));
results.clear();
await tester.pump();
}
void main() {
testWidgets('Route settings', (WidgetTester tester) async {
const RouteSettings settings = RouteSettings(name: 'A');
expect(settings, hasOneLineDescription);
final RouteSettings settings2 = settings.copyWith(name: 'B');
expect(settings2.name, 'B');
});
testWidgets('Route settings arguments', (WidgetTester tester) async {
const RouteSettings settings = RouteSettings(name: 'A');
expect(settings.arguments, isNull);
final Object arguments = Object();
final RouteSettings settings2 = RouteSettings(name: 'A', arguments: arguments);
expect(settings2.arguments, same(arguments));
final RouteSettings settings3 = settings2.copyWith();
expect(settings3.arguments, equals(arguments));
final Object arguments2 = Object();
final RouteSettings settings4 = settings2.copyWith(arguments: arguments2);
expect(settings4.arguments, same(arguments2));
expect(settings4.arguments, isNot(same(arguments)));
});
testWidgets('Route management - push, replace, pop sequence', (WidgetTester tester) async {
final GlobalKey<NavigatorState> navigatorKey = GlobalKey<NavigatorState>();
await tester.pumpWidget(
Directionality(
textDirection: TextDirection.ltr,
child: Navigator(
key: navigatorKey,
onGenerateRoute: (_) => TestRoute('initial'),
),
),
);
final NavigatorState host = navigatorKey.currentState;
await runNavigatorTest(
tester,
host,
() { },
<String>[
'initial: install',
'initial: didAdd',
'initial: didChangeNext null',
],
);
TestRoute second;
await runNavigatorTest(
tester,
host,
() { host.push(second = TestRoute('second')); },
<String>[ // stack is: initial, second
'second: install',
'second: didPush',
'second: didChangeNext null',
'initial: didChangeNext second',
],
);
await runNavigatorTest(
tester,
host,
() { host.push(TestRoute('third')); },
<String>[ // stack is: initial, second, third
'third: install',
'third: didPush',
'third: didChangeNext null',
'second: didChangeNext third',
],
);
await runNavigatorTest(
tester,
host,
() { host.replace(oldRoute: second, newRoute: TestRoute('two')); },
<String>[ // stack is: initial, two, third
'two: install',
'two: didReplace second',
'two: didChangeNext third',
'initial: didChangeNext two',
'second: dispose',
],
);
await runNavigatorTest(
tester,
host,
() { host.pop('hello'); },
<String>[ // stack is: initial, two
'third: didPop hello',
'two: didPopNext third',
'third: dispose',
],
);
await runNavigatorTest(
tester,
host,
() { host.pop('good bye'); },
<String>[ // stack is: initial
'two: didPop good bye',
'initial: didPopNext two',
'two: dispose',
],
);
await tester.pumpWidget(Container());
expect(results, equals(<String>['initial: dispose']));
expect(routes.isEmpty, isTrue);
results.clear();
});
testWidgets('Route management - push, remove, pop', (WidgetTester tester) async {
final GlobalKey<NavigatorState> navigatorKey = GlobalKey<NavigatorState>();
await tester.pumpWidget(
Directionality(
textDirection: TextDirection.ltr,
child: Navigator(
key: navigatorKey,
onGenerateRoute: (_) => TestRoute('first'),
),
),
);
final NavigatorState host = navigatorKey.currentState;
await runNavigatorTest(
tester,
host,
() { },
<String>[
'first: install',
'first: didAdd',
'first: didChangeNext null',
],
);
TestRoute second;
await runNavigatorTest(
tester,
host,
() { host.push(second = TestRoute('second')); },
<String>[
'second: install',
'second: didPush',
'second: didChangeNext null',
'first: didChangeNext second',
],
);
await runNavigatorTest(
tester,
host,
() { host.push(TestRoute('third')); },
<String>[
'third: install',
'third: didPush',
'third: didChangeNext null',
'second: didChangeNext third',
],
);
await runNavigatorTest(
tester,
host,
() { host.removeRouteBelow(second); },
<String>[
'first: dispose',
],
);
await runNavigatorTest(
tester,
host,
() { host.pop('good bye'); },
<String>[
'third: didPop good bye',
'second: didPopNext third',
'third: dispose',
],
);
await runNavigatorTest(
tester,
host,
() { host.push(TestRoute('three')); },
<String>[
'three: install',
'three: didPush',
'three: didChangeNext null',
'second: didChangeNext three',
],
);
TestRoute four;
await runNavigatorTest(
tester,
host,
() { host.push(four = TestRoute('four')); },
<String>[
'four: install',
'four: didPush',
'four: didChangeNext null',
'three: didChangeNext four',
],
);
await runNavigatorTest(
tester,
host,
() { host.removeRouteBelow(four); },
<String>[
'second: didChangeNext four',
'three: dispose',
],
);
await runNavigatorTest(
tester,
host,
() { host.pop('the end'); },
<String>[
'four: didPop the end',
'second: didPopNext four',
'four: dispose',
],
);
await tester.pumpWidget(Container());
expect(results, equals(<String>['second: dispose']));
expect(routes.isEmpty, isTrue);
results.clear();
});
testWidgets('Route management - push, replace, popUntil', (WidgetTester tester) async {
final GlobalKey<NavigatorState> navigatorKey = GlobalKey<NavigatorState>();
await tester.pumpWidget(
Directionality(
textDirection: TextDirection.ltr,
child: Navigator(
key: navigatorKey,
onGenerateRoute: (_) => TestRoute('A'),
),
),
);
final NavigatorState host = navigatorKey.currentState;
await runNavigatorTest(
tester,
host,
() { },
<String>[
'A: install',
'A: didAdd',
'A: didChangeNext null',
],
);
await runNavigatorTest(
tester,
host,
() { host.push(TestRoute('B')); },
<String>[
'B: install',
'B: didPush',
'B: didChangeNext null',
'A: didChangeNext B',
],
);
TestRoute routeC;
await runNavigatorTest(
tester,
host,
() { host.push(routeC = TestRoute('C')); },
<String>[
'C: install',
'C: didPush',
'C: didChangeNext null',
'B: didChangeNext C',
],
);
expect(routeC.isActive, isTrue);
TestRoute routeB;
await runNavigatorTest(
tester,
host,
() { host.replaceRouteBelow(anchorRoute: routeC, newRoute: routeB = TestRoute('b')); },
<String>[
'b: install',
'b: didReplace B',
'b: didChangeNext C',
'A: didChangeNext b',
'B: dispose',
],
);
await runNavigatorTest(
tester,
host,
() { host.popUntil((Route<dynamic> route) => route == routeB); },
<String>[
'C: didPop null',
'b: didPopNext C',
'C: dispose',
],
);
await tester.pumpWidget(Container());
expect(results, equals(<String>['A: dispose', 'b: dispose']));
expect(routes.isEmpty, isTrue);
results.clear();
});
testWidgets('Route localHistory - popUntil', (WidgetTester tester) async {
final TestRoute routeA = TestRoute('A');
routeA.addLocalHistoryEntry(LocalHistoryEntry(
onRemove: () { routeA.log('onRemove 0'); }
));
routeA.addLocalHistoryEntry(LocalHistoryEntry(
onRemove: () { routeA.log('onRemove 1'); }
));
final GlobalKey<NavigatorState> navigatorKey = GlobalKey<NavigatorState>();
await tester.pumpWidget(
Directionality(
textDirection: TextDirection.ltr,
child: Navigator(
key: navigatorKey,
onGenerateRoute: (_) => routeA,
),
),
);
final NavigatorState host = navigatorKey.currentState;
await runNavigatorTest(
tester,
host,
() { host.popUntil((Route<dynamic> route) => !route.willHandlePopInternally); },
<String>[
'A: install',
'A: didAdd',
'A: didChangeNext null',
'A: didPop null',
'A: onRemove 1',
'A: didPop null',
'A: onRemove 0',
],
);
await runNavigatorTest(
tester,
host,
() { host.popUntil((Route<dynamic> route) => !route.willHandlePopInternally); },
<String>[
],
);
});
group('PageRouteObserver', () {
test('calls correct listeners', () {
final RouteObserver<PageRoute<dynamic>> observer = RouteObserver<PageRoute<dynamic>>();
final RouteAware pageRouteAware1 = MockRouteAware();
final MockPageRoute route1 = MockPageRoute();
observer.subscribe(pageRouteAware1, route1);
verify(pageRouteAware1.didPush()).called(1);
final RouteAware pageRouteAware2 = MockRouteAware();
final MockPageRoute route2 = MockPageRoute();
observer.didPush(route2, route1);
verify(pageRouteAware1.didPushNext()).called(1);
observer.subscribe(pageRouteAware2, route2);
verify(pageRouteAware2.didPush()).called(1);
observer.didPop(route2, route1);
verify(pageRouteAware2.didPop()).called(1);
verify(pageRouteAware1.didPopNext()).called(1);
});
test('does not call listeners for non-PageRoute', () {
final RouteObserver<PageRoute<dynamic>> observer = RouteObserver<PageRoute<dynamic>>();
final RouteAware pageRouteAware = MockRouteAware();
final MockPageRoute pageRoute = MockPageRoute();
final MockRoute route = MockRoute();
observer.subscribe(pageRouteAware, pageRoute);
verify(pageRouteAware.didPush());
observer.didPush(route, pageRoute);
observer.didPop(route, pageRoute);
verifyNoMoreInteractions(pageRouteAware);
});
test('does not call listeners when already subscribed', () {
final RouteObserver<PageRoute<dynamic>> observer = RouteObserver<PageRoute<dynamic>>();
final RouteAware pageRouteAware = MockRouteAware();
final MockPageRoute pageRoute = MockPageRoute();
observer.subscribe(pageRouteAware, pageRoute);
observer.subscribe(pageRouteAware, pageRoute);
verify(pageRouteAware.didPush()).called(1);
});
test('does not call listeners when unsubscribed', () {
final RouteObserver<PageRoute<dynamic>> observer = RouteObserver<PageRoute<dynamic>>();
final RouteAware pageRouteAware = MockRouteAware();
final MockPageRoute pageRoute = MockPageRoute();
final MockPageRoute nextPageRoute = MockPageRoute();
observer.subscribe(pageRouteAware, pageRoute);
observer.subscribe(pageRouteAware, nextPageRoute);
verify(pageRouteAware.didPush()).called(2);
observer.unsubscribe(pageRouteAware);
observer.didPush(nextPageRoute, pageRoute);
observer.didPop(nextPageRoute, pageRoute);
verifyNoMoreInteractions(pageRouteAware);
});
});
testWidgets('Can autofocus a TextField nested in a Focus in a route.', (WidgetTester tester) async {
final TextEditingController controller = TextEditingController();
final FocusNode focusNode = FocusNode(debugLabel: 'Test Node');
await tester.pumpWidget(
Material(
child: MaterialApp(
onGenerateRoute: (RouteSettings settings) {
return PageRouteBuilder<void>(
settings: settings,
pageBuilder: (BuildContext context, Animation<double> input, Animation<double> out) {
return Focus(
child: TextField(
autofocus: true,
focusNode: focusNode,
controller: controller,
),
);
},
);
},
),
),
);
await tester.pump();
expect(focusNode.hasPrimaryFocus, isTrue);
});
group('TransitionRoute', () {
testWidgets('secondary animation is kDismissed when next route finishes pop', (WidgetTester tester) async {
final GlobalKey<NavigatorState> navigator = GlobalKey<NavigatorState>();
await tester.pumpWidget(
MaterialApp(
navigatorKey: navigator,
home: const Text('home'),
)
);
// Push page one, its secondary animation is kAlwaysDismissedAnimation.
ProxyAnimation secondaryAnimationProxyPageOne;
ProxyAnimation animationPageOne;
navigator.currentState.push(
PageRouteBuilder<void>(
pageBuilder: (_, Animation<double> animation, Animation<double> secondaryAnimation) {
secondaryAnimationProxyPageOne = secondaryAnimation as ProxyAnimation;
animationPageOne = animation as ProxyAnimation;
return const Text('Page One');
},
),
);
await tester.pump();
await tester.pumpAndSettle();
final ProxyAnimation secondaryAnimationPageOne = secondaryAnimationProxyPageOne.parent as ProxyAnimation;
expect(animationPageOne.value, 1.0);
expect(secondaryAnimationPageOne.parent, kAlwaysDismissedAnimation);
// Push page two, the secondary animation of page one is the primary
// animation of page two.
ProxyAnimation secondaryAnimationProxyPageTwo;
ProxyAnimation animationPageTwo;
navigator.currentState.push(
PageRouteBuilder<void>(
pageBuilder: (_, Animation<double> animation, Animation<double> secondaryAnimation) {
secondaryAnimationProxyPageTwo = secondaryAnimation as ProxyAnimation;
animationPageTwo = animation as ProxyAnimation;
return const Text('Page Two');
},
),
);
await tester.pump();
await tester.pumpAndSettle();
final ProxyAnimation secondaryAnimationPageTwo = secondaryAnimationProxyPageTwo.parent as ProxyAnimation;
expect(animationPageTwo.value, 1.0);
expect(secondaryAnimationPageTwo.parent, kAlwaysDismissedAnimation);
expect(secondaryAnimationPageOne.parent, animationPageTwo.parent);
// Pop page two, the secondary animation of page one becomes
// kAlwaysDismissedAnimation.
navigator.currentState.pop();
await tester.pump();
await tester.pump(const Duration(milliseconds: 100));
expect(secondaryAnimationPageOne.parent, animationPageTwo.parent);
await tester.pumpAndSettle();
expect(animationPageTwo.value, 0.0);
expect(secondaryAnimationPageOne.parent, kAlwaysDismissedAnimation);
});
testWidgets('secondary animation is kDismissed when next route is removed', (WidgetTester tester) async {
final GlobalKey<NavigatorState> navigator = GlobalKey<NavigatorState>();
await tester.pumpWidget(
MaterialApp(
navigatorKey: navigator,
home: const Text('home'),
)
);
// Push page one, its secondary animation is kAlwaysDismissedAnimation.
ProxyAnimation secondaryAnimationProxyPageOne;
ProxyAnimation animationPageOne;
navigator.currentState.push(
PageRouteBuilder<void>(
pageBuilder: (_, Animation<double> animation, Animation<double> secondaryAnimation) {
secondaryAnimationProxyPageOne = secondaryAnimation as ProxyAnimation;
animationPageOne = animation as ProxyAnimation;
return const Text('Page One');
},
),
);
await tester.pump();
await tester.pumpAndSettle();
final ProxyAnimation secondaryAnimationPageOne = secondaryAnimationProxyPageOne.parent as ProxyAnimation;
expect(animationPageOne.value, 1.0);
expect(secondaryAnimationPageOne.parent, kAlwaysDismissedAnimation);
// Push page two, the secondary animation of page one is the primary
// animation of page two.
ProxyAnimation secondaryAnimationProxyPageTwo;
ProxyAnimation animationPageTwo;
Route<void> secondRoute;
navigator.currentState.push(
secondRoute = PageRouteBuilder<void>(
pageBuilder: (_, Animation<double> animation, Animation<double> secondaryAnimation) {
secondaryAnimationProxyPageTwo = secondaryAnimation as ProxyAnimation;
animationPageTwo = animation as ProxyAnimation;
return const Text('Page Two');
},
),
);
await tester.pump();
await tester.pumpAndSettle();
final ProxyAnimation secondaryAnimationPageTwo = secondaryAnimationProxyPageTwo.parent as ProxyAnimation;
expect(animationPageTwo.value, 1.0);
expect(secondaryAnimationPageTwo.parent, kAlwaysDismissedAnimation);
expect(secondaryAnimationPageOne.parent, animationPageTwo.parent);
// Remove the second route, the secondary animation of page one is
// kAlwaysDismissedAnimation again.
navigator.currentState.removeRoute(secondRoute);
await tester.pump();
expect(secondaryAnimationPageOne.parent, kAlwaysDismissedAnimation);
});
testWidgets('secondary animation is kDismissed after train hopping finishes and pop', (WidgetTester tester) async {
final GlobalKey<NavigatorState> navigator = GlobalKey<NavigatorState>();
await tester.pumpWidget(
MaterialApp(
navigatorKey: navigator,
home: const Text('home'),
)
);
// Push page one, its secondary animation is kAlwaysDismissedAnimation.
ProxyAnimation secondaryAnimationProxyPageOne;
ProxyAnimation animationPageOne;
navigator.currentState.push(
PageRouteBuilder<void>(
pageBuilder: (_, Animation<double> animation, Animation<double> secondaryAnimation) {
secondaryAnimationProxyPageOne = secondaryAnimation as ProxyAnimation;
animationPageOne = animation as ProxyAnimation;
return const Text('Page One');
},
),
);
await tester.pump();
await tester.pumpAndSettle();
final ProxyAnimation secondaryAnimationPageOne = secondaryAnimationProxyPageOne.parent as ProxyAnimation;
expect(animationPageOne.value, 1.0);
expect(secondaryAnimationPageOne.parent, kAlwaysDismissedAnimation);
// Push page two, the secondary animation of page one is the primary
// animation of page two.
ProxyAnimation animationPageTwo;
navigator.currentState.push(
PageRouteBuilder<void>(
pageBuilder: (_, Animation<double> animation, Animation<double> secondaryAnimation) {
animationPageTwo = animation as ProxyAnimation;
return const Text('Page Two');
},
),
);
await tester.pump();
await tester.pump(const Duration(milliseconds: 100));
expect(secondaryAnimationPageOne.parent, animationPageTwo.parent);
// Replace with a different route while push is ongoing to trigger
// TrainHopping.
ProxyAnimation animationPageThree;
navigator.currentState.pushReplacement(
TestPageRouteBuilder(
pageBuilder: (_, Animation<double> animation, Animation<double> secondaryAnimation) {
animationPageThree = animation as ProxyAnimation;
return const Text('Page Three');
},
),
);
await tester.pump();
await tester.pump(const Duration(milliseconds: 1));
expect(secondaryAnimationPageOne.parent, isA<TrainHoppingAnimation>());
final TrainHoppingAnimation trainHopper = secondaryAnimationPageOne.parent as TrainHoppingAnimation;
expect(trainHopper.currentTrain, animationPageTwo.parent);
await tester.pump(const Duration(milliseconds: 100));
expect(secondaryAnimationPageOne.parent, isNot(isA<TrainHoppingAnimation>()));
expect(secondaryAnimationPageOne.parent, animationPageThree.parent);
expect(trainHopper.currentTrain, isNull); // Has been disposed.
await tester.pumpAndSettle();
expect(secondaryAnimationPageOne.parent, animationPageThree.parent);
// Pop page three.
navigator.currentState.pop();
await tester.pump();
await tester.pumpAndSettle();
expect(secondaryAnimationPageOne.parent, kAlwaysDismissedAnimation);
});
testWidgets('secondary animation is kDismissed when train hopping is interrupted', (WidgetTester tester) async {
final GlobalKey<NavigatorState> navigator = GlobalKey<NavigatorState>();
await tester.pumpWidget(
MaterialApp(
navigatorKey: navigator,
home: const Text('home'),
)
);
// Push page one, its secondary animation is kAlwaysDismissedAnimation.
ProxyAnimation secondaryAnimationProxyPageOne;
ProxyAnimation animationPageOne;
navigator.currentState.push(
PageRouteBuilder<void>(
pageBuilder: (_, Animation<double> animation, Animation<double> secondaryAnimation) {
secondaryAnimationProxyPageOne = secondaryAnimation as ProxyAnimation;
animationPageOne = animation as ProxyAnimation;
return const Text('Page One');
},
),
);
await tester.pump();
await tester.pumpAndSettle();
final ProxyAnimation secondaryAnimationPageOne = secondaryAnimationProxyPageOne.parent as ProxyAnimation;
expect(animationPageOne.value, 1.0);
expect(secondaryAnimationPageOne.parent, kAlwaysDismissedAnimation);
// Push page two, the secondary animation of page one is the primary
// animation of page two.
ProxyAnimation animationPageTwo;
navigator.currentState.push(
PageRouteBuilder<void>(
pageBuilder: (_, Animation<double> animation, Animation<double> secondaryAnimation) {
animationPageTwo = animation as ProxyAnimation;
return const Text('Page Two');
},
),
);
await tester.pump();
await tester.pump(const Duration(milliseconds: 100));
expect(secondaryAnimationPageOne.parent, animationPageTwo.parent);
// Replace with a different route while push is ongoing to trigger
// TrainHopping.
navigator.currentState.pushReplacement(
TestPageRouteBuilder(
pageBuilder: (_, Animation<double> animation, Animation<double> secondaryAnimation) {
return const Text('Page Three');
},
),
);
await tester.pump();
await tester.pump(const Duration(milliseconds: 10));
expect(secondaryAnimationPageOne.parent, isA<TrainHoppingAnimation>());
final TrainHoppingAnimation trainHopper = secondaryAnimationPageOne.parent as TrainHoppingAnimation;
expect(trainHopper.currentTrain, animationPageTwo.parent);
// Pop page three while replacement push is ongoing.
navigator.currentState.pop();
await tester.pump();
expect(secondaryAnimationPageOne.parent, isA<TrainHoppingAnimation>());
final TrainHoppingAnimation trainHopper2 = secondaryAnimationPageOne.parent as TrainHoppingAnimation;
expect(trainHopper2.currentTrain, animationPageTwo.parent);
expect(trainHopper.currentTrain, isNull); // Has been disposed.
await tester.pumpAndSettle();
expect(secondaryAnimationPageOne.parent, kAlwaysDismissedAnimation);
expect(trainHopper2.currentTrain, isNull); // Has been disposed.
});
testWidgets('secondary animation is triggered when pop initial route', (WidgetTester tester) async {
final GlobalKey<NavigatorState> navigator = GlobalKey<NavigatorState>();
Animation<double> secondaryAnimationOfRouteOne;
Animation<double> primaryAnimationOfRouteTwo;
await tester.pumpWidget(
MaterialApp(
navigatorKey: navigator,
onGenerateRoute: (RouteSettings settings) {
return PageRouteBuilder<void>(
settings: settings,
pageBuilder: (_, Animation<double> animation, Animation<double> secondaryAnimation) {
if (settings.name == '/')
secondaryAnimationOfRouteOne = secondaryAnimation;
else
primaryAnimationOfRouteTwo = animation;
return const Text('Page');
},
);
},
initialRoute: '/a',
)
);
// The secondary animation of the bottom route should be chained with the
// primary animation of top most route.
expect(secondaryAnimationOfRouteOne.value, 1.0);
expect(secondaryAnimationOfRouteOne.value, primaryAnimationOfRouteTwo.value);
// Pops the top most route and verifies two routes are still chained.
navigator.currentState.pop();
await tester.pump();
await tester.pump(const Duration(milliseconds: 30));
expect(secondaryAnimationOfRouteOne.value, 0.9);
expect(secondaryAnimationOfRouteOne.value, primaryAnimationOfRouteTwo.value);
await tester.pumpAndSettle();
expect(secondaryAnimationOfRouteOne.value, 0.0);
expect(secondaryAnimationOfRouteOne.value, primaryAnimationOfRouteTwo.value);
});
testWidgets('showGeneralDialog uses root navigator by default', (WidgetTester tester) async {
final DialogObserver rootObserver = DialogObserver();
final DialogObserver nestedObserver = DialogObserver();
await tester.pumpWidget(MaterialApp(
navigatorObservers: <NavigatorObserver>[rootObserver],
home: Navigator(
observers: <NavigatorObserver>[nestedObserver],
onGenerateRoute: (RouteSettings settings) {
return MaterialPageRoute<dynamic>(
builder: (BuildContext context) {
return RaisedButton(
onPressed: () {
showGeneralDialog<void>(
context: context,
barrierDismissible: false,
transitionDuration: Duration.zero,
pageBuilder: (BuildContext innerContext, _, __) {
return const SizedBox();
},
);
},
child: const Text('Show Dialog'),
);
},
);
},
),
));
// Open the dialog.
await tester.tap(find.byType(RaisedButton));
expect(rootObserver.dialogCount, 1);
expect(nestedObserver.dialogCount, 0);
});
testWidgets('showGeneralDialog uses nested navigator if useRootNavigator is false', (WidgetTester tester) async {
final DialogObserver rootObserver = DialogObserver();
final DialogObserver nestedObserver = DialogObserver();
await tester.pumpWidget(MaterialApp(
navigatorObservers: <NavigatorObserver>[rootObserver],
home: Navigator(
observers: <NavigatorObserver>[nestedObserver],
onGenerateRoute: (RouteSettings settings) {
return MaterialPageRoute<dynamic>(
builder: (BuildContext context) {
return RaisedButton(
onPressed: () {
showGeneralDialog<void>(
useRootNavigator: false,
context: context,
barrierDismissible: false,
transitionDuration: Duration.zero,
pageBuilder: (BuildContext innerContext, _, __) {
return const SizedBox();
},
);
},
child: const Text('Show Dialog'),
);
},
);
},
),
));
// Open the dialog.
await tester.tap(find.byType(RaisedButton));
expect(rootObserver.dialogCount, 0);
expect(nestedObserver.dialogCount, 1);
});
testWidgets('reverseTransitionDuration defaults to transitionDuration', (WidgetTester tester) async {
final GlobalKey containerKey = GlobalKey();
// Default MaterialPageRoute transition duration should be 300ms.
await tester.pumpWidget(MaterialApp(
onGenerateRoute: (RouteSettings settings) {
return MaterialPageRoute<dynamic>(
builder: (BuildContext context) {
return RaisedButton(
onPressed: () {
Navigator.of(context).push<void>(
MaterialPageRoute<void>(
builder: (BuildContext innerContext) {
return Container(
key: containerKey,
color: Colors.green,
);
},
),
);
},
child: const Text('Open page'),
);
},
);
},
));
// Open the new route.
await tester.tap(find.byType(RaisedButton));
await tester.pumpAndSettle();
expect(find.text('Open page'), findsNothing);
expect(find.byKey(containerKey), findsOneWidget);
// Pop the new route.
tester.state<NavigatorState>(find.byType(Navigator)).pop();
await tester.pump();
expect(find.byKey(containerKey), findsOneWidget);
// Container should be present halfway through the transition.
await tester.pump(const Duration(milliseconds: 150));
expect(find.byKey(containerKey), findsOneWidget);
// Container should be present at the very end of the transition.
await tester.pump(const Duration(milliseconds: 150));
expect(find.byKey(containerKey), findsOneWidget);
// Container have transitioned out after 300ms.
await tester.pump(const Duration(milliseconds: 1));
expect(find.byKey(containerKey), findsNothing);
});
testWidgets('reverseTransitionDuration can be customized', (WidgetTester tester) async {
final GlobalKey containerKey = GlobalKey();
await tester.pumpWidget(MaterialApp(
onGenerateRoute: (RouteSettings settings) {
return MaterialPageRoute<dynamic>(
builder: (BuildContext context) {
return RaisedButton(
onPressed: () {
Navigator.of(context).push<void>(
ModifiedReverseTransitionDurationRoute<void>(
builder: (BuildContext innerContext) {
return Container(
key: containerKey,
color: Colors.green,
);
},
// modified value, default MaterialPageRoute transition duration should be 300ms.
reverseTransitionDuration: const Duration(milliseconds: 150),
),
);
},
child: const Text('Open page'),
);
},
);
},
));
// Open the new route.
await tester.tap(find.byType(RaisedButton));
await tester.pumpAndSettle();
expect(find.text('Open page'), findsNothing);
expect(find.byKey(containerKey), findsOneWidget);
// Pop the new route.
tester.state<NavigatorState>(find.byType(Navigator)).pop();
await tester.pump();
expect(find.byKey(containerKey), findsOneWidget);
// Container should be present halfway through the transition.
await tester.pump(const Duration(milliseconds: 75));
expect(find.byKey(containerKey), findsOneWidget);
// Container should be present at the very end of the transition.
await tester.pump(const Duration(milliseconds: 75));
expect(find.byKey(containerKey), findsOneWidget);
// Container have transitioned out after 150ms.
await tester.pump(const Duration(milliseconds: 1));
expect(find.byKey(containerKey), findsNothing);
});
testWidgets('custom reverseTransitionDuration does not result in interrupted animations', (WidgetTester tester) async {
final GlobalKey containerKey = GlobalKey();
await tester.pumpWidget(MaterialApp(
theme: ThemeData(
pageTransitionsTheme: const PageTransitionsTheme(
builders: <TargetPlatform, PageTransitionsBuilder>{
TargetPlatform.android: FadeUpwardsPageTransitionsBuilder(), // use a fade transition
},
),
),
onGenerateRoute: (RouteSettings settings) {
return MaterialPageRoute<dynamic>(
builder: (BuildContext context) {
return RaisedButton(
onPressed: () {
Navigator.of(context).push<void>(
ModifiedReverseTransitionDurationRoute<void>(
builder: (BuildContext innerContext) {
return Container(
key: containerKey,
color: Colors.green,
);
},
// modified value, default MaterialPageRoute transition duration should be 300ms.
reverseTransitionDuration: const Duration(milliseconds: 150),
),
);
},
child: const Text('Open page'),
);
},
);
},
));
// Open the new route.
await tester.tap(find.byType(RaisedButton));
await tester.pump();
await tester.pump(const Duration(milliseconds: 200)); // jump partway through the forward transition
expect(find.byKey(containerKey), findsOneWidget);
// Gets the opacity of the fade transition while animating forwards.
final double topFadeTransitionOpacity = _getOpacity(containerKey, tester);
// Pop the new route mid-transition.
tester.state<NavigatorState>(find.byType(Navigator)).pop();
await tester.pump();
// Transition should not jump. In other words, the fade transition
// opacity before and after animation changes directions should remain
// the same.
expect(_getOpacity(containerKey, tester), topFadeTransitionOpacity);
// Reverse transition duration should be:
// Forward transition elapsed time: 200ms / 300ms = 2 / 3
// Reverse transition remaining time: 150ms * 2 / 3 = 100ms
// Container should be present at the very end of the transition.
await tester.pump(const Duration(milliseconds: 100));
expect(find.byKey(containerKey), findsOneWidget);
// Container have transitioned out after 100ms.
await tester.pump(const Duration(milliseconds: 1));
expect(find.byKey(containerKey), findsNothing);
});
});
group('ModalRoute', () {
testWidgets('default barrierCurve', (WidgetTester tester) async {
await tester.pumpWidget(MaterialApp(
home: Material(
child: Builder(
builder: (BuildContext context) {
return Center(
child: RaisedButton(
child: const Text('X'),
onPressed: () {
Navigator.of(context).push<void>(
_TestDialogRouteWithCustomBarrierCurve<void>(
child: const Text('Hello World'),
)
);
},
),
);
}
),
),
));
final CurveTween _defaultBarrierTween = CurveTween(curve: Curves.ease);
int _getExpectedBarrierTweenAlphaValue(double t) {
return Color.getAlphaFromOpacity(_defaultBarrierTween.transform(t));
}
await tester.tap(find.text('X'));
await tester.pump();
final Finder animatedModalBarrier = find.byType(AnimatedModalBarrier);
expect(animatedModalBarrier, findsOneWidget);
Animation<Color> modalBarrierAnimation;
modalBarrierAnimation = tester.widget<AnimatedModalBarrier>(animatedModalBarrier).color;
expect(modalBarrierAnimation.value, Colors.transparent);
await tester.pump(const Duration(milliseconds: 25));
modalBarrierAnimation = tester.widget<AnimatedModalBarrier>(animatedModalBarrier).color;
expect(
modalBarrierAnimation.value.alpha,
closeTo(_getExpectedBarrierTweenAlphaValue(0.25), 1.0),
);
await tester.pump(const Duration(milliseconds: 25));
modalBarrierAnimation = tester.widget<AnimatedModalBarrier>(animatedModalBarrier).color;
expect(
modalBarrierAnimation.value.alpha,
closeTo(_getExpectedBarrierTweenAlphaValue(0.50), 1.0),
);
await tester.pump(const Duration(milliseconds: 25));
modalBarrierAnimation = tester.widget<AnimatedModalBarrier>(animatedModalBarrier).color;
expect(
modalBarrierAnimation.value.alpha,
closeTo(_getExpectedBarrierTweenAlphaValue(0.75), 1.0),
);
await tester.pumpAndSettle();
modalBarrierAnimation = tester.widget<AnimatedModalBarrier>(animatedModalBarrier).color;
expect(modalBarrierAnimation.value, Colors.black);
});
testWidgets('custom barrierCurve', (WidgetTester tester) async {
await tester.pumpWidget(MaterialApp(
home: Material(
child: Builder(
builder: (BuildContext context) {
return Center(
child: RaisedButton(
child: const Text('X'),
onPressed: () {
Navigator.of(context).push<void>(
_TestDialogRouteWithCustomBarrierCurve<void>(
child: const Text('Hello World'),
barrierCurve: Curves.linear,
),
);
},
),
);
},
),
),
));
final CurveTween _customBarrierTween = CurveTween(curve: Curves.linear);
int _getExpectedBarrierTweenAlphaValue(double t) {
return Color.getAlphaFromOpacity(_customBarrierTween.transform(t));
}
await tester.tap(find.text('X'));
await tester.pump();
final Finder animatedModalBarrier = find.byType(AnimatedModalBarrier);
expect(animatedModalBarrier, findsOneWidget);
Animation<Color> modalBarrierAnimation;
modalBarrierAnimation = tester.widget<AnimatedModalBarrier>(animatedModalBarrier).color;
expect(modalBarrierAnimation.value, Colors.transparent);
await tester.pump(const Duration(milliseconds: 25));
modalBarrierAnimation = tester.widget<AnimatedModalBarrier>(animatedModalBarrier).color;
expect(
modalBarrierAnimation.value.alpha,
closeTo(_getExpectedBarrierTweenAlphaValue(0.25), 1.0),
);
await tester.pump(const Duration(milliseconds: 25));
modalBarrierAnimation = tester.widget<AnimatedModalBarrier>(animatedModalBarrier).color;
expect(
modalBarrierAnimation.value.alpha,
closeTo(_getExpectedBarrierTweenAlphaValue(0.50), 1.0),
);
await tester.pump(const Duration(milliseconds: 25));
modalBarrierAnimation = tester.widget<AnimatedModalBarrier>(animatedModalBarrier).color;
expect(
modalBarrierAnimation.value.alpha,
closeTo(_getExpectedBarrierTweenAlphaValue(0.75), 1.0),
);
await tester.pumpAndSettle();
modalBarrierAnimation = tester.widget<AnimatedModalBarrier>(animatedModalBarrier).color;
expect(modalBarrierAnimation.value, Colors.black);
});
testWidgets('focus traverse correct when pop mutiple page simultaneously', (WidgetTester tester) async {
// Regression test: https://github.com/flutter/flutter/issues/48903
final GlobalKey<NavigatorState> navigatorKey = GlobalKey<NavigatorState>();
await tester.pumpWidget(MaterialApp(
navigatorKey: navigatorKey,
home: const Text('dummy1'),
));
final Element textOnPageOne = tester.element(find.text('dummy1'));
final FocusScopeNode focusNodeOnPageOne = FocusScope.of(textOnPageOne);
expect(focusNodeOnPageOne.hasFocus, isTrue);
// Pushes one page.
navigatorKey.currentState.push<void>(
MaterialPageRoute<void>(
builder: (BuildContext context) => const Text('dummy2'),
)
);
await tester.pumpAndSettle();
final Element textOnPageTwo = tester.element(find.text('dummy2'));
final FocusScopeNode focusNodeOnPageTwo = FocusScope.of(textOnPageTwo);
// The focus should be on second page.
expect(focusNodeOnPageOne.hasFocus, isFalse);
expect(focusNodeOnPageTwo.hasFocus, isTrue);
// Pushes another page.
navigatorKey.currentState.push<void>(
MaterialPageRoute<void>(
builder: (BuildContext context) => const Text('dummy3'),
)
);
await tester.pumpAndSettle();
final Element textOnPageThree = tester.element(find.text('dummy3'));
final FocusScopeNode focusNodeOnPageThree = FocusScope.of(textOnPageThree);
// The focus should be on third page.
expect(focusNodeOnPageOne.hasFocus, isFalse);
expect(focusNodeOnPageTwo.hasFocus, isFalse);
expect(focusNodeOnPageThree.hasFocus, isTrue);
// Pops two pages simultaneously.
navigatorKey.currentState.popUntil((Route<void> route) => route.isFirst);
await tester.pumpAndSettle();
// It should refocus page one after pops.
expect(focusNodeOnPageOne.hasFocus, isTrue);
});
testWidgets('focus traversal is correct when popping mutiple pages simultaneously - with focused children', (WidgetTester tester) async {
// Regression test: https://github.com/flutter/flutter/issues/48903
final GlobalKey<NavigatorState> navigatorKey = GlobalKey<NavigatorState>();
await tester.pumpWidget(MaterialApp(
navigatorKey: navigatorKey,
home: const Text('dummy1'),
));
final Element textOnPageOne = tester.element(find.text('dummy1'));
final FocusScopeNode focusNodeOnPageOne = FocusScope.of(textOnPageOne);
expect(focusNodeOnPageOne.hasFocus, isTrue);
// Pushes one page.
navigatorKey.currentState.push<void>(
MaterialPageRoute<void>(
builder: (BuildContext context) => const Material(child: TextField()),
)
);
await tester.pumpAndSettle();
final Element textOnPageTwo = tester.element(find.byType(TextField));
final FocusScopeNode focusNodeOnPageTwo = FocusScope.of(textOnPageTwo);
// The focus should be on second page.
expect(focusNodeOnPageOne.hasFocus, isFalse);
expect(focusNodeOnPageTwo.hasFocus, isTrue);
// Move the focus to another node.
focusNodeOnPageTwo.nextFocus();
await tester.pumpAndSettle();
expect(focusNodeOnPageTwo.hasFocus, isTrue);
expect(focusNodeOnPageTwo.hasPrimaryFocus, isFalse);
// Pushes another page.
navigatorKey.currentState.push<void>(
MaterialPageRoute<void>(
builder: (BuildContext context) => const Text('dummy3'),
)
);
await tester.pumpAndSettle();
final Element textOnPageThree = tester.element(find.text('dummy3'));
final FocusScopeNode focusNodeOnPageThree = FocusScope.of(textOnPageThree);
// The focus should be on third page.
expect(focusNodeOnPageOne.hasFocus, isFalse);
expect(focusNodeOnPageTwo.hasFocus, isFalse);
expect(focusNodeOnPageThree.hasFocus, isTrue);
// Pops two pages simultaneously.
navigatorKey.currentState.popUntil((Route<void> route) => route.isFirst);
await tester.pumpAndSettle();
// It should refocus page one after pops.
expect(focusNodeOnPageOne.hasFocus, isTrue);
});
testWidgets('child with local history can be disposed', (WidgetTester tester) async {
// Regression test: https://github.com/flutter/flutter/issues/52478
await tester.pumpWidget(MaterialApp(
home: WidgetWithLocalHistory(),
));
final WidgetWithLocalHistoryState state = tester.state(find.byType(WidgetWithLocalHistory));
state.addLocalHistory();
// Waits for modal route to update its internal state;
await tester.pump();
// Pumps a new widget to dispose WidgetWithLocalHistory. This should cause
// it to remove the local history entry from modal route during
// finalizeTree.
await tester.pumpWidget(const MaterialApp(
home: Text('dummy'),
));
// Waits for modal route to update its internal state;
await tester.pump();
expect(tester.takeException(), null);
});
});
}
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;
});
}
class ModifiedReverseTransitionDurationRoute<T> extends MaterialPageRoute<T> {
ModifiedReverseTransitionDurationRoute({
@required WidgetBuilder builder,
RouteSettings settings,
this.reverseTransitionDuration,
bool fullscreenDialog = false,
}) : super(
builder: builder,
settings: settings,
fullscreenDialog: fullscreenDialog,
);
@override
final Duration reverseTransitionDuration;
}
class MockPageRoute extends Mock implements PageRoute<dynamic> { }
class MockRoute extends Mock implements Route<dynamic> { }
class MockRouteAware extends Mock implements RouteAware { }
class TestPageRouteBuilder extends PageRouteBuilder<void> {
TestPageRouteBuilder({RoutePageBuilder pageBuilder}) : super(pageBuilder: pageBuilder);
@override
Animation<double> createAnimation() {
return CurvedAnimation(parent: super.createAnimation(), curve: Curves.easeOutExpo);
}
}
class DialogObserver extends NavigatorObserver {
int dialogCount = 0;
@override
void didPush(Route<dynamic> route, Route<dynamic> previousRoute) {
if (route.toString().contains('_DialogRoute')) {
dialogCount++;
}
super.didPush(route, previousRoute);
}
}
class _TestDialogRouteWithCustomBarrierCurve<T> extends PopupRoute<T> {
_TestDialogRouteWithCustomBarrierCurve({
@required Widget child,
Curve barrierCurve,
}) : _barrierCurve = barrierCurve,
_child = child;
final Widget _child;
@override
bool get barrierDismissible => true;
@override
String get barrierLabel => null;
@override
Color get barrierColor => Colors.black; // easier value to test against
@override
Curve get barrierCurve {
if (_barrierCurve == null) {
return super.barrierCurve;
}
return _barrierCurve;
}
final Curve _barrierCurve;
@override
Duration get transitionDuration => const Duration(milliseconds: 100); // easier value to test against
@override
Widget buildPage(BuildContext context, Animation<double> animation, Animation<double> secondaryAnimation) {
return Semantics(
child: _child,
scopesRoute: true,
explicitChildNodes: true,
);
}
}
class WidgetWithLocalHistory extends StatefulWidget {
@override
WidgetWithLocalHistoryState createState() => WidgetWithLocalHistoryState();
}
class WidgetWithLocalHistoryState extends State<WidgetWithLocalHistory> {
LocalHistoryEntry _localHistory;
void addLocalHistory() {
final ModalRoute<dynamic> route = ModalRoute.of(context);
_localHistory = LocalHistoryEntry();
route.addLocalHistoryEntry(_localHistory);
}
@override
void dispose() {
super.dispose();
_localHistory.remove();
}
@override
Widget build(BuildContext context) {
return const Text('dummy');
}
}