1854 lines
64 KiB
Dart
1854 lines
64 KiB
Dart
// 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/services.dart';
|
|
import 'package:flutter/widgets.dart';
|
|
import 'package:flutter_test/flutter_test.dart';
|
|
|
|
import '../flutter_test_alternative.dart' show Fake;
|
|
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,
|
|
) 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',
|
|
],
|
|
);
|
|
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',
|
|
'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',
|
|
],
|
|
);
|
|
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',
|
|
'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',
|
|
'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',
|
|
'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('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('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>>());
|
|
});
|
|
}
|
|
|
|
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.toString().contains('_DialogRoute')) {
|
|
dialogRoutes.add(route as ModalRoute<dynamic>);
|
|
dialogCount++;
|
|
}
|
|
super.didPush(route, previousRoute);
|
|
}
|
|
|
|
@override
|
|
void didPop(Route<dynamic> route, Route<dynamic>? previousRoute) {
|
|
if (route.toString().contains('_DialogRoute')) {
|
|
dialogRoutes.removeLast();
|
|
dialogCount--;
|
|
}
|
|
super.didPop(route, previousRoute);
|
|
}
|
|
}
|
|
|
|
class _TestDialogRouteWithCustomBarrierCurve<T> extends PopupRoute<T> {
|
|
_TestDialogRouteWithCustomBarrierCurve({
|
|
required Widget child,
|
|
this.barrierLabel,
|
|
Curve? barrierCurve,
|
|
}) : _barrierCurve = barrierCurve,
|
|
_child = child;
|
|
|
|
final Widget _child;
|
|
|
|
@override
|
|
bool get barrierDismissible => true;
|
|
|
|
@override
|
|
final String? barrierLabel;
|
|
|
|
@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> {
|
|
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>(),
|
|
),
|
|
),
|
|
),
|
|
);
|
|
}
|