blob: 0d756b4f824e9731c2b4d90037679e2d6ce21a0d [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/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_test/flutter_test.dart';
import 'semantics_tester.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, [
List<String> expectationsAfterAnotherPump = const <String>[],
]) async {
expect(host, isNotNull);
test();
expect(results, equals(expectations));
results.clear();
await tester.pump();
expect(results, equals(expectationsAfterAnotherPump));
results.clear();
}
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',
],
);
late 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',
],
<String>[
'third: dispose',
],
);
await runNavigatorTest(
tester,
host,
() { host.pop('good bye'); },
<String>[ // stack is: initial
'two: didPop good bye',
'initial: didPopNext two',
],
<String>[
'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',
],
);
late 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',
],
<String>[
'third: dispose',
],
);
await runNavigatorTest(
tester,
host,
() { host.push(TestRoute('three')); },
<String>[
'three: install',
'three: didPush',
'three: didChangeNext null',
'second: didChangeNext three',
],
);
late 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',
],
<String>[
'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',
],
);
late 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);
late 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',
],
<String>[
'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 MockRouteAware pageRouteAware1 = MockRouteAware();
final MockPageRoute route1 = MockPageRoute();
observer.subscribe(pageRouteAware1, route1);
expect(pageRouteAware1.didPushCount, 1);
final MockRouteAware pageRouteAware2 = MockRouteAware();
final MockPageRoute route2 = MockPageRoute();
observer.didPush(route2, route1);
expect(pageRouteAware1.didPushNextCount, 1);
observer.subscribe(pageRouteAware2, route2);
expect(pageRouteAware2.didPushCount, 1);
observer.didPop(route2, route1);
expect(pageRouteAware2.didPopCount, 1);
expect(pageRouteAware1.didPopNextCount, 1);
});
test('does not call listeners for non-PageRoute', () {
final RouteObserver<PageRoute<dynamic>> observer = RouteObserver<PageRoute<dynamic>>();
final MockRouteAware pageRouteAware = MockRouteAware();
final MockPageRoute pageRoute = MockPageRoute();
final MockRoute route = MockRoute();
observer.subscribe(pageRouteAware, pageRoute);
expect(pageRouteAware.didPushCount, 1);
observer.didPush(route, pageRoute);
observer.didPop(route, pageRoute);
expect(pageRouteAware.didPushCount, 1);
expect(pageRouteAware.didPopCount, 0);
});
test('does not call listeners when already subscribed', () {
final RouteObserver<PageRoute<dynamic>> observer = RouteObserver<PageRoute<dynamic>>();
final MockRouteAware pageRouteAware = MockRouteAware();
final MockPageRoute pageRoute = MockPageRoute();
observer.subscribe(pageRouteAware, pageRoute);
observer.subscribe(pageRouteAware, pageRoute);
expect(pageRouteAware.didPushCount, 1);
});
test('does not call listeners when unsubscribed', () {
final RouteObserver<PageRoute<dynamic>> observer = RouteObserver<PageRoute<dynamic>>();
final MockRouteAware pageRouteAware = MockRouteAware();
final MockPageRoute pageRoute = MockPageRoute();
final MockPageRoute nextPageRoute = MockPageRoute();
observer.subscribe(pageRouteAware, pageRoute);
observer.subscribe(pageRouteAware, nextPageRoute);
expect(pageRouteAware.didPushCount, 2);
observer.unsubscribe(pageRouteAware);
observer.didPush(nextPageRoute, pageRoute);
observer.didPop(nextPageRoute, pageRoute);
expect(pageRouteAware.didPushCount, 2);
expect(pageRouteAware.didPopCount, 0);
});
});
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('PageRouteBuilder', () {
testWidgets('reverseTransitionDuration defaults to 300ms', (WidgetTester tester) async {
// Default PageRouteBuilder reverse transition duration should be 300ms.
await tester.pumpWidget(
MaterialApp(
onGenerateRoute: (RouteSettings settings) {
return MaterialPageRoute<dynamic>(
builder: (BuildContext context) {
return ElevatedButton(
onPressed: () {
Navigator.of(context).push<void>(
PageRouteBuilder<void>(
settings: settings,
pageBuilder: (BuildContext context, Animation<double> input, Animation<double> out) {
return const Text('Page Two');
},
)
);
},
child: const Text('Open page'),
);
},
);
},
)
);
// Open the new route.
await tester.tap(find.byType(ElevatedButton));
await tester.pumpAndSettle();
expect(find.text('Open page'), findsNothing);
expect(find.text('Page Two'), findsOneWidget);
// Pop the new route.
tester.state<NavigatorState>(find.byType(Navigator)).pop();
await tester.pump();
expect(find.text('Page Two'), findsOneWidget);
// Text('Page Two') should be present halfway through the reverse transition.
await tester.pump(const Duration(milliseconds: 150));
expect(find.text('Page Two'), findsOneWidget);
// Text('Page Two') should be present at the very end of the reverse transition.
await tester.pump(const Duration(milliseconds: 150));
expect(find.text('Page Two'), findsOneWidget);
// Text('Page Two') have transitioned out after 300ms.
await tester.pump(const Duration(milliseconds: 1));
expect(find.text('Page Two'), findsNothing);
expect(find.text('Open page'), findsOneWidget);
});
testWidgets('reverseTransitionDuration can be customized', (WidgetTester tester) async {
await tester.pumpWidget(MaterialApp(
onGenerateRoute: (RouteSettings settings) {
return MaterialPageRoute<dynamic>(
builder: (BuildContext context) {
return ElevatedButton(
onPressed: () {
Navigator.of(context).push<void>(
PageRouteBuilder<void>(
settings: settings,
pageBuilder: (BuildContext context, Animation<double> input, Animation<double> out) {
return const Text('Page Two');
},
// modified value, default PageRouteBuilder reverse transition duration should be 300ms.
reverseTransitionDuration: const Duration(milliseconds: 150),
)
);
},
child: const Text('Open page'),
);
},
);
})
);
// Open the new route.
await tester.tap(find.byType(ElevatedButton));
await tester.pumpAndSettle();
expect(find.text('Open page'), findsNothing);
expect(find.text('Page Two'), findsOneWidget);
// Pop the new route.
tester.state<NavigatorState>(find.byType(Navigator)).pop();
await tester.pump();
expect(find.text('Page Two'), findsOneWidget);
// Text('Page Two') should be present halfway through the reverse transition.
await tester.pump(const Duration(milliseconds: 75));
expect(find.text('Page Two'), findsOneWidget);
// Text('Page Two') should be present at the very end of the reverse transition.
await tester.pump(const Duration(milliseconds: 75));
expect(find.text('Page Two'), findsOneWidget);
// Text('Page Two') have transitioned out after 500ms.
await tester.pump(const Duration(milliseconds: 1));
expect(find.text('Page Two'), findsNothing);
expect(find.text('Open page'), findsOneWidget);
});
});
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.
late ProxyAnimation secondaryAnimationProxyPageOne;
late 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.
late ProxyAnimation secondaryAnimationProxyPageTwo;
late 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.
late ProxyAnimation secondaryAnimationProxyPageOne;
late 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.
late ProxyAnimation secondaryAnimationProxyPageTwo;
late 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.
late ProxyAnimation secondaryAnimationProxyPageOne;
late 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.
late 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.
late 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.
late ProxyAnimation secondaryAnimationProxyPageOne;
late 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.
late 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>();
late Animation<double> secondaryAnimationOfRouteOne;
late 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 handles transparent barrier color', (WidgetTester tester) async {
await tester.pumpWidget(MaterialApp(
home: Builder(
builder: (BuildContext context) {
return ElevatedButton(
onPressed: () {
showGeneralDialog<void>(
context: context,
barrierDismissible: true,
barrierLabel: 'barrier_label',
barrierColor: const Color(0x00000000),
transitionDuration: Duration.zero,
pageBuilder: (BuildContext innerContext, _, __) {
return const SizedBox();
},
);
},
child: const Text('Show Dialog'),
);
},
),
));
// Open the dialog.
await tester.tap(find.byType(ElevatedButton));
await tester.pump();
expect(find.byType(ModalBarrier), findsNWidgets(2));
// Close the dialog.
await tester.tapAt(Offset.zero);
await tester.pump();
expect(find.byType(ModalBarrier), findsNWidgets(1));
});
testWidgets('showGeneralDialog adds non-dismissable barrier when barrierDismissable is false', (WidgetTester tester) async {
await tester.pumpWidget(MaterialApp(
home: Builder(
builder: (BuildContext context) {
return ElevatedButton(
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(ElevatedButton));
await tester.pump();
expect(find.byType(ModalBarrier), findsNWidgets(2));
final ModalBarrier barrier = find.byType(ModalBarrier).evaluate().last.widget as ModalBarrier;
expect(barrier.dismissible, isFalse);
// Close the dialog.
final StatefulElement navigatorElement = find.byType(Navigator).evaluate().last as StatefulElement;
final NavigatorState navigatorState = navigatorElement.state as NavigatorState;
navigatorState.pop();
await tester.pumpAndSettle();
expect(find.byType(ModalBarrier), findsNWidgets(1));
});
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 ElevatedButton(
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(ElevatedButton));
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 ElevatedButton(
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(ElevatedButton));
expect(rootObserver.dialogCount, 0);
expect(nestedObserver.dialogCount, 1);
});
testWidgets('showGeneralDialog default argument values', (WidgetTester tester) async {
final DialogObserver rootObserver = DialogObserver();
await tester.pumpWidget(MaterialApp(
navigatorObservers: <NavigatorObserver>[rootObserver],
home: Navigator(
onGenerateRoute: (RouteSettings settings) {
return MaterialPageRoute<dynamic>(
builder: (BuildContext context) {
return ElevatedButton(
onPressed: () {
showGeneralDialog<void>(
context: context,
pageBuilder: (BuildContext innerContext, _, __) {
return const SizedBox();
},
);
},
child: const Text('Show Dialog'),
);
},
);
},
),
));
// Open the dialog.
await tester.tap(find.byType(ElevatedButton));
expect(rootObserver.dialogRoutes.length, equals(1));
final ModalRoute<dynamic> route = rootObserver.dialogRoutes.last;
expect(route.barrierDismissible, isNotNull);
expect(route.barrierColor, isNotNull);
expect(route.transitionDuration, isNotNull);
});
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 ElevatedButton(
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(ElevatedButton));
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 ElevatedButton(
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(ElevatedButton));
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 ElevatedButton(
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(ElevatedButton));
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: ElevatedButton(
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),
);
await tester.pump(const Duration(milliseconds: 25));
modalBarrierAnimation = tester.widget<AnimatedModalBarrier>(animatedModalBarrier).color;
expect(
modalBarrierAnimation.value!.alpha,
closeTo(_getExpectedBarrierTweenAlphaValue(0.50), 1),
);
await tester.pump(const Duration(milliseconds: 25));
modalBarrierAnimation = tester.widget<AnimatedModalBarrier>(animatedModalBarrier).color;
expect(
modalBarrierAnimation.value!.alpha,
closeTo(_getExpectedBarrierTweenAlphaValue(0.75), 1),
);
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: ElevatedButton(
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),
);
await tester.pump(const Duration(milliseconds: 25));
modalBarrierAnimation = tester.widget<AnimatedModalBarrier>(animatedModalBarrier).color;
expect(
modalBarrierAnimation.value!.alpha,
closeTo(_getExpectedBarrierTweenAlphaValue(0.50), 1),
);
await tester.pump(const Duration(milliseconds: 25));
modalBarrierAnimation = tester.widget<AnimatedModalBarrier>(animatedModalBarrier).color;
expect(
modalBarrierAnimation.value!.alpha,
closeTo(_getExpectedBarrierTweenAlphaValue(0.75), 1),
);
await tester.pumpAndSettle();
modalBarrierAnimation = tester.widget<AnimatedModalBarrier>(animatedModalBarrier).color;
expect(modalBarrierAnimation.value, Colors.black);
});
testWidgets('white barrierColor', (WidgetTester tester) async {
await tester.pumpWidget(MaterialApp(
home: Material(
child: Builder(
builder: (BuildContext context) {
return Center(
child: ElevatedButton(
child: const Text('X'),
onPressed: () {
Navigator.of(context).push<void>(
_TestDialogRouteWithCustomBarrierCurve<void>(
child: const Text('Hello World'),
barrierColor: Colors.white,
)
);
},
),
);
}
),
),
));
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.white.withOpacity(0));
await tester.pump(const Duration(milliseconds: 25));
modalBarrierAnimation = tester.widget<AnimatedModalBarrier>(animatedModalBarrier).color;
expect(
modalBarrierAnimation.value!.alpha,
closeTo(_getExpectedBarrierTweenAlphaValue(0.25), 1),
);
await tester.pump(const Duration(milliseconds: 25));
modalBarrierAnimation = tester.widget<AnimatedModalBarrier>(animatedModalBarrier).color;
expect(
modalBarrierAnimation.value!.alpha,
closeTo(_getExpectedBarrierTweenAlphaValue(0.50), 1),
);
await tester.pump(const Duration(milliseconds: 25));
modalBarrierAnimation = tester.widget<AnimatedModalBarrier>(animatedModalBarrier).color;
expect(
modalBarrierAnimation.value!.alpha,
closeTo(_getExpectedBarrierTweenAlphaValue(0.75), 1),
);
await tester.pumpAndSettle();
modalBarrierAnimation = tester.widget<AnimatedModalBarrier>(animatedModalBarrier).color;
expect(modalBarrierAnimation.value, Colors.white);
});
testWidgets('modal route semantics order', (WidgetTester tester) async {
// Regression test for https://github.com/flutter/flutter/issues/46625.
final SemanticsTester semantics = SemanticsTester(tester);
await tester.pumpWidget(MaterialApp(
home: Material(
child: Builder(
builder: (BuildContext context) {
return Center(
child: ElevatedButton(
child: const Text('X'),
onPressed: () {
Navigator.of(context).push<void>(
_TestDialogRouteWithCustomBarrierCurve<void>(
child: const Text('Hello World'),
barrierLabel: 'test label',
barrierCurve: Curves.linear,
),
);
},
),
);
},
),
),
));
await tester.tap(find.text('X'));
await tester.pumpAndSettle();
expect(find.text('Hello World'), findsOneWidget);
final TestSemantics expectedSemantics = TestSemantics.root(
children: <TestSemantics>[
TestSemantics.rootChild(
id: 1,
rect: TestSemantics.fullScreen,
children: <TestSemantics>[
TestSemantics(
id: 6,
rect: TestSemantics.fullScreen,
children: <TestSemantics>[
TestSemantics(
id: 7,
rect: TestSemantics.fullScreen,
flags: <SemanticsFlag>[SemanticsFlag.scopesRoute],
children: <TestSemantics>[
TestSemantics(
id: 8,
label: 'Hello World',
rect: TestSemantics.fullScreen,
textDirection: TextDirection.ltr,
),
],
),
],
),
// Modal barrier is put after modal scope
TestSemantics(
id: 5,
rect: TestSemantics.fullScreen,
actions: <SemanticsAction>[SemanticsAction.tap],
label: 'test label',
textDirection: TextDirection.ltr,
),
],
),
],
)
;
expect(semantics, hasSemantics(expectedSemantics));
semantics.dispose();
}, variant: const TargetPlatformVariant(<TargetPlatform>{TargetPlatform.iOS}));
testWidgets('focus traverse correct when pop multiple 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 multiple 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);
});
});
testWidgets('can be dismissed with escape keyboard shortcut', (WidgetTester tester) async {
final GlobalKey<NavigatorState> navigatorKey = GlobalKey<NavigatorState>();
await tester.pumpWidget(MaterialApp(
navigatorKey: navigatorKey,
home: const Text('dummy1'),
));
final Element textOnPageOne = tester.element(find.text('dummy1'));
// Show a simple dialog
showDialog<void>(
context: textOnPageOne,
builder: (BuildContext context) => const Text('dialog1'),
);
await tester.pumpAndSettle();
expect(find.text('dialog1'), findsOneWidget);
// Try to dismiss the dialog with the shortcut key
await tester.sendKeyEvent(LogicalKeyboardKey.escape);
await tester.pumpAndSettle();
expect(find.text('dialog1'), findsNothing);
});
testWidgets('can not be dismissed with escape keyboard shortcut if barrier not dismissible', (WidgetTester tester) async {
final GlobalKey<NavigatorState> navigatorKey = GlobalKey<NavigatorState>();
await tester.pumpWidget(MaterialApp(
navigatorKey: navigatorKey,
home: const Text('dummy1'),
));
final Element textOnPageOne = tester.element(find.text('dummy1'));
// Show a simple dialog
showDialog<void>(
context: textOnPageOne,
barrierDismissible: false,
builder: (BuildContext context) => const Text('dialog1'),
);
await tester.pumpAndSettle();
expect(find.text('dialog1'), findsOneWidget);
// Try to dismiss the dialog with the shortcut key
await tester.sendKeyEvent(LogicalKeyboardKey.escape);
await tester.pumpAndSettle();
expect(find.text('dialog1'), findsOneWidget);
});
testWidgets('ModalRoute.of works for void routes', (WidgetTester tester) async {
final GlobalKey<NavigatorState> navigatorKey = GlobalKey<NavigatorState>();
await tester.pumpWidget(MaterialApp(
navigatorKey: navigatorKey,
home: const Text('home'),
));
expect(find.text('page2'), findsNothing);
navigatorKey.currentState!.push<void>(MaterialPageRoute<void>(
builder: (BuildContext context) {
return const Text('page2');
}
));
await tester.pumpAndSettle();
expect(find.text('page2'), findsOneWidget);
final ModalRoute<void>? parentRoute = ModalRoute.of<void>(tester.element(find.text('page2')));
expect(parentRoute, isNotNull);
expect(parentRoute, isA<MaterialPageRoute<void>>());
});
testWidgets('RawDialogRoute is state restorable', (WidgetTester tester) async {
await tester.pumpWidget(
MaterialApp(
restorationScopeId: 'app',
home: _RestorableDialogTestWidget(),
),
);
expect(find.byType(AlertDialog), findsNothing);
await tester.tap(find.text('X'));
await tester.pumpAndSettle();
expect(find.byType(AlertDialog), findsOneWidget);
final TestRestorationData restorationData = await tester.getRestorationData();
await tester.restartAndRestore();
expect(find.byType(AlertDialog), findsOneWidget);
// Tap on the barrier.
await tester.tapAt(const Offset(10.0, 10.0));
await tester.pumpAndSettle();
expect(find.byType(AlertDialog), findsNothing);
await tester.restoreFrom(restorationData);
expect(find.byType(AlertDialog), findsOneWidget);
}, skip: isBrowser); // https://github.com/flutter/flutter/issues/33615
}
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,
required this.reverseTransitionDuration,
bool fullscreenDialog = false,
}) : super(
builder: builder,
settings: settings,
fullscreenDialog: fullscreenDialog,
);
@override
final Duration reverseTransitionDuration;
}
class MockPageRoute extends Fake implements PageRoute<dynamic> { }
class MockRoute extends Fake implements Route<dynamic> { }
class MockRouteAware extends Fake implements RouteAware {
int didPushCount = 0;
int didPushNextCount = 0;
int didPopCount = 0;
int didPopNextCount = 0;
@override
void didPush() {
didPushCount += 1;
}
@override
void didPushNext() {
didPushNextCount += 1;
}
@override
void didPop() {
didPopCount += 1;
}
@override
void didPopNext() {
didPopNextCount += 1;
}
}
class TestPageRouteBuilder extends PageRouteBuilder<void> {
TestPageRouteBuilder({required RoutePageBuilder pageBuilder}) : super(pageBuilder: pageBuilder);
@override
Animation<double> createAnimation() {
return CurvedAnimation(parent: super.createAnimation(), curve: Curves.easeOutExpo);
}
}
class DialogObserver extends NavigatorObserver {
final List<ModalRoute<dynamic>> dialogRoutes = <ModalRoute<dynamic>>[];
int dialogCount = 0;
@override
void didPush(Route<dynamic> route, Route<dynamic>? previousRoute) {
if (route is RawDialogRoute) {
dialogRoutes.add(route);
dialogCount++;
}
super.didPush(route, previousRoute);
}
@override
void didPop(Route<dynamic> route, Route<dynamic>? previousRoute) {
if (route is RawDialogRoute) {
dialogRoutes.removeLast();
dialogCount--;
}
super.didPop(route, previousRoute);
}
}
class _TestDialogRouteWithCustomBarrierCurve<T> extends PopupRoute<T> {
_TestDialogRouteWithCustomBarrierCurve({
required Widget child,
this.barrierLabel,
this.barrierColor = Colors.black,
Curve? barrierCurve,
}) : _barrierCurve = barrierCurve,
_child = child;
final Widget _child;
@override
bool get barrierDismissible => true;
@override
final String? barrierLabel;
@override
final Color? barrierColor;
@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> {
late 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');
}
}
class TransitionDetector extends DefaultTransitionDelegate<void> {
bool hasTransition = false;
@override
Iterable<RouteTransitionRecord> resolve({
required List<RouteTransitionRecord> newPageRouteHistory,
required Map<RouteTransitionRecord?, RouteTransitionRecord> locationToExitingPageRoute,
required Map<RouteTransitionRecord?, List<RouteTransitionRecord>> pageRouteToPagelessRoutes
}) {
hasTransition = true;
return super.resolve(
newPageRouteHistory: newPageRouteHistory,
locationToExitingPageRoute: locationToExitingPageRoute,
pageRouteToPagelessRoutes: pageRouteToPagelessRoutes
);
}
}
Widget buildNavigator({
required List<Page<dynamic>> pages,
required PopPageCallback onPopPage,
GlobalKey<NavigatorState>? key,
TransitionDelegate<dynamic>? transitionDelegate
}) {
return MediaQuery(
data: MediaQueryData.fromWindow(WidgetsBinding.instance!.window),
child: Localizations(
locale: const Locale('en', 'US'),
delegates: const <LocalizationsDelegate<dynamic>>[
DefaultMaterialLocalizations.delegate,
DefaultWidgetsLocalizations.delegate
],
child: Directionality(
textDirection: TextDirection.ltr,
child: Navigator(
key: key,
pages: pages,
onPopPage: onPopPage,
transitionDelegate: transitionDelegate ?? const DefaultTransitionDelegate<dynamic>(),
),
),
),
);
}
class _RestorableDialogTestWidget extends StatelessWidget {
static Route<Object?> _dialogBuilder(BuildContext context, Object? arguments) {
return RawDialogRoute<void>(
pageBuilder: (
BuildContext context,
Animation<double> animation,
Animation<double> secondaryAnimation,
) {
return const AlertDialog(title: Text('Alert!'));
},
);
}
@override
Widget build(BuildContext context) {
return Scaffold(
body: Center(
child: OutlinedButton(
onPressed: () {
Navigator.of(context).restorablePush(_dialogBuilder);
},
child: const Text('X'),
),
),
);
}
}