| // 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:ui'; |
| |
| import 'package:flutter/foundation.dart'; |
| import 'package:flutter/scheduler.dart'; |
| import 'package:flutter_test/flutter_test.dart'; |
| import 'package:flutter/material.dart'; |
| |
| import 'observer_tester.dart'; |
| import 'semantics_tester.dart'; |
| |
| class FirstWidget extends StatelessWidget { |
| const FirstWidget({ Key? key }) : super(key: key); |
| @override |
| Widget build(BuildContext context) { |
| return GestureDetector( |
| onTap: () { |
| Navigator.pushNamed(context, '/second'); |
| }, |
| child: Container( |
| color: const Color(0xFFFFFF00), |
| child: const Text('X'), |
| ), |
| ); |
| } |
| } |
| |
| class SecondWidget extends StatefulWidget { |
| const SecondWidget({ Key? key }) : super(key: key); |
| @override |
| SecondWidgetState createState() => SecondWidgetState(); |
| } |
| |
| class SecondWidgetState extends State<SecondWidget> { |
| @override |
| Widget build(BuildContext context) { |
| return GestureDetector( |
| onTap: () => Navigator.pop(context), |
| child: Container( |
| color: const Color(0xFFFF00FF), |
| child: const Text('Y'), |
| ), |
| ); |
| } |
| } |
| |
| typedef ExceptionCallback = void Function(dynamic exception); |
| |
| class ThirdWidget extends StatelessWidget { |
| const ThirdWidget({ Key? key, required this.targetKey, required this.onException }) : super(key: key); |
| |
| final Key targetKey; |
| final ExceptionCallback onException; |
| |
| @override |
| Widget build(BuildContext context) { |
| return GestureDetector( |
| key: targetKey, |
| onTap: () { |
| try { |
| Navigator.of(context); |
| } catch (e) { |
| onException(e); |
| } |
| }, |
| behavior: HitTestBehavior.opaque, |
| ); |
| } |
| } |
| |
| class OnTapPage extends StatelessWidget { |
| const OnTapPage({ Key? key, required this.id, this.onTap }) : super(key: key); |
| |
| final String id; |
| final VoidCallback? onTap; |
| |
| @override |
| Widget build(BuildContext context) { |
| return Scaffold( |
| appBar: AppBar(title: Text('Page $id')), |
| body: GestureDetector( |
| onTap: onTap, |
| behavior: HitTestBehavior.opaque, |
| child: Container( |
| child: Center( |
| child: Text(id, style: Theme.of(context).textTheme.headline3), |
| ), |
| ), |
| ), |
| ); |
| } |
| } |
| |
| class SlideInOutPageRoute<T> extends PageRouteBuilder<T> { |
| SlideInOutPageRoute({required WidgetBuilder bodyBuilder, RouteSettings? settings}) : super( |
| settings: settings, |
| pageBuilder: (BuildContext context, Animation<double> animation, Animation<double> secondaryAnimation) => bodyBuilder(context), |
| transitionsBuilder: (BuildContext context, Animation<double> animation, Animation<double> secondaryAnimation, Widget child) { |
| return SlideTransition( |
| position: Tween<Offset>( |
| begin: const Offset(1.0, 0), |
| end: Offset.zero, |
| ).animate(animation), |
| child: SlideTransition( |
| position: Tween<Offset>( |
| begin: Offset.zero, |
| end: const Offset(-1.0, 0), |
| ).animate(secondaryAnimation), |
| child: child, |
| ), |
| ); |
| }, |
| ); |
| |
| @override |
| AnimationController? get controller => super.controller; |
| } |
| |
| void main() { |
| testWidgets('Can navigator navigate to and from a stateful widget', (WidgetTester tester) async { |
| final Map<String, WidgetBuilder> routes = <String, WidgetBuilder>{ |
| '/': (BuildContext context) => const FirstWidget(), // X |
| '/second': (BuildContext context) => const SecondWidget(), // Y |
| }; |
| |
| await tester.pumpWidget(MaterialApp(routes: routes)); |
| expect(find.text('X'), findsOneWidget); |
| expect(find.text('Y', skipOffstage: false), findsNothing); |
| |
| await tester.tap(find.text('X')); |
| await tester.pump(); |
| expect(find.text('X'), findsOneWidget); |
| expect(find.text('Y', skipOffstage: false), isOffstage); |
| |
| await tester.pump(const Duration(milliseconds: 10)); |
| expect(find.text('X'), findsOneWidget); |
| expect(find.text('Y'), findsOneWidget); |
| |
| await tester.pump(const Duration(milliseconds: 10)); |
| expect(find.text('X'), findsOneWidget); |
| expect(find.text('Y'), findsOneWidget); |
| |
| await tester.pump(const Duration(milliseconds: 10)); |
| expect(find.text('X'), findsOneWidget); |
| expect(find.text('Y'), findsOneWidget); |
| |
| await tester.pump(const Duration(seconds: 1)); |
| expect(find.text('X'), findsNothing); |
| expect(find.text('X', skipOffstage: false), findsOneWidget); |
| expect(find.text('Y'), findsOneWidget); |
| |
| await tester.tap(find.text('Y')); |
| expect(find.text('X'), findsNothing); |
| expect(find.text('Y'), findsOneWidget); |
| |
| await tester.pump(); |
| await tester.pump(); |
| expect(find.text('X'), findsOneWidget); |
| expect(find.text('Y'), findsOneWidget); |
| |
| await tester.pump(const Duration(milliseconds: 10)); |
| expect(find.text('X'), findsOneWidget); |
| expect(find.text('Y'), findsOneWidget); |
| |
| await tester.pump(const Duration(seconds: 1)); |
| expect(find.text('X'), findsOneWidget); |
| expect(find.text('Y', skipOffstage: false), findsNothing); |
| }); |
| |
| testWidgets('Navigator.of fails gracefully when not found in context', (WidgetTester tester) async { |
| const Key targetKey = Key('foo'); |
| dynamic exception; |
| final Widget widget = ThirdWidget( |
| targetKey: targetKey, |
| onException: (dynamic e) { |
| exception = e; |
| }, |
| ); |
| await tester.pumpWidget(widget); |
| await tester.tap(find.byKey(targetKey)); |
| expect(exception, isFlutterError); |
| expect('$exception', startsWith('Navigator operation requested with a context')); |
| }); |
| |
| testWidgets('Navigator.of rootNavigator finds root Navigator', (WidgetTester tester) async { |
| await tester.pumpWidget(MaterialApp( |
| home: Material( |
| child: Column( |
| children: <Widget>[ |
| const SizedBox( |
| height: 300.0, |
| child: Text('Root page'), |
| ), |
| SizedBox( |
| height: 300.0, |
| child: Navigator( |
| onGenerateRoute: (RouteSettings settings) { |
| if (settings.name == '/') { |
| return MaterialPageRoute<void>( |
| builder: (BuildContext context) { |
| return ElevatedButton( |
| child: const Text('Next'), |
| onPressed: () { |
| Navigator.of(context).push( |
| MaterialPageRoute<void>( |
| builder: (BuildContext context) { |
| return ElevatedButton( |
| child: const Text('Inner page'), |
| onPressed: () { |
| Navigator.of(context, rootNavigator: true).push( |
| MaterialPageRoute<void>( |
| builder: (BuildContext context) { |
| return const Text('Dialog'); |
| } |
| ), |
| ); |
| }, |
| ); |
| } |
| ), |
| ); |
| }, |
| ); |
| }, |
| ); |
| } |
| return null; |
| }, |
| ), |
| ), |
| ], |
| ), |
| ), |
| )); |
| |
| await tester.tap(find.text('Next')); |
| await tester.pump(); |
| await tester.pump(const Duration(milliseconds: 300)); |
| |
| // Both elements are on screen. |
| expect(tester.getTopLeft(find.text('Root page')).dy, 0.0); |
| expect(tester.getTopLeft(find.text('Inner page')).dy, greaterThan(300.0)); |
| |
| await tester.tap(find.text('Inner page')); |
| await tester.pump(); |
| await tester.pump(const Duration(milliseconds: 300)); |
| |
| // Dialog is pushed to the whole page and is at the top of the screen, not |
| // inside the inner page. |
| expect(tester.getTopLeft(find.text('Dialog')).dy, 0.0); |
| }); |
| |
| testWidgets('Gestures between push and build are ignored', (WidgetTester tester) async { |
| final List<String> log = <String>[]; |
| final Map<String, WidgetBuilder> routes = <String, WidgetBuilder>{ |
| '/': (BuildContext context) { |
| return Row( |
| children: <Widget>[ |
| GestureDetector( |
| onTap: () { |
| log.add('left'); |
| Navigator.pushNamed(context, '/second'); |
| }, |
| child: const Text('left'), |
| ), |
| GestureDetector( |
| onTap: () { log.add('right'); }, |
| child: const Text('right'), |
| ), |
| ], |
| ); |
| }, |
| '/second': (BuildContext context) => Container(), |
| }; |
| await tester.pumpWidget(MaterialApp(routes: routes)); |
| expect(log, isEmpty); |
| await tester.tap(find.text('left')); |
| expect(log, equals(<String>['left'])); |
| await tester.tap(find.text('right'), warnIfMissed: false); |
| expect(log, equals(<String>['left'])); |
| }); |
| |
| testWidgets('Pending gestures are rejected', (WidgetTester tester) async { |
| final List<String> log = <String>[]; |
| final Map<String, WidgetBuilder> routes = <String, WidgetBuilder>{ |
| '/': (BuildContext context) { |
| return Row( |
| children: <Widget>[ |
| GestureDetector( |
| onTap: () { |
| log.add('left'); |
| Navigator.pushNamed(context, '/second'); |
| }, |
| child: const Text('left') |
| ), |
| GestureDetector( |
| onTap: () { log.add('right'); }, |
| child: const Text('right'), |
| ), |
| ] |
| ); |
| }, |
| '/second': (BuildContext context) => Container(), |
| }; |
| await tester.pumpWidget(MaterialApp(routes: routes)); |
| final TestGesture gesture = await tester.startGesture(tester.getCenter(find.text('right')), pointer: 23); |
| expect(log, isEmpty); |
| await tester.tap(find.text('left')); |
| expect(log, equals(<String>['left'])); |
| await gesture.up(); |
| expect(log, equals(<String>['left'])); |
| }, skip: true); // https://github.com/flutter/flutter/issues/4771 |
| |
| testWidgets('popAndPushNamed', (WidgetTester tester) async { |
| final Map<String, WidgetBuilder> routes = <String, WidgetBuilder>{ |
| '/' : (BuildContext context) => OnTapPage(id: '/', onTap: () { Navigator.pushNamed(context, '/A'); }), |
| '/A': (BuildContext context) => OnTapPage(id: 'A', onTap: () { Navigator.popAndPushNamed(context, '/B'); }), |
| '/B': (BuildContext context) => OnTapPage(id: 'B', onTap: () { Navigator.pop(context); }), |
| }; |
| |
| await tester.pumpWidget(MaterialApp(routes: routes)); |
| expect(find.text('/'), findsOneWidget); |
| expect(find.text('A', skipOffstage: false), findsNothing); |
| expect(find.text('B', skipOffstage: false), findsNothing); |
| |
| await tester.tap(find.text('/')); |
| await tester.pump(); |
| await tester.pump(const Duration(seconds: 1)); |
| expect(find.text('/'), findsNothing); |
| expect(find.text('A'), findsOneWidget); |
| expect(find.text('B'), findsNothing); |
| |
| await tester.tap(find.text('A')); |
| await tester.pump(); |
| await tester.pump(const Duration(seconds: 1)); |
| expect(find.text('/'), findsNothing); |
| expect(find.text('A'), findsNothing); |
| expect(find.text('B'), findsOneWidget); |
| }); |
| |
| testWidgets('popAndPushNamed with explicit void type parameter', (WidgetTester tester) async { |
| final Map<String, WidgetBuilder> routes = <String, WidgetBuilder>{ |
| '/' : (BuildContext context) => OnTapPage(id: '/', onTap: () { Navigator.pushNamed<void>(context, '/A'); }), |
| '/A': (BuildContext context) => OnTapPage(id: 'A', onTap: () { Navigator.popAndPushNamed<void, void>(context, '/B'); }), |
| '/B': (BuildContext context) => OnTapPage(id: 'B', onTap: () { Navigator.pop<void>(context); }), |
| }; |
| |
| await tester.pumpWidget(MaterialApp(routes: routes)); |
| expect(find.text('/'), findsOneWidget); |
| expect(find.text('A', skipOffstage: false), findsNothing); |
| expect(find.text('B', skipOffstage: false), findsNothing); |
| |
| await tester.tap(find.text('/')); |
| await tester.pump(); |
| await tester.pump(const Duration(seconds: 1)); |
| expect(find.text('/'), findsNothing); |
| expect(find.text('A'), findsOneWidget); |
| expect(find.text('B'), findsNothing); |
| |
| await tester.tap(find.text('A')); |
| await tester.pump(); |
| await tester.pump(const Duration(seconds: 1)); |
| expect(find.text('/'), findsNothing); |
| expect(find.text('A'), findsNothing); |
| expect(find.text('B'), findsOneWidget); |
| }); |
| |
| testWidgets('Push and pop should trigger the observers', (WidgetTester tester) async { |
| final Map<String, WidgetBuilder> routes = <String, WidgetBuilder>{ |
| '/' : (BuildContext context) => OnTapPage(id: '/', onTap: () { Navigator.pushNamed(context, '/A'); }), |
| '/A': (BuildContext context) => OnTapPage(id: 'A', onTap: () { Navigator.pop(context); }), |
| }; |
| bool isPushed = false; |
| bool isPopped = false; |
| final TestObserver observer = TestObserver() |
| ..onPushed = (Route<dynamic>? route, Route<dynamic>? previousRoute) { |
| // Pushes the initial route. |
| expect(route is PageRoute && route.settings.name == '/', isTrue); |
| expect(previousRoute, isNull); |
| isPushed = true; |
| } |
| ..onPopped = (Route<dynamic>? route, Route<dynamic>? previousRoute) { |
| isPopped = true; |
| }; |
| |
| await tester.pumpWidget(MaterialApp( |
| routes: routes, |
| navigatorObservers: <NavigatorObserver>[observer], |
| )); |
| expect(find.text('/'), findsOneWidget); |
| expect(find.text('A'), findsNothing); |
| expect(isPushed, isTrue); |
| expect(isPopped, isFalse); |
| |
| isPushed = false; |
| isPopped = false; |
| observer.onPushed = (Route<dynamic>? route, Route<dynamic>? previousRoute) { |
| expect(route is PageRoute && route.settings.name == '/A', isTrue); |
| expect(previousRoute is PageRoute && previousRoute.settings.name == '/', isTrue); |
| isPushed = true; |
| }; |
| |
| await tester.tap(find.text('/')); |
| await tester.pump(); |
| await tester.pump(const Duration(seconds: 1)); |
| expect(find.text('/'), findsNothing); |
| expect(find.text('A'), findsOneWidget); |
| expect(isPushed, isTrue); |
| expect(isPopped, isFalse); |
| |
| isPushed = false; |
| isPopped = false; |
| observer.onPopped = (Route<dynamic>? route, Route<dynamic>? previousRoute) { |
| expect(route is PageRoute && route.settings.name == '/A', isTrue); |
| expect(previousRoute is PageRoute && previousRoute.settings.name == '/', isTrue); |
| isPopped = true; |
| }; |
| |
| await tester.tap(find.text('A')); |
| await tester.pump(); |
| await tester.pump(const Duration(seconds: 1)); |
| expect(find.text('/'), findsOneWidget); |
| expect(find.text('A'), findsNothing); |
| expect(isPushed, isFalse); |
| expect(isPopped, isTrue); |
| }); |
| |
| testWidgets('Add and remove an observer should work', (WidgetTester tester) async { |
| final Map<String, WidgetBuilder> routes = <String, WidgetBuilder>{ |
| '/' : (BuildContext context) => OnTapPage(id: '/', onTap: () { Navigator.pushNamed(context, '/A'); }), |
| '/A': (BuildContext context) => OnTapPage(id: 'A', onTap: () { Navigator.pop(context); }), |
| }; |
| bool isPushed = false; |
| bool isPopped = false; |
| final TestObserver observer1 = TestObserver(); |
| final TestObserver observer2 = TestObserver() |
| ..onPushed = (Route<dynamic>? route, Route<dynamic>? previousRoute) { |
| isPushed = true; |
| } |
| ..onPopped = (Route<dynamic>? route, Route<dynamic>? previousRoute) { |
| isPopped = true; |
| }; |
| |
| await tester.pumpWidget(MaterialApp( |
| routes: routes, |
| navigatorObservers: <NavigatorObserver>[observer1], |
| )); |
| expect(isPushed, isFalse); |
| expect(isPopped, isFalse); |
| |
| await tester.pumpWidget(MaterialApp( |
| routes: routes, |
| navigatorObservers: <NavigatorObserver>[observer1, observer2], |
| )); |
| await tester.tap(find.text('/')); |
| await tester.pump(); |
| await tester.pump(const Duration(seconds: 1)); |
| expect(isPushed, isTrue); |
| expect(isPopped, isFalse); |
| |
| isPushed = false; |
| isPopped = false; |
| |
| await tester.pumpWidget(MaterialApp( |
| routes: routes, |
| navigatorObservers: <NavigatorObserver>[observer1], |
| )); |
| await tester.tap(find.text('A')); |
| await tester.pump(); |
| await tester.pump(const Duration(seconds: 1)); |
| expect(isPushed, isFalse); |
| expect(isPopped, isFalse); |
| }); |
| |
| testWidgets('initial route trigger observer in the right order', (WidgetTester tester) async { |
| final Map<String, WidgetBuilder> routes = <String, WidgetBuilder>{ |
| '/' : (BuildContext context) => const Text('/'), |
| '/A': (BuildContext context) => const Text('A'), |
| '/A/B': (BuildContext context) => const Text('B'), |
| }; |
| final List<NavigatorObservation> observations = <NavigatorObservation>[]; |
| final TestObserver observer = TestObserver() |
| ..onPushed = (Route<dynamic>? route, Route<dynamic>? previousRoute) { |
| // Pushes the initial route. |
| observations.add( |
| NavigatorObservation( |
| current: route?.settings.name, |
| previous: previousRoute?.settings.name, |
| operation: 'push' |
| ) |
| ); |
| }; |
| |
| await tester.pumpWidget(MaterialApp( |
| routes: routes, |
| initialRoute: '/A/B', |
| navigatorObservers: <NavigatorObserver>[observer], |
| )); |
| |
| expect(observations.length, 3); |
| expect(observations[0].operation, 'push'); |
| expect(observations[0].current, '/'); |
| expect(observations[0].previous, isNull); |
| |
| expect(observations[1].operation, 'push'); |
| expect(observations[1].current, '/A'); |
| expect(observations[1].previous, '/'); |
| |
| expect(observations[2].operation, 'push'); |
| expect(observations[2].current, '/A/B'); |
| expect(observations[2].previous, '/A'); |
| }); |
| |
| testWidgets('Route didAdd and dispose in same frame work', (WidgetTester tester) async { |
| // Regression Test for https://github.com/flutter/flutter/issues/61346. |
| Widget buildNavigator() { |
| return Navigator( |
| pages: const <Page<void>>[ |
| MaterialPage<void>( |
| child: Placeholder(), |
| ) |
| ], |
| onPopPage: (Route<dynamic> route, dynamic result) => false, |
| ); |
| } |
| final TabController controller = TabController(length: 3, vsync: tester); |
| await tester.pumpWidget( |
| Directionality( |
| textDirection: TextDirection.ltr, |
| child: TabBarView( |
| controller: controller, |
| children: <Widget>[ |
| buildNavigator(), |
| buildNavigator(), |
| buildNavigator(), |
| ], |
| ) |
| ), |
| ); |
| |
| // This test should finish without crashing. |
| controller.index = 2; |
| await tester.pumpAndSettle(); |
| }); |
| |
| testWidgets('Pages update does update overlay correctly', (WidgetTester tester) async { |
| // Regression Test for https://github.com/flutter/flutter/issues/64941. |
| List<Page<void>> pages = const <Page<void>>[ |
| MaterialPage<void>( |
| key: ValueKey<int>(0), |
| child: Text('page 0'), |
| ), |
| MaterialPage<void>( |
| key: ValueKey<int>(1), |
| child: Text('page 1'), |
| ), |
| ]; |
| Widget buildNavigator() { |
| return Navigator( |
| pages: pages, |
| onPopPage: (Route<dynamic> route, dynamic result) => false, |
| ); |
| } |
| await tester.pumpWidget( |
| Directionality( |
| textDirection: TextDirection.ltr, |
| child: buildNavigator(), |
| ), |
| ); |
| |
| expect(find.text('page 1'), findsOneWidget); |
| expect(find.text('page 0'), findsNothing); |
| |
| // Removes the first page. |
| pages = const <Page<void>>[ |
| MaterialPage<void>( |
| key: ValueKey<int>(1), |
| child: Text('page 1'), |
| ), |
| ]; |
| |
| await tester.pumpWidget( |
| Directionality( |
| textDirection: TextDirection.ltr, |
| child: buildNavigator(), |
| ), |
| ); |
| // Overlay updates correctly. |
| expect(find.text('page 1'), findsOneWidget); |
| expect(find.text('page 0'), findsNothing); |
| |
| await tester.pumpAndSettle(); |
| expect(find.text('page 1'), findsOneWidget); |
| expect(find.text('page 0'), findsNothing); |
| }); |
| |
| testWidgets('replaceNamed replaces', (WidgetTester tester) async { |
| final Map<String, WidgetBuilder> routes = <String, WidgetBuilder>{ |
| '/' : (BuildContext context) => OnTapPage(id: '/', onTap: () { Navigator.pushReplacementNamed(context, '/A'); }), |
| '/A': (BuildContext context) => OnTapPage(id: 'A', onTap: () { Navigator.pushReplacementNamed(context, '/B'); }), |
| '/B': (BuildContext context) => const OnTapPage(id: 'B'), |
| }; |
| |
| await tester.pumpWidget(MaterialApp(routes: routes)); |
| await tester.tap(find.text('/')); // replaceNamed('/A') |
| await tester.pump(); |
| await tester.pump(const Duration(seconds: 1)); |
| expect(find.text('/'), findsNothing); |
| expect(find.text('A'), findsOneWidget); |
| |
| await tester.tap(find.text('A')); // replaceNamed('/B') |
| await tester.pump(); |
| await tester.pump(const Duration(seconds: 1)); |
| expect(find.text('/'), findsNothing); |
| expect(find.text('A'), findsNothing); |
| expect(find.text('B'), findsOneWidget); |
| }); |
| |
| testWidgets('pushReplacement sets secondaryAnimation after transition, with history change during transition', (WidgetTester tester) async { |
| final Map<String, SlideInOutPageRoute<dynamic>> routes = <String, SlideInOutPageRoute<dynamic>>{}; |
| final Map<String, WidgetBuilder> builders = <String, WidgetBuilder>{ |
| '/' : (BuildContext context) => OnTapPage( |
| id: '/', |
| onTap: () { |
| Navigator.pushNamed(context, '/A'); |
| } |
| ), |
| '/A': (BuildContext context) => OnTapPage( |
| id: 'A', |
| onTap: () { |
| Navigator.pushNamed(context, '/B'); |
| } |
| ), |
| '/B': (BuildContext context) => OnTapPage( |
| id: 'B', |
| onTap: () { |
| Navigator.pushReplacementNamed(context, '/C'); |
| }, |
| ), |
| '/C': (BuildContext context) => OnTapPage( |
| id: 'C', |
| onTap: () { |
| Navigator.removeRoute(context, routes['/']!); |
| }, |
| ), |
| }; |
| await tester.pumpWidget(MaterialApp( |
| onGenerateRoute: (RouteSettings settings) { |
| final SlideInOutPageRoute<dynamic> ret = SlideInOutPageRoute<dynamic>(bodyBuilder: builders[settings.name]!, settings: settings); |
| routes[settings.name!] = ret; |
| return ret; |
| } |
| )); |
| await tester.pumpAndSettle(); |
| await tester.tap(find.text('/')); |
| await tester.pumpAndSettle(); |
| final double a2 = routes['/A']!.secondaryAnimation!.value; |
| await tester.tap(find.text('A')); |
| await tester.pump(); |
| await tester.pump(const Duration(milliseconds: 16)); |
| expect(routes['/A']!.secondaryAnimation!.value, greaterThan(a2)); |
| await tester.pumpAndSettle(); |
| await tester.tap(find.text('B')); |
| await tester.pump(); |
| await tester.pump(const Duration(milliseconds: 200)); |
| expect(routes['/A']!.secondaryAnimation!.value, equals(1.0)); |
| await tester.tap(find.text('C')); |
| await tester.pumpAndSettle(); |
| expect(find.text('C'), isOnstage); |
| expect(routes['/A']!.secondaryAnimation!.value, equals(routes['/C']!.animation!.value)); |
| final AnimationController controller = routes['/C']!.controller!; |
| controller.value = 1 - controller.value; |
| expect(routes['/A']!.secondaryAnimation!.value, equals(routes['/C']!.animation!.value)); |
| }); |
| |
| testWidgets('new route removed from navigator history during pushReplacement transition', (WidgetTester tester) async { |
| final Map<String, SlideInOutPageRoute<dynamic>> routes = <String, SlideInOutPageRoute<dynamic>>{}; |
| final Map<String, WidgetBuilder> builders = <String, WidgetBuilder>{ |
| '/' : (BuildContext context) => OnTapPage( |
| id: '/', |
| onTap: () { |
| Navigator.pushNamed(context, '/A'); |
| } |
| ), |
| '/A': (BuildContext context) => OnTapPage( |
| id: 'A', |
| onTap: () { |
| Navigator.pushReplacementNamed(context, '/B'); |
| } |
| ), |
| '/B': (BuildContext context) => OnTapPage( |
| id: 'B', |
| onTap: () { |
| Navigator.removeRoute(context, routes['/B']!); |
| }, |
| ), |
| }; |
| await tester.pumpWidget(MaterialApp( |
| onGenerateRoute: (RouteSettings settings) { |
| final SlideInOutPageRoute<dynamic> ret = SlideInOutPageRoute<dynamic>(bodyBuilder: builders[settings.name]!, settings: settings); |
| routes[settings.name!] = ret; |
| return ret; |
| } |
| )); |
| await tester.pumpAndSettle(); |
| await tester.tap(find.text('/')); |
| await tester.pumpAndSettle(); |
| await tester.tap(find.text('A')); |
| await tester.pump(); |
| await tester.pump(const Duration(milliseconds: 200)); |
| expect(find.text('A'), isOnstage); |
| expect(find.text('B'), isOnstage); |
| await tester.tap(find.text('B')); |
| await tester.pumpAndSettle(); |
| expect(find.text('/'), isOnstage); |
| expect(find.text('B'), findsNothing); |
| expect(find.text('A'), findsNothing); |
| expect(routes['/']!.secondaryAnimation!.value, equals(0.0)); |
| expect(routes['/']!.animation!.value, equals(1.0)); |
| }); |
| |
| testWidgets('pushReplacement triggers secondaryAnimation', (WidgetTester tester) async { |
| final Map<String, WidgetBuilder> routes = <String, WidgetBuilder>{ |
| '/' : (BuildContext context) => OnTapPage( |
| id: '/', |
| onTap: () { |
| Navigator.pushReplacementNamed(context, '/A'); |
| } |
| ), |
| '/A': (BuildContext context) => OnTapPage( |
| id: 'A', |
| onTap: () { |
| Navigator.pushReplacementNamed(context, '/B'); |
| } |
| ), |
| '/B': (BuildContext context) => const OnTapPage(id: 'B'), |
| }; |
| |
| await tester.pumpWidget(MaterialApp( |
| onGenerateRoute: (RouteSettings settings) { |
| return SlideInOutPageRoute<dynamic>(bodyBuilder: routes[settings.name]!); |
| } |
| )); |
| await tester.pumpAndSettle(); |
| final Offset rootOffsetOriginal = tester.getTopLeft(find.text('/')); |
| await tester.tap(find.text('/')); |
| await tester.pump(); |
| await tester.pump(const Duration(milliseconds: 16)); |
| expect(find.text('/'), isOnstage); |
| expect(find.text('A'), isOnstage); |
| expect(find.text('B'), findsNothing); |
| final Offset rootOffset = tester.getTopLeft(find.text('/')); |
| expect(rootOffset.dx, lessThan(rootOffsetOriginal.dx)); |
| |
| Offset aOffsetOriginal = tester.getTopLeft(find.text('A')); |
| await tester.pumpAndSettle(); |
| Offset aOffset = tester.getTopLeft(find.text('A')); |
| expect(aOffset.dx, lessThan(aOffsetOriginal.dx)); |
| |
| aOffsetOriginal = aOffset; |
| await tester.tap(find.text('A')); |
| await tester.pump(); |
| await tester.pump(const Duration(milliseconds: 16)); |
| expect(find.text('/'), findsNothing); |
| expect(find.text('A'), isOnstage); |
| expect(find.text('B'), isOnstage); |
| aOffset = tester.getTopLeft(find.text('A')); |
| expect(aOffset.dx, lessThan(aOffsetOriginal.dx)); |
| }); |
| |
| testWidgets('pushReplacement correctly reports didReplace to the observer', (WidgetTester tester) async { |
| // Regression test for https://github.com/flutter/flutter/issues/56892. |
| final Map<String, WidgetBuilder> routes = <String, WidgetBuilder>{ |
| '/' : (BuildContext context) => const OnTapPage( |
| id: '/', |
| ), |
| '/A': (BuildContext context) => const OnTapPage( |
| id: 'A', |
| ), |
| '/A/B': (BuildContext context) => OnTapPage( |
| id: 'B', |
| onTap: (){ |
| Navigator.of(context).popUntil((Route<dynamic> route) => route.isFirst); |
| Navigator.of(context).pushReplacementNamed('/C'); |
| }, |
| ), |
| '/C': (BuildContext context) => const OnTapPage(id: 'C', |
| ), |
| }; |
| final List<NavigatorObservation> observations = <NavigatorObservation>[]; |
| final TestObserver observer = TestObserver() |
| ..onPopped = (Route<dynamic>? route, Route<dynamic>? previousRoute) { |
| observations.add( |
| NavigatorObservation( |
| current: route?.settings.name, |
| previous: previousRoute?.settings.name, |
| operation: 'didPop' |
| ) |
| ); |
| } |
| ..onReplaced = (Route<dynamic>? route, Route<dynamic>? previousRoute) { |
| observations.add( |
| NavigatorObservation( |
| current: route?.settings.name, |
| previous: previousRoute?.settings.name, |
| operation: 'didReplace' |
| ) |
| ); |
| }; |
| await tester.pumpWidget( |
| MaterialApp( |
| routes: routes, |
| navigatorObservers: <NavigatorObserver>[observer], |
| initialRoute: '/A/B', |
| ) |
| ); |
| await tester.pumpAndSettle(); |
| expect(find.text('B'), isOnstage); |
| |
| await tester.tap(find.text('B')); |
| await tester.pump(); |
| await tester.pump(const Duration(milliseconds: 16)); |
| expect(observations.length, 3); |
| expect(observations[0].current, '/A/B'); |
| expect(observations[0].previous, '/A'); |
| expect(observations[0].operation, 'didPop'); |
| expect(observations[1].current, '/A'); |
| expect(observations[1].previous, '/'); |
| expect(observations[1].operation, 'didPop'); |
| |
| expect(observations[2].current, '/C'); |
| expect(observations[2].previous, '/'); |
| expect(observations[2].operation, 'didReplace'); |
| |
| await tester.pumpAndSettle(); |
| expect(find.text('C'), isOnstage); |
| }); |
| |
| testWidgets('Able to pop all routes', (WidgetTester tester) async { |
| final Map<String, WidgetBuilder> routes = <String, WidgetBuilder>{ |
| '/' : (BuildContext context) => const OnTapPage( |
| id: '/', |
| ), |
| '/A': (BuildContext context) => const OnTapPage( |
| id: 'A', |
| ), |
| '/A/B': (BuildContext context) => OnTapPage( |
| id: 'B', |
| onTap: (){ |
| // Pops all routes with bad predicate. |
| Navigator.of(context).popUntil((Route<dynamic> route) => false); |
| }, |
| ), |
| }; |
| await tester.pumpWidget( |
| MaterialApp( |
| routes: routes, |
| initialRoute: '/A/B', |
| ) |
| ); |
| await tester.tap(find.text('B')); |
| await tester.pumpAndSettle(); |
| expect(tester.takeException(), isNull); |
| }); |
| |
| testWidgets('pushAndRemoveUntil triggers secondaryAnimation', (WidgetTester tester) async { |
| final Map<String, WidgetBuilder> routes = <String, WidgetBuilder>{ |
| '/' : (BuildContext context) => OnTapPage( |
| id: '/', |
| onTap: () { |
| Navigator.pushNamed(context, '/A'); |
| } |
| ), |
| '/A': (BuildContext context) => OnTapPage( |
| id: 'A', |
| onTap: () { |
| Navigator.pushNamedAndRemoveUntil(context, '/B', (Route<dynamic> route) => false); |
| } |
| ), |
| '/B': (BuildContext context) => const OnTapPage(id: 'B'), |
| }; |
| |
| await tester.pumpWidget(MaterialApp( |
| onGenerateRoute: (RouteSettings settings) { |
| return SlideInOutPageRoute<dynamic>(bodyBuilder: routes[settings.name]!); |
| } |
| )); |
| await tester.pumpAndSettle(); |
| final Offset rootOffsetOriginal = tester.getTopLeft(find.text('/')); |
| await tester.tap(find.text('/')); |
| await tester.pump(); |
| await tester.pump(const Duration(milliseconds: 16)); |
| expect(find.text('/'), isOnstage); |
| expect(find.text('A'), isOnstage); |
| expect(find.text('B'), findsNothing); |
| final Offset rootOffset = tester.getTopLeft(find.text('/')); |
| expect(rootOffset.dx, lessThan(rootOffsetOriginal.dx)); |
| |
| Offset aOffsetOriginal = tester.getTopLeft(find.text('A')); |
| await tester.pumpAndSettle(); |
| Offset aOffset = tester.getTopLeft(find.text('A')); |
| expect(aOffset.dx, lessThan(aOffsetOriginal.dx)); |
| |
| aOffsetOriginal = aOffset; |
| await tester.tap(find.text('A')); |
| await tester.pump(); |
| await tester.pump(const Duration(milliseconds: 16)); |
| expect(find.text('/'), findsNothing); |
| expect(find.text('A'), isOnstage); |
| expect(find.text('B'), isOnstage); |
| aOffset = tester.getTopLeft(find.text('A')); |
| expect(aOffset.dx, lessThan(aOffsetOriginal.dx)); |
| |
| await tester.pumpAndSettle(); |
| expect(find.text('/'), findsNothing); |
| expect(find.text('A'), findsNothing); |
| expect(find.text('B'), isOnstage); |
| }); |
| |
| testWidgets('pushAndRemoveUntil does not remove routes below the first route that pass the predicate', (WidgetTester tester) async { |
| // Regression https://github.com/flutter/flutter/issues/56688 |
| final GlobalKey<NavigatorState> navigator = GlobalKey<NavigatorState>(); |
| final Map<String, WidgetBuilder> routes = <String, WidgetBuilder>{ |
| '/': (BuildContext context) => const Text('home'), |
| '/A': (BuildContext context) => const Text('page A'), |
| '/A/B': (BuildContext context) => OnTapPage( |
| id: 'B', |
| onTap: () { |
| Navigator.of(context).pushNamedAndRemoveUntil('/D', ModalRoute.withName('/A')); |
| }, |
| ), |
| '/D': (BuildContext context) => const Text('page D'), |
| }; |
| |
| await tester.pumpWidget( |
| MaterialApp( |
| navigatorKey: navigator, |
| routes: routes, |
| initialRoute: '/A/B', |
| ) |
| ); |
| await tester.pumpAndSettle(); |
| await tester.tap(find.text('B')); |
| await tester.pumpAndSettle(); |
| expect(find.text('page D'), isOnstage); |
| |
| navigator.currentState!.pop(); |
| await tester.pumpAndSettle(); |
| expect(find.text('page A'), isOnstage); |
| |
| navigator.currentState!.pop(); |
| await tester.pumpAndSettle(); |
| expect(find.text('home'), isOnstage); |
| }); |
| |
| testWidgets('replaceNamed returned value', (WidgetTester tester) async { |
| late Future<String?> value; |
| |
| final Map<String, WidgetBuilder> routes = <String, WidgetBuilder>{ |
| '/' : (BuildContext context) => OnTapPage(id: '/', onTap: () { Navigator.pushNamed(context, '/A'); }), |
| '/A': (BuildContext context) => OnTapPage(id: 'A', onTap: () { value = Navigator.pushReplacementNamed(context, '/B', result: 'B'); }), |
| '/B': (BuildContext context) => OnTapPage(id: 'B', onTap: () { Navigator.pop(context, 'B'); }), |
| }; |
| |
| await tester.pumpWidget(MaterialApp( |
| onGenerateRoute: (RouteSettings settings) { |
| return PageRouteBuilder<String>( |
| settings: settings, |
| pageBuilder: (BuildContext context, Animation<double> _, Animation<double> __) { |
| return routes[settings.name]!(context); |
| }, |
| ); |
| } |
| )); |
| |
| expect(find.text('/'), findsOneWidget); |
| expect(find.text('A', skipOffstage: false), findsNothing); |
| expect(find.text('B', skipOffstage: false), findsNothing); |
| |
| await tester.tap(find.text('/')); // pushNamed('/A'), stack becomes /, /A |
| await tester.pump(); |
| await tester.pump(const Duration(seconds: 1)); |
| expect(find.text('/'), findsNothing); |
| expect(find.text('A'), findsOneWidget); |
| expect(find.text('B'), findsNothing); |
| |
| await tester.tap(find.text('A')); // replaceNamed('/B'), stack becomes /, /B |
| await tester.pump(); |
| await tester.pump(const Duration(seconds: 1)); |
| expect(find.text('/'), findsNothing); |
| expect(find.text('A'), findsNothing); |
| expect(find.text('B'), findsOneWidget); |
| |
| await tester.tap(find.text('B')); // pop, stack becomes / |
| await tester.pump(); |
| await tester.pump(const Duration(seconds: 1)); |
| expect(find.text('/'), findsOneWidget); |
| expect(find.text('A'), findsNothing); |
| expect(find.text('B'), findsNothing); |
| |
| final String? replaceNamedValue = await value; // replaceNamed result was 'B' |
| expect(replaceNamedValue, 'B'); |
| }); |
| |
| testWidgets('removeRoute', (WidgetTester tester) async { |
| final Map<String, WidgetBuilder> pageBuilders = <String, WidgetBuilder>{ |
| '/' : (BuildContext context) => OnTapPage(id: '/', onTap: () { Navigator.pushNamed(context, '/A'); }), |
| '/A': (BuildContext context) => OnTapPage(id: 'A', onTap: () { Navigator.pushNamed(context, '/B'); }), |
| '/B': (BuildContext context) => const OnTapPage(id: 'B'), |
| }; |
| final Map<String, Route<String>> routes = <String, Route<String>>{}; |
| |
| late Route<String> removedRoute; |
| late Route<String> previousRoute; |
| |
| final TestObserver observer = TestObserver() |
| ..onRemoved = (Route<dynamic>? route, Route<dynamic>? previous) { |
| removedRoute = route! as Route<String>; |
| previousRoute = previous! as Route<String>; |
| }; |
| |
| await tester.pumpWidget(MaterialApp( |
| navigatorObservers: <NavigatorObserver>[observer], |
| onGenerateRoute: (RouteSettings settings) { |
| routes[settings.name!] = PageRouteBuilder<String>( |
| settings: settings, |
| pageBuilder: (BuildContext context, Animation<double> _, Animation<double> __) { |
| return pageBuilders[settings.name!]!(context); |
| }, |
| ); |
| return routes[settings.name]; |
| }, |
| )); |
| |
| expect(find.text('/'), findsOneWidget); |
| expect(find.text('A'), findsNothing); |
| expect(find.text('B'), findsNothing); |
| |
| await tester.tap(find.text('/')); // pushNamed('/A'), stack becomes /, /A |
| await tester.pumpAndSettle(); |
| expect(find.text('/'), findsNothing); |
| expect(find.text('A'), findsOneWidget); |
| expect(find.text('B'), findsNothing); |
| |
| await tester.tap(find.text('A')); // pushNamed('/B'), stack becomes /, /A, /B |
| await tester.pumpAndSettle(); |
| expect(find.text('/'), findsNothing); |
| expect(find.text('A'), findsNothing); |
| expect(find.text('B'), findsOneWidget); |
| |
| // Verify that the navigator's stack is ordered as expected. |
| expect(routes['/']!.isActive, true); |
| expect(routes['/A']!.isActive, true); |
| expect(routes['/B']!.isActive, true); |
| expect(routes['/']!.isFirst, true); |
| expect(routes['/B']!.isCurrent, true); |
| |
| final NavigatorState navigator = tester.state<NavigatorState>(find.byType(Navigator)); |
| navigator.removeRoute(routes['/B']!); // stack becomes /, /A |
| await tester.pump(); |
| expect(find.text('/'), findsNothing); |
| expect(find.text('A'), findsOneWidget); |
| expect(find.text('B'), findsNothing); |
| |
| // Verify that the navigator's stack no longer includes /B |
| expect(routes['/']!.isActive, true); |
| expect(routes['/A']!.isActive, true); |
| expect(routes['/B']!.isActive, false); |
| expect(routes['/']!.isFirst, true); |
| expect(routes['/A']!.isCurrent, true); |
| |
| expect(removedRoute, routes['/B']); |
| expect(previousRoute, routes['/A']); |
| |
| navigator.removeRoute(routes['/A']!); // stack becomes just / |
| await tester.pump(); |
| expect(find.text('/'), findsOneWidget); |
| expect(find.text('A'), findsNothing); |
| expect(find.text('B'), findsNothing); |
| |
| // Verify that the navigator's stack no longer includes /A |
| expect(routes['/']!.isActive, true); |
| expect(routes['/A']!.isActive, false); |
| expect(routes['/B']!.isActive, false); |
| expect(routes['/']!.isFirst, true); |
| expect(routes['/']!.isCurrent, true); |
| expect(removedRoute, routes['/A']); |
| expect(previousRoute, routes['/']); |
| }); |
| |
| testWidgets('remove a route whose value is awaited', (WidgetTester tester) async { |
| late Future<String?> pageValue; |
| final Map<String, WidgetBuilder> pageBuilders = <String, WidgetBuilder>{ |
| '/': (BuildContext context) => OnTapPage(id: '/', onTap: () { pageValue = Navigator.pushNamed(context, '/A'); }), |
| '/A': (BuildContext context) => OnTapPage(id: 'A', onTap: () { Navigator.pop(context, 'A'); }), |
| }; |
| final Map<String, Route<String>> routes = <String, Route<String>>{}; |
| |
| await tester.pumpWidget(MaterialApp( |
| onGenerateRoute: (RouteSettings settings) { |
| routes[settings.name!] = PageRouteBuilder<String>( |
| settings: settings, |
| pageBuilder: (BuildContext context, Animation<double> _, Animation<double> __) { |
| return pageBuilders[settings.name!]!(context); |
| }, |
| ); |
| return routes[settings.name]; |
| } |
| )); |
| |
| await tester.tap(find.text('/')); // pushNamed('/A'), stack becomes /, /A |
| await tester.pumpAndSettle(); |
| pageValue.then((String? value) { assert(false); }); |
| |
| final NavigatorState navigator = tester.state<NavigatorState>(find.byType(Navigator)); |
| navigator.removeRoute(routes['/A']!); // stack becomes /, pageValue will not complete |
| }); |
| |
| testWidgets('replacing route can be observed', (WidgetTester tester) async { |
| final GlobalKey<NavigatorState> key = GlobalKey<NavigatorState>(); |
| final List<String> log = <String>[]; |
| final TestObserver observer = TestObserver() |
| ..onPushed = (Route<dynamic>? route, Route<dynamic>? previousRoute) { |
| log.add('pushed ${route!.settings.name} (previous is ${previousRoute == null ? "<none>" : previousRoute.settings.name})'); |
| } |
| ..onPopped = (Route<dynamic>? route, Route<dynamic>? previousRoute) { |
| log.add('popped ${route!.settings.name} (previous is ${previousRoute == null ? "<none>" : previousRoute.settings.name})'); |
| } |
| ..onRemoved = (Route<dynamic>? route, Route<dynamic>? previousRoute) { |
| log.add('removed ${route!.settings.name} (previous is ${previousRoute == null ? "<none>" : previousRoute.settings.name})'); |
| } |
| ..onReplaced = (Route<dynamic>? newRoute, Route<dynamic>? oldRoute) { |
| log.add('replaced ${oldRoute!.settings.name} with ${newRoute!.settings.name}'); |
| }; |
| late Route<void> routeB; |
| await tester.pumpWidget(MaterialApp( |
| navigatorKey: key, |
| navigatorObservers: <NavigatorObserver>[observer], |
| home: TextButton( |
| child: const Text('A'), |
| onPressed: () { |
| key.currentState!.push<void>(routeB = MaterialPageRoute<void>( |
| settings: const RouteSettings(name: 'B'), |
| builder: (BuildContext context) { |
| return TextButton( |
| child: const Text('B'), |
| onPressed: () { |
| key.currentState!.push<void>(MaterialPageRoute<int>( |
| settings: const RouteSettings(name: 'C'), |
| builder: (BuildContext context) { |
| return TextButton( |
| child: const Text('C'), |
| onPressed: () { |
| key.currentState!.replace( |
| oldRoute: routeB, |
| newRoute: MaterialPageRoute<int>( |
| settings: const RouteSettings(name: 'D'), |
| builder: (BuildContext context) { |
| return const Text('D'); |
| }, |
| ), |
| ); |
| }, |
| ); |
| }, |
| )); |
| }, |
| ); |
| }, |
| )); |
| }, |
| ), |
| )); |
| expect(log, <String>['pushed / (previous is <none>)']); |
| await tester.tap(find.text('A')); |
| await tester.pump(); |
| await tester.pump(const Duration(seconds: 1)); |
| expect(log, <String>['pushed / (previous is <none>)', 'pushed B (previous is /)']); |
| await tester.tap(find.text('B')); |
| await tester.pump(); |
| await tester.pump(const Duration(seconds: 1)); |
| expect(log, <String>['pushed / (previous is <none>)', 'pushed B (previous is /)', 'pushed C (previous is B)']); |
| await tester.tap(find.text('C')); |
| await tester.pump(); |
| await tester.pump(const Duration(seconds: 1)); |
| expect(log, <String>['pushed / (previous is <none>)', 'pushed B (previous is /)', 'pushed C (previous is B)', 'replaced B with D']); |
| }); |
| |
| testWidgets('didStartUserGesture observable', (WidgetTester tester) async { |
| final Map<String, WidgetBuilder> routes = <String, WidgetBuilder>{ |
| '/' : (BuildContext context) => OnTapPage(id: '/', onTap: () { Navigator.pushNamed(context, '/A'); }), |
| '/A': (BuildContext context) => OnTapPage(id: 'A', onTap: () { Navigator.pop(context); }), |
| }; |
| |
| late Route<dynamic> observedRoute; |
| late Route<dynamic> observedPreviousRoute; |
| final TestObserver observer = TestObserver() |
| ..onStartUserGesture = (Route<dynamic>? route, Route<dynamic>? previousRoute) { |
| observedRoute = route!; |
| observedPreviousRoute = previousRoute!; |
| }; |
| |
| await tester.pumpWidget(MaterialApp( |
| routes: routes, |
| navigatorObservers: <NavigatorObserver>[observer], |
| )); |
| |
| await tester.tap(find.text('/')); |
| await tester.pump(); |
| await tester.pump(const Duration(seconds: 1)); |
| expect(find.text('/'), findsNothing); |
| expect(find.text('A'), findsOneWidget); |
| |
| tester.state<NavigatorState>(find.byType(Navigator)).didStartUserGesture(); |
| |
| expect(observedRoute.settings.name, '/A'); |
| expect(observedPreviousRoute.settings.name, '/'); |
| }); |
| |
| testWidgets('ModalRoute.of sets up a route to rebuild if its state changes', (WidgetTester tester) async { |
| final GlobalKey<NavigatorState> key = GlobalKey<NavigatorState>(); |
| final List<String> log = <String>[]; |
| late Route<void> routeB; |
| await tester.pumpWidget(MaterialApp( |
| navigatorKey: key, |
| theme: ThemeData( |
| pageTransitionsTheme: const PageTransitionsTheme( |
| builders: <TargetPlatform, PageTransitionsBuilder>{ |
| TargetPlatform.android: FadeUpwardsPageTransitionsBuilder(), |
| }, |
| ), |
| ), |
| home: TextButton( |
| child: const Text('A'), |
| onPressed: () { |
| key.currentState!.push<void>(routeB = MaterialPageRoute<void>( |
| settings: const RouteSettings(name: 'B'), |
| builder: (BuildContext context) { |
| log.add('building B'); |
| return TextButton( |
| child: const Text('B'), |
| onPressed: () { |
| key.currentState!.push<void>(MaterialPageRoute<int>( |
| settings: const RouteSettings(name: 'C'), |
| builder: (BuildContext context) { |
| log.add('building C'); |
| log.add('found ${ModalRoute.of(context)!.settings.name}'); |
| return TextButton( |
| child: const Text('C'), |
| onPressed: () { |
| key.currentState!.replace( |
| oldRoute: routeB, |
| newRoute: MaterialPageRoute<int>( |
| settings: const RouteSettings(name: 'D'), |
| builder: (BuildContext context) { |
| log.add('building D'); |
| return const Text('D'); |
| }, |
| ), |
| ); |
| }, |
| ); |
| }, |
| )); |
| }, |
| ); |
| }, |
| )); |
| }, |
| ), |
| )); |
| expect(log, <String>[]); |
| await tester.tap(find.text('A')); |
| await tester.pumpAndSettle(const Duration(milliseconds: 10)); |
| expect(log, <String>['building B']); |
| await tester.tap(find.text('B')); |
| await tester.pumpAndSettle(const Duration(milliseconds: 10)); |
| expect(log, <String>['building B', 'building C', 'found C']); |
| await tester.tap(find.text('C')); |
| await tester.pumpAndSettle(const Duration(milliseconds: 10)); |
| expect(log, <String>['building B', 'building C', 'found C', 'building D']); |
| key.currentState!.pop<void>(); |
| await tester.pumpAndSettle(const Duration(milliseconds: 10)); |
| expect(log, <String>['building B', 'building C', 'found C', 'building D']); |
| }); |
| |
| testWidgets('Routes don\'t rebuild just because their animations ended', (WidgetTester tester) async { |
| final GlobalKey<NavigatorState> key = GlobalKey<NavigatorState>(); |
| final List<String> log = <String>[]; |
| Route<dynamic>? nextRoute = PageRouteBuilder<int>( |
| pageBuilder: (BuildContext context, Animation<double> animation, Animation<double> secondaryAnimation) { |
| log.add('building page 1 - ${ModalRoute.of(context)!.canPop}'); |
| return const Placeholder(); |
| }, |
| ); |
| await tester.pumpWidget(MaterialApp( |
| navigatorKey: key, |
| onGenerateRoute: (RouteSettings settings) { |
| assert(nextRoute != null); |
| final Route<dynamic> result = nextRoute!; |
| nextRoute = null; |
| return result; |
| }, |
| )); |
| expect(log, <String>['building page 1 - false']); |
| key.currentState!.pushReplacement(PageRouteBuilder<int>( |
| pageBuilder: (BuildContext context, Animation<double> animation, Animation<double> secondaryAnimation) { |
| log.add('building page 2 - ${ModalRoute.of(context)!.canPop}'); |
| return const Placeholder(); |
| }, |
| )); |
| expect(log, <String>['building page 1 - false']); |
| await tester.pump(); |
| expect(log, <String>['building page 1 - false', 'building page 2 - false']); |
| await tester.pump(const Duration(milliseconds: 150)); |
| expect(log, <String>['building page 1 - false', 'building page 2 - false']); |
| key.currentState!.pushReplacement(PageRouteBuilder<int>( |
| pageBuilder: (BuildContext context, Animation<double> animation, Animation<double> secondaryAnimation) { |
| log.add('building page 3 - ${ModalRoute.of(context)!.canPop}'); |
| return const Placeholder(); |
| }, |
| )); |
| expect(log, <String>['building page 1 - false', 'building page 2 - false']); |
| await tester.pump(); |
| expect(log, <String>['building page 1 - false', 'building page 2 - false', 'building page 3 - false']); |
| await tester.pump(const Duration(milliseconds: 200)); |
| expect(log, <String>['building page 1 - false', 'building page 2 - false', 'building page 3 - false']); |
| }); |
| |
| testWidgets('route semantics', (WidgetTester tester) async { |
| final SemanticsTester semantics = SemanticsTester(tester); |
| final Map<String, WidgetBuilder> routes = <String, WidgetBuilder>{ |
| '/': (BuildContext context) => OnTapPage(id: '1', onTap: () { Navigator.pushNamed(context, '/A'); }), |
| '/A': (BuildContext context) => OnTapPage(id: '2', onTap: () { Navigator.pushNamed(context, '/B/C'); }), |
| '/B/C': (BuildContext context) => const OnTapPage(id: '3'), |
| }; |
| |
| await tester.pumpWidget(MaterialApp(routes: routes)); |
| |
| expect(semantics, includesNodeWith( |
| flags: <SemanticsFlag>[SemanticsFlag.scopesRoute], |
| )); |
| expect(semantics, includesNodeWith( |
| label: 'Page 1', |
| flags: <SemanticsFlag>[ |
| SemanticsFlag.namesRoute, |
| SemanticsFlag.isHeader, |
| ], |
| )); |
| |
| await tester.tap(find.text('1')); // pushNamed('/A') |
| await tester.pump(); |
| await tester.pump(const Duration(seconds: 1)); |
| |
| expect(semantics, includesNodeWith( |
| flags: <SemanticsFlag>[SemanticsFlag.scopesRoute], |
| )); |
| expect(semantics, includesNodeWith( |
| label: 'Page 2', |
| flags: <SemanticsFlag>[ |
| SemanticsFlag.namesRoute, |
| SemanticsFlag.isHeader, |
| ], |
| )); |
| |
| await tester.tap(find.text('2')); // pushNamed('/B/C') |
| await tester.pump(); |
| await tester.pump(const Duration(seconds: 1)); |
| |
| expect(semantics, includesNodeWith( |
| flags: <SemanticsFlag>[ |
| SemanticsFlag.scopesRoute, |
| ], |
| )); |
| expect(semantics, includesNodeWith( |
| label: 'Page 3', |
| flags: <SemanticsFlag>[ |
| SemanticsFlag.namesRoute, |
| SemanticsFlag.isHeader, |
| ], |
| )); |
| |
| |
| semantics.dispose(); |
| }); |
| |
| testWidgets('arguments for named routes on Navigator', (WidgetTester tester) async { |
| late GlobalKey currentRouteKey; |
| final List<Object?> arguments = <Object?>[]; |
| |
| await tester.pumpWidget(MaterialApp( |
| onGenerateRoute: (RouteSettings settings) { |
| arguments.add(settings.arguments); |
| return MaterialPageRoute<void>( |
| settings: settings, |
| builder: (BuildContext context) => Center(key: currentRouteKey = GlobalKey(), child: Text(settings.name!)), |
| ); |
| }, |
| )); |
| |
| expect(find.text('/'), findsOneWidget); |
| expect(arguments.single, isNull); |
| arguments.clear(); |
| |
| Navigator.pushNamed( |
| currentRouteKey.currentContext!, |
| '/A', |
| arguments: 'pushNamed', |
| ); |
| await tester.pumpAndSettle(); |
| |
| expect(find.text('/'), findsNothing); |
| expect(find.text('/A'), findsOneWidget); |
| expect(arguments.single, 'pushNamed'); |
| arguments.clear(); |
| |
| Navigator.popAndPushNamed( |
| currentRouteKey.currentContext!, |
| '/B', |
| arguments: 'popAndPushNamed', |
| ); |
| await tester.pumpAndSettle(); |
| |
| expect(find.text('/'), findsNothing); |
| expect(find.text('/A'), findsNothing); |
| expect(find.text('/B'), findsOneWidget); |
| expect(arguments.single, 'popAndPushNamed'); |
| arguments.clear(); |
| |
| Navigator.pushNamedAndRemoveUntil( |
| currentRouteKey.currentContext!, |
| '/C', |
| (Route<dynamic> route) => route.isFirst, |
| arguments: 'pushNamedAndRemoveUntil', |
| ); |
| await tester.pumpAndSettle(); |
| |
| expect(find.text('/'), findsNothing); |
| expect(find.text('/A'), findsNothing); |
| expect(find.text('/B'), findsNothing); |
| expect(find.text('/C'), findsOneWidget); |
| expect(arguments.single, 'pushNamedAndRemoveUntil'); |
| arguments.clear(); |
| |
| Navigator.pushReplacementNamed( |
| currentRouteKey.currentContext!, |
| '/D', |
| arguments: 'pushReplacementNamed', |
| ); |
| await tester.pumpAndSettle(); |
| |
| expect(find.text('/'), findsNothing); |
| expect(find.text('/A'), findsNothing); |
| expect(find.text('/B'), findsNothing); |
| expect(find.text('/C'), findsNothing); |
| expect(find.text('/D'), findsOneWidget); |
| expect(arguments.single, 'pushReplacementNamed'); |
| arguments.clear(); |
| }); |
| |
| testWidgets('arguments for named routes on NavigatorState', (WidgetTester tester) async { |
| final GlobalKey<NavigatorState> navigatorKey = GlobalKey<NavigatorState>(); |
| final List<Object?> arguments = <Object?>[]; |
| |
| await tester.pumpWidget(MaterialApp( |
| navigatorKey: navigatorKey, |
| onGenerateRoute: (RouteSettings settings) { |
| arguments.add(settings.arguments); |
| return MaterialPageRoute<void>( |
| settings: settings, |
| builder: (BuildContext context) => Center(child: Text(settings.name!)), |
| ); |
| }, |
| )); |
| |
| expect(find.text('/'), findsOneWidget); |
| expect(arguments.single, isNull); |
| arguments.clear(); |
| |
| navigatorKey.currentState!.pushNamed( |
| '/A', |
| arguments:'pushNamed', |
| ); |
| await tester.pumpAndSettle(); |
| |
| expect(find.text('/'), findsNothing); |
| expect(find.text('/A'), findsOneWidget); |
| expect(arguments.single, 'pushNamed'); |
| arguments.clear(); |
| |
| navigatorKey.currentState!.popAndPushNamed( |
| '/B', |
| arguments: 'popAndPushNamed', |
| ); |
| await tester.pumpAndSettle(); |
| |
| expect(find.text('/'), findsNothing); |
| expect(find.text('/A'), findsNothing); |
| expect(find.text('/B'), findsOneWidget); |
| expect(arguments.single, 'popAndPushNamed'); |
| arguments.clear(); |
| |
| navigatorKey.currentState!.pushNamedAndRemoveUntil( |
| '/C', |
| (Route<dynamic> route) => route.isFirst, |
| arguments: 'pushNamedAndRemoveUntil', |
| ); |
| await tester.pumpAndSettle(); |
| |
| expect(find.text('/'), findsNothing); |
| expect(find.text('/A'), findsNothing); |
| expect(find.text('/B'), findsNothing); |
| expect(find.text('/C'), findsOneWidget); |
| expect(arguments.single, 'pushNamedAndRemoveUntil'); |
| arguments.clear(); |
| |
| navigatorKey.currentState!.pushReplacementNamed( |
| '/D', |
| arguments: 'pushReplacementNamed', |
| ); |
| await tester.pumpAndSettle(); |
| |
| expect(find.text('/'), findsNothing); |
| expect(find.text('/A'), findsNothing); |
| expect(find.text('/B'), findsNothing); |
| expect(find.text('/C'), findsNothing); |
| expect(find.text('/D'), findsOneWidget); |
| expect(arguments.single, 'pushReplacementNamed'); |
| arguments.clear(); |
| }); |
| |
| testWidgets('Initial route can have gaps', (WidgetTester tester) async { |
| final GlobalKey<NavigatorState> keyNav = GlobalKey<NavigatorState>(); |
| const Key keyRoot = Key('Root'); |
| const Key keyA = Key('A'); |
| const Key keyABC = Key('ABC'); |
| |
| await tester.pumpWidget( |
| MaterialApp( |
| navigatorKey: keyNav, |
| initialRoute: '/A/B/C', |
| routes: <String, WidgetBuilder>{ |
| '/': (BuildContext context) => Container(key: keyRoot), |
| '/A': (BuildContext context) => Container(key: keyA), |
| // The route /A/B is intentionally left out. |
| '/A/B/C': (BuildContext context) => Container(key: keyABC), |
| }, |
| ), |
| ); |
| |
| // The initial route /A/B/C should've been pushed successfully. |
| expect(find.byKey(keyRoot, skipOffstage: false), findsOneWidget); |
| expect(find.byKey(keyA, skipOffstage: false), findsOneWidget); |
| expect(find.byKey(keyABC), findsOneWidget); |
| |
| keyNav.currentState!.pop(); |
| await tester.pumpAndSettle(); |
| expect(find.byKey(keyRoot, skipOffstage: false), findsOneWidget); |
| expect(find.byKey(keyA), findsOneWidget); |
| expect(find.byKey(keyABC, skipOffstage: false), findsNothing); |
| }); |
| |
| testWidgets('The full initial route has to be matched', (WidgetTester tester) async { |
| final GlobalKey<NavigatorState> keyNav = GlobalKey<NavigatorState>(); |
| const Key keyRoot = Key('Root'); |
| const Key keyA = Key('A'); |
| const Key keyAB = Key('AB'); |
| |
| await tester.pumpWidget( |
| MaterialApp( |
| navigatorKey: keyNav, |
| initialRoute: '/A/B/C', |
| routes: <String, WidgetBuilder>{ |
| '/': (BuildContext context) => Container(key: keyRoot), |
| '/A': (BuildContext context) => Container(key: keyA), |
| '/A/B': (BuildContext context) => Container(key: keyAB), |
| // The route /A/B/C is intentionally left out. |
| }, |
| ), |
| ); |
| |
| final dynamic exception = tester.takeException(); |
| expect(exception, isA<String>()); |
| expect(exception.startsWith('Could not navigate to initial route.'), isTrue); |
| |
| // Only the root route should've been pushed. |
| expect(find.byKey(keyRoot), findsOneWidget); |
| expect(find.byKey(keyA), findsNothing); |
| expect(find.byKey(keyAB), findsNothing); |
| }); |
| |
| testWidgets("Popping immediately after pushing doesn't crash", (WidgetTester tester) async { |
| // Added this test to protect against regression of https://github.com/flutter/flutter/issues/45539 |
| final Map<String, WidgetBuilder> routes = <String, WidgetBuilder>{ |
| '/' : (BuildContext context) => OnTapPage(id: '/', onTap: () { |
| Navigator.pushNamed(context, '/A'); |
| Navigator.of(context).pop(); |
| }), |
| '/A': (BuildContext context) => OnTapPage(id: 'A', onTap: () { Navigator.pop(context); }), |
| }; |
| bool isPushed = false; |
| bool isPopped = false; |
| final TestObserver observer = TestObserver() |
| ..onPushed = (Route<dynamic>? route, Route<dynamic>? previousRoute) { |
| // Pushes the initial route. |
| expect(route is PageRoute && route.settings.name == '/', isTrue); |
| expect(previousRoute, isNull); |
| isPushed = true; |
| } |
| ..onPopped = (Route<dynamic>? route, Route<dynamic>? previousRoute) { |
| isPopped = true; |
| }; |
| |
| await tester.pumpWidget(MaterialApp( |
| routes: routes, |
| navigatorObservers: <NavigatorObserver>[observer], |
| )); |
| expect(find.text('/'), findsOneWidget); |
| expect(find.text('A'), findsNothing); |
| expect(isPushed, isTrue); |
| expect(isPopped, isFalse); |
| |
| isPushed = false; |
| isPopped = false; |
| observer.onPushed = (Route<dynamic>? route, Route<dynamic>? previousRoute) { |
| expect(route is PageRoute && route.settings.name == '/A', isTrue); |
| expect(previousRoute is PageRoute && previousRoute.settings.name == '/', isTrue); |
| isPushed = true; |
| }; |
| |
| await tester.tap(find.text('/')); |
| await tester.pump(); |
| await tester.pump(const Duration(seconds: 1)); |
| expect(find.text('/'), findsOneWidget); |
| expect(find.text('A'), findsNothing); |
| expect(isPushed, isTrue); |
| expect(isPopped, isTrue); |
| }); |
| |
| group('error control test', () { |
| testWidgets('onUnknownRoute null and onGenerateRoute returns null', (WidgetTester tester) async { |
| final GlobalKey<NavigatorState> navigatorKey = GlobalKey<NavigatorState>(); |
| await tester.pumpWidget(Navigator( |
| key: navigatorKey, |
| onGenerateRoute: (_) => null, |
| )); |
| final dynamic exception = tester.takeException(); |
| expect(exception, isNotNull); |
| expect(exception, isFlutterError); |
| final FlutterError error = exception as FlutterError; |
| expect(error, isNotNull); |
| expect(error.diagnostics.last, isA<DiagnosticsProperty<NavigatorState>>()); |
| expect( |
| error.toStringDeep(), |
| equalsIgnoringHashCodes( |
| 'FlutterError\n' |
| ' Navigator.onGenerateRoute returned null when requested to build\n' |
| ' route "/".\n' |
| ' The onGenerateRoute callback must never return null, unless an\n' |
| ' onUnknownRoute callback is provided as well.\n' |
| ' The Navigator was:\n' |
| ' NavigatorState#00000(lifecycle state: initialized)\n' |
| ), |
| ); |
| }); |
| |
| testWidgets('onUnknownRoute null and onGenerateRoute returns null', (WidgetTester tester) async { |
| final GlobalKey<NavigatorState> navigatorKey = GlobalKey<NavigatorState>(); |
| await tester.pumpWidget(Navigator( |
| key: navigatorKey, |
| onGenerateRoute: (_) => null, |
| onUnknownRoute: (_) => null, |
| )); |
| final dynamic exception = tester.takeException(); |
| expect(exception, isNotNull); |
| expect(exception, isFlutterError); |
| final FlutterError error = exception as FlutterError; |
| expect(error, isNotNull); |
| expect(error.diagnostics.last, isA<DiagnosticsProperty<NavigatorState>>()); |
| expect( |
| error.toStringDeep(), |
| equalsIgnoringHashCodes( |
| 'FlutterError\n' |
| ' Navigator.onUnknownRoute returned null when requested to build\n' |
| ' route "/".\n' |
| ' The onUnknownRoute callback must never return null.\n' |
| ' The Navigator was:\n' |
| ' NavigatorState#00000(lifecycle state: initialized)\n', |
| ), |
| ); |
| }); |
| }); |
| |
| testWidgets('OverlayEntry of topmost initial route is marked as opaque', (WidgetTester tester) async { |
| // Regression test for https://github.com/flutter/flutter/issues/38038. |
| |
| final Key root = UniqueKey(); |
| final Key intermediate = UniqueKey(); |
| final GlobalKey topmost = GlobalKey(); |
| await tester.pumpWidget( |
| MaterialApp( |
| initialRoute: '/A/B', |
| routes: <String, WidgetBuilder>{ |
| '/': (BuildContext context) => Container(key: root), |
| '/A': (BuildContext context) => Container(key: intermediate), |
| '/A/B': (BuildContext context) => Container(key: topmost), |
| }, |
| ), |
| ); |
| |
| expect(ModalRoute.of(topmost.currentContext!)!.overlayEntries.first.opaque, isTrue); |
| |
| expect(find.byKey(root), findsNothing); // hidden by opaque Route |
| expect(find.byKey(intermediate), findsNothing); // hidden by opaque Route |
| expect(find.byKey(topmost), findsOneWidget); |
| }); |
| |
| testWidgets('OverlayEntry of topmost route is set to opaque after Push', (WidgetTester tester) async { |
| // Regression test for https://github.com/flutter/flutter/issues/38038. |
| |
| final GlobalKey<NavigatorState> navigator = GlobalKey<NavigatorState>(); |
| await tester.pumpWidget( |
| MaterialApp( |
| navigatorKey: navigator, |
| initialRoute: '/', |
| onGenerateRoute: (RouteSettings settings) { |
| return NoAnimationPageRoute( |
| pageBuilder: (_) => Container(key: ValueKey<String>(settings.name!)), |
| ); |
| }, |
| ), |
| ); |
| expect(find.byKey(const ValueKey<String>('/')), findsOneWidget); |
| |
| navigator.currentState!.pushNamed('/A'); |
| await tester.pump(); |
| |
| final BuildContext topMostContext = tester.element(find.byKey(const ValueKey<String>('/A'))); |
| expect(ModalRoute.of(topMostContext)!.overlayEntries.first.opaque, isTrue); |
| |
| expect(find.byKey(const ValueKey<String>('/')), findsNothing); // hidden by /A |
| expect(find.byKey(const ValueKey<String>('/A')), findsOneWidget); |
| }); |
| |
| testWidgets('OverlayEntry of topmost route is set to opaque after Replace', (WidgetTester tester) async { |
| // Regression test for https://github.com/flutter/flutter/issues/38038. |
| |
| final GlobalKey<NavigatorState> navigator = GlobalKey<NavigatorState>(); |
| await tester.pumpWidget( |
| MaterialApp( |
| navigatorKey: navigator, |
| initialRoute: '/A/B', |
| onGenerateRoute: (RouteSettings settings) { |
| return NoAnimationPageRoute( |
| pageBuilder: (_) => Container(key: ValueKey<String>(settings.name!)), |
| ); |
| }, |
| ), |
| ); |
| expect(find.byKey(const ValueKey<String>('/')), findsNothing); |
| expect(find.byKey(const ValueKey<String>('/A')), findsNothing); |
| expect(find.byKey(const ValueKey<String>('/A/B')), findsOneWidget); |
| |
| final Route<dynamic> oldRoute = ModalRoute.of( |
| tester.element(find.byKey(const ValueKey<String>('/A'), skipOffstage: false)), |
| )!; |
| final Route<void> newRoute = NoAnimationPageRoute( |
| pageBuilder: (_) => Container(key: const ValueKey<String>('/C')), |
| ); |
| |
| navigator.currentState!.replace<void>(oldRoute: oldRoute, newRoute: newRoute); |
| await tester.pump(); |
| |
| expect(newRoute.overlayEntries.first.opaque, isTrue); |
| |
| expect(find.byKey(const ValueKey<String>('/')), findsNothing); // hidden by /A/B |
| expect(find.byKey(const ValueKey<String>('/A')), findsNothing); // replaced |
| expect(find.byKey(const ValueKey<String>('/C')), findsNothing); // hidden by /A/B |
| expect(find.byKey(const ValueKey<String>('/A/B')), findsOneWidget); |
| |
| navigator.currentState!.pop(); |
| await tester.pumpAndSettle(); |
| |
| expect(find.byKey(const ValueKey<String>('/')), findsNothing); // hidden by /C |
| expect(find.byKey(const ValueKey<String>('/A')), findsNothing); // replaced |
| expect(find.byKey(const ValueKey<String>('/A/B')), findsNothing); // popped |
| expect(find.byKey(const ValueKey<String>('/C')), findsOneWidget); |
| }); |
| |
| testWidgets('Pushing opaque Route does not rebuild routes below', (WidgetTester tester) async { |
| // Regression test for https://github.com/flutter/flutter/issues/45797. |
| |
| final GlobalKey<NavigatorState> navigator = GlobalKey<NavigatorState>(); |
| final Key bottomRoute = UniqueKey(); |
| final Key topRoute = UniqueKey(); |
| await tester.pumpWidget( |
| MaterialApp( |
| theme: ThemeData( |
| pageTransitionsTheme: const PageTransitionsTheme( |
| builders: <TargetPlatform, PageTransitionsBuilder>{ |
| TargetPlatform.android: FadeUpwardsPageTransitionsBuilder(), |
| }, |
| ), |
| ), |
| navigatorKey: navigator, |
| routes: <String, WidgetBuilder>{ |
| '/' : (BuildContext context) => StatefulTestWidget(key: bottomRoute), |
| '/a': (BuildContext context) => StatefulTestWidget(key: topRoute), |
| }, |
| ), |
| ); |
| expect(tester.state<StatefulTestState>(find.byKey(bottomRoute)).rebuildCount, 1); |
| |
| navigator.currentState!.pushNamed('/a'); |
| await tester.pumpAndSettle(); |
| |
| // Bottom route is offstage and did not rebuild. |
| expect(find.byKey(bottomRoute), findsNothing); |
| expect(tester.state<StatefulTestState>(find.byKey(bottomRoute, skipOffstage: false)).rebuildCount, 1); |
| |
| expect(tester.state<StatefulTestState>(find.byKey(topRoute)).rebuildCount, 1); |
| }); |
| |
| testWidgets('initial routes below opaque route are offstage', (WidgetTester tester) async { |
| final GlobalKey<NavigatorState> testKey = GlobalKey<NavigatorState>(); |
| await tester.pumpWidget( |
| Directionality( |
| textDirection: TextDirection.ltr, |
| child: Navigator( |
| key: testKey, |
| initialRoute: '/a/b', |
| onGenerateRoute: (RouteSettings s) { |
| return MaterialPageRoute<void>( |
| builder: (BuildContext c) { |
| return Text('+${s.name}+'); |
| }, |
| settings: s, |
| ); |
| }, |
| ), |
| ), |
| ); |
| |
| expect(find.text('+/+'), findsNothing); |
| expect(find.text('+/+', skipOffstage: false), findsOneWidget); |
| expect(find.text('+/a+'), findsNothing); |
| expect(find.text('+/a+', skipOffstage: false), findsOneWidget); |
| expect(find.text('+/a/b+'), findsOneWidget); |
| |
| testKey.currentState!.pop(); |
| await tester.pumpAndSettle(); |
| |
| expect(find.text('+/+'), findsNothing); |
| expect(find.text('+/+', skipOffstage: false), findsOneWidget); |
| expect(find.text('+/a+'), findsOneWidget); |
| expect(find.text('+/a/b+'), findsNothing); |
| |
| testKey.currentState!.pop(); |
| await tester.pumpAndSettle(); |
| |
| expect(find.text('+/+'), findsOneWidget); |
| expect(find.text('+/a+'), findsNothing); |
| expect(find.text('+/a/b+'), findsNothing); |
| }); |
| |
| testWidgets('Can provide custom onGenerateInitialRoutes', (WidgetTester tester) async { |
| bool onGenerateInitialRoutesCalled = false; |
| final GlobalKey<NavigatorState> testKey = GlobalKey<NavigatorState>(); |
| await tester.pumpWidget( |
| Directionality( |
| textDirection: TextDirection.ltr, |
| child: Navigator( |
| key: testKey, |
| initialRoute: 'Hello World', |
| onGenerateInitialRoutes: (NavigatorState navigator, String initialRoute) { |
| onGenerateInitialRoutesCalled = true; |
| final List<Route<void>> result = <Route<void>>[]; |
| for (final String route in initialRoute.split(' ')) { |
| result.add(MaterialPageRoute<void>(builder: (BuildContext context) { |
| return Text(route); |
| })); |
| } |
| return result; |
| }, |
| ), |
| ), |
| ); |
| |
| expect(onGenerateInitialRoutesCalled, true); |
| expect(find.text('Hello'), findsNothing); |
| expect(find.text('World'), findsOneWidget); |
| |
| testKey.currentState!.pop(); |
| await tester.pumpAndSettle(); |
| |
| expect(find.text('Hello'), findsOneWidget); |
| expect(find.text('World'), findsNothing); |
| }); |
| |
| testWidgets('Navigator.of able to handle input context is a navigator context', (WidgetTester tester) async { |
| final GlobalKey<NavigatorState> testKey = GlobalKey<NavigatorState>(); |
| await tester.pumpWidget( |
| MaterialApp( |
| navigatorKey: testKey, |
| home: const Text('home'), |
| ) |
| ); |
| |
| final NavigatorState state = Navigator.of(testKey.currentContext!); |
| expect(state, testKey.currentState); |
| }); |
| |
| testWidgets('Navigator.of able to handle input context is a navigator context - root navigator', (WidgetTester tester) async { |
| final GlobalKey<NavigatorState> root = GlobalKey<NavigatorState>(); |
| final GlobalKey<NavigatorState> sub = GlobalKey<NavigatorState>(); |
| await tester.pumpWidget( |
| MaterialApp( |
| navigatorKey: root, |
| home: Navigator( |
| key: sub, |
| onGenerateRoute: (RouteSettings settings) { |
| return MaterialPageRoute<void>( |
| settings: settings, |
| builder: (BuildContext context) => const Text('dummy'), |
| ); |
| }, |
| ), |
| ) |
| ); |
| |
| final NavigatorState state = Navigator.of(sub.currentContext!, rootNavigator: true); |
| expect(state, root.currentState); |
| }); |
| |
| testWidgets('Navigator.maybeOf throws when there is no navigator', (WidgetTester tester) async { |
| final GlobalKey<NavigatorState> testKey = GlobalKey<NavigatorState>(); |
| await tester.pumpWidget(SizedBox(key: testKey)); |
| |
| expect(() async { |
| Navigator.of(testKey.currentContext!); |
| }, throwsFlutterError); |
| }); |
| |
| testWidgets('Navigator.maybeOf works when there is no navigator', (WidgetTester tester) async { |
| final GlobalKey<NavigatorState> testKey = GlobalKey<NavigatorState>(); |
| await tester.pumpWidget(SizedBox(key: testKey)); |
| |
| final NavigatorState? state = Navigator.maybeOf(testKey.currentContext!); |
| expect(state, isNull); |
| }); |
| |
| testWidgets('Navigator.maybeOf able to handle input context is a navigator context', (WidgetTester tester) async { |
| final GlobalKey<NavigatorState> testKey = GlobalKey<NavigatorState>(); |
| await tester.pumpWidget( |
| MaterialApp( |
| navigatorKey: testKey, |
| home: const Text('home'), |
| ) |
| ); |
| |
| final NavigatorState? state = Navigator.maybeOf(testKey.currentContext!); |
| expect(state, isNotNull); |
| expect(state, testKey.currentState); |
| }); |
| |
| testWidgets('Navigator.maybeOf able to handle input context is a navigator context - root navigator', (WidgetTester tester) async { |
| final GlobalKey<NavigatorState> root = GlobalKey<NavigatorState>(); |
| final GlobalKey<NavigatorState> sub = GlobalKey<NavigatorState>(); |
| await tester.pumpWidget( |
| MaterialApp( |
| navigatorKey: root, |
| home: Navigator( |
| key: sub, |
| onGenerateRoute: (RouteSettings settings) { |
| return MaterialPageRoute<void>( |
| settings: settings, |
| builder: (BuildContext context) => const Text('dummy'), |
| ); |
| }, |
| ), |
| ) |
| ); |
| |
| final NavigatorState? state = Navigator.maybeOf(sub.currentContext!, rootNavigator: true); |
| expect(state, isNotNull); |
| expect(state, root.currentState); |
| }); |
| |
| testWidgets('pushAndRemove until animates the push', (WidgetTester tester) async { |
| // Regression test for https://github.com/flutter/flutter/issues/25080. |
| |
| const Duration kFourTenthsOfTheTransitionDuration = Duration(milliseconds: 120); |
| final GlobalKey<NavigatorState> navigator = GlobalKey<NavigatorState>(); |
| final Map<String, MaterialPageRoute<dynamic>> routeNameToContext = <String, MaterialPageRoute<dynamic>>{}; |
| |
| await tester.pumpWidget( |
| Directionality( |
| textDirection: TextDirection.ltr, |
| child: Navigator( |
| key: navigator, |
| initialRoute: 'root', |
| onGenerateRoute: (RouteSettings settings) { |
| return MaterialPageRoute<void>( |
| settings: settings, |
| builder: (BuildContext context) { |
| routeNameToContext[settings.name!] = ModalRoute.of(context)! as MaterialPageRoute<dynamic>; |
| return Text('Route: ${settings.name}'); |
| }, |
| ); |
| }, |
| ), |
| ), |
| ); |
| |
| expect(find.text('Route: root'), findsOneWidget); |
| |
| navigator.currentState!.pushNamed('1'); |
| await tester.pumpAndSettle(); |
| |
| expect(find.text('Route: 1'), findsOneWidget); |
| |
| navigator.currentState!.pushNamed('2'); |
| await tester.pumpAndSettle(); |
| |
| expect(find.text('Route: 2'), findsOneWidget); |
| |
| navigator.currentState!.pushNamed('3'); |
| await tester.pumpAndSettle(); |
| |
| expect(find.text('Route: 3'), findsOneWidget); |
| expect(find.text('Route: 2', skipOffstage: false), findsOneWidget); |
| expect(find.text('Route: 1', skipOffstage: false), findsOneWidget); |
| expect(find.text('Route: root', skipOffstage: false), findsOneWidget); |
| |
| navigator.currentState!.pushNamedAndRemoveUntil('4', (Route<dynamic> route) => route.isFirst); |
| await tester.pump(); |
| |
| expect(find.text('Route: 3'), findsOneWidget); |
| expect(find.text('Route: 4'), findsOneWidget); |
| final Animation<double> route4Entry = routeNameToContext['4']!.animation!; |
| expect(route4Entry.value, 0.0); // Entry animation has not started. |
| |
| await tester.pump(kFourTenthsOfTheTransitionDuration); |
| expect(find.text('Route: 3'), findsOneWidget); |
| expect(find.text('Route: 4'), findsOneWidget); |
| expect(route4Entry.value, 0.4); |
| |
| await tester.pump(kFourTenthsOfTheTransitionDuration); |
| expect(find.text('Route: 3'), findsOneWidget); |
| expect(find.text('Route: 4'), findsOneWidget); |
| expect(route4Entry.value, 0.8); |
| expect(find.text('Route: 2', skipOffstage: false), findsOneWidget); |
| expect(find.text('Route: 1', skipOffstage: false), findsOneWidget); |
| expect(find.text('Route: root', skipOffstage: false), findsOneWidget); |
| |
| // When we hit 1.0 all but root and current have been removed. |
| await tester.pump(kFourTenthsOfTheTransitionDuration); |
| expect(find.text('Route: 3', skipOffstage: false), findsNothing); |
| expect(find.text('Route: 4'), findsOneWidget); |
| expect(route4Entry.value, 1.0); |
| expect(find.text('Route: 2', skipOffstage: false), findsNothing); |
| expect(find.text('Route: 1', skipOffstage: false), findsNothing); |
| expect(find.text('Route: root', skipOffstage: false), findsOneWidget); |
| |
| navigator.currentState!.pop(); |
| await tester.pumpAndSettle(); |
| |
| expect(find.text('Route: root'), findsOneWidget); |
| expect(find.text('Route: 4', skipOffstage: false), findsNothing); |
| }); |
| |
| testWidgets('Wrapping TickerMode can turn off ticking in routes', (WidgetTester tester) async { |
| int tickCount = 0; |
| Widget widgetUnderTest({required bool enabled}) { |
| return TickerMode( |
| enabled: enabled, |
| child: Directionality( |
| textDirection: TextDirection.ltr, |
| child: Navigator( |
| initialRoute: 'root', |
| onGenerateRoute: (RouteSettings settings) { |
| return MaterialPageRoute<void>( |
| settings: settings, |
| builder: (BuildContext context) { |
| return _TickingWidget( |
| onTick: () { |
| tickCount++; |
| }, |
| ); |
| }, |
| ); |
| }, |
| ), |
| ), |
| ); |
| } |
| |
| await tester.pumpWidget(widgetUnderTest(enabled: false)); |
| expect(tickCount, 0); |
| await tester.pump(const Duration(seconds: 1)); |
| await tester.pump(const Duration(seconds: 1)); |
| await tester.pump(const Duration(seconds: 1)); |
| await tester.pump(const Duration(seconds: 1)); |
| expect(tickCount, 0); |
| |
| await tester.pumpWidget(widgetUnderTest(enabled: true)); |
| expect(tickCount, 0); |
| await tester.pump(const Duration(seconds: 1)); |
| await tester.pump(const Duration(seconds: 1)); |
| await tester.pump(const Duration(seconds: 1)); |
| await tester.pump(const Duration(seconds: 1)); |
| expect(tickCount, 4); |
| }); |
| |
| testWidgets('Route announce correctly for first route and last route', (WidgetTester tester) async { |
| // Regression test for https://github.com/flutter/flutter/issues/57133. |
| Route<void>? previousOfFirst = NotAnnounced(); |
| Route<void>? nextOfFirst = NotAnnounced(); |
| Route<void>? popNextOfFirst = NotAnnounced(); |
| Route<void>? firstRoute; |
| |
| Route<void>? previousOfSecond = NotAnnounced(); |
| Route<void>? nextOfSecond = NotAnnounced(); |
| Route<void>? popNextOfSecond = NotAnnounced(); |
| Route<void>? secondRoute; |
| |
| final GlobalKey<NavigatorState> navigator = GlobalKey<NavigatorState>(); |
| await tester.pumpWidget( |
| MaterialApp( |
| navigatorKey: navigator, |
| initialRoute: '/second', |
| onGenerateRoute: (RouteSettings settings) { |
| if (settings.name == '/') { |
| firstRoute = RouteAnnouncementSpy( |
| onDidChangeNext: (Route<void>? next) => nextOfFirst = next, |
| onDidChangePrevious: (Route<void>? previous) => previousOfFirst = previous, |
| onDidPopNext: (Route<void>? next) => popNextOfFirst = next, |
| settings: settings, |
| ); |
| return firstRoute; |
| } |
| secondRoute = RouteAnnouncementSpy( |
| onDidChangeNext: (Route<void>? next) => nextOfSecond = next, |
| onDidChangePrevious: (Route<void>? previous) => previousOfSecond = previous, |
| onDidPopNext: (Route<void>? next) => popNextOfSecond = next, |
| settings: settings, |
| ); |
| return secondRoute; |
| }, |
| ), |
| ); |
| await tester.pumpAndSettle(); |
| |
| expect(previousOfFirst, isNull); |
| expect(nextOfFirst, secondRoute); |
| expect(popNextOfFirst, isA<NotAnnounced>()); |
| |
| expect(previousOfSecond, firstRoute); |
| expect(nextOfSecond, isNull); |
| expect(popNextOfSecond, isA<NotAnnounced>()); |
| |
| navigator.currentState!.pop(); |
| expect(popNextOfFirst, secondRoute); |
| }); |
| |
| testWidgets('hero controller scope works', (WidgetTester tester) async { |
| final GlobalKey<NavigatorState> top = GlobalKey<NavigatorState>(); |
| final GlobalKey<NavigatorState> sub = GlobalKey<NavigatorState>(); |
| |
| final List<NavigatorObservation> observations = <NavigatorObservation>[]; |
| final HeroControllerSpy spy = HeroControllerSpy() |
| ..onPushed = (Route<dynamic>? route, Route<dynamic>? previousRoute) { |
| observations.add( |
| NavigatorObservation( |
| current: route?.settings.name, |
| previous: previousRoute?.settings.name, |
| operation: 'didPush' |
| ) |
| ); |
| }; |
| await tester.pumpWidget( |
| HeroControllerScope( |
| controller: spy, |
| child: Directionality( |
| textDirection: TextDirection.ltr, |
| child: Navigator( |
| key: top, |
| initialRoute: 'top1', |
| onGenerateRoute: (RouteSettings s) { |
| return MaterialPageRoute<void>( |
| builder: (BuildContext c) { |
| return Navigator( |
| key: sub, |
| initialRoute: 'sub1', |
| onGenerateRoute: (RouteSettings s) { |
| return MaterialPageRoute<void>( |
| builder: (BuildContext c) { |
| return const Placeholder(); |
| }, |
| settings: s, |
| ); |
| }, |
| ); |
| }, |
| settings: s, |
| ); |
| }, |
| ), |
| ) |
| ) |
| ); |
| // It should only observe the top navigator. |
| expect(observations.length, 1); |
| expect(observations[0].current, 'top1'); |
| expect(observations[0].previous, isNull); |
| |
| sub.currentState!.push(MaterialPageRoute<void>( |
| settings: const RouteSettings(name:'sub2'), |
| builder: (BuildContext context) => const Text('sub2') |
| )); |
| await tester.pumpAndSettle(); |
| |
| expect(find.text('sub2'), findsOneWidget); |
| // It should not record sub navigator. |
| expect(observations.length, 1); |
| |
| top.currentState!.push(MaterialPageRoute<void>( |
| settings: const RouteSettings(name:'top2'), |
| builder: (BuildContext context) => const Text('top2') |
| )); |
| await tester.pumpAndSettle(); |
| expect(observations.length, 2); |
| expect(observations[1].current, 'top2'); |
| expect(observations[1].previous, 'top1'); |
| }); |
| |
| testWidgets('hero controller can correctly transfer subscription - replacing navigator', (WidgetTester tester) async { |
| final GlobalKey<NavigatorState> key1 = GlobalKey<NavigatorState>(); |
| final GlobalKey<NavigatorState> key2 = GlobalKey<NavigatorState>(); |
| |
| final List<NavigatorObservation> observations = <NavigatorObservation>[]; |
| final HeroControllerSpy spy = HeroControllerSpy() |
| ..onPushed = (Route<dynamic>? route, Route<dynamic>? previousRoute) { |
| observations.add( |
| NavigatorObservation( |
| current: route?.settings.name, |
| previous: previousRoute?.settings.name, |
| operation: 'didPush' |
| ) |
| ); |
| }; |
| await tester.pumpWidget( |
| HeroControllerScope( |
| controller: spy, |
| child: Directionality( |
| textDirection: TextDirection.ltr, |
| child: Navigator( |
| key: key1, |
| initialRoute: 'navigator1', |
| onGenerateRoute: (RouteSettings s) { |
| return MaterialPageRoute<void>( |
| builder: (BuildContext c) { |
| return const Placeholder(); |
| }, |
| settings: s, |
| ); |
| }, |
| ), |
| ) |
| ) |
| ); |
| // Transfer the subscription to another navigator |
| await tester.pumpWidget( |
| HeroControllerScope( |
| controller: spy, |
| child: Directionality( |
| textDirection: TextDirection.ltr, |
| child: Navigator( |
| key: key2, |
| initialRoute: 'navigator2', |
| onGenerateRoute: (RouteSettings s) { |
| return MaterialPageRoute<void>( |
| builder: (BuildContext c) { |
| return const Placeholder(); |
| }, |
| settings: s, |
| ); |
| }, |
| ), |
| ) |
| ) |
| ); |
| observations.clear(); |
| |
| key2.currentState!.push(MaterialPageRoute<void>( |
| settings: const RouteSettings(name:'new route'), |
| builder: (BuildContext context) => const Text('new route') |
| )); |
| await tester.pumpAndSettle(); |
| |
| expect(find.text('new route'), findsOneWidget); |
| // It should record from the new navigator. |
| expect(observations.length, 1); |
| expect(observations[0].current, 'new route'); |
| expect(observations[0].previous, 'navigator2'); |
| }); |
| |
| testWidgets('hero controller can correctly transfer subscription - swapping navigator', (WidgetTester tester) async { |
| final GlobalKey<NavigatorState> key1 = GlobalKey<NavigatorState>(); |
| final GlobalKey<NavigatorState> key2 = GlobalKey<NavigatorState>(); |
| |
| final List<NavigatorObservation> observations1 = <NavigatorObservation>[]; |
| final HeroControllerSpy spy1 = HeroControllerSpy() |
| ..onPushed = (Route<dynamic>? route, Route<dynamic>? previousRoute) { |
| observations1.add( |
| NavigatorObservation( |
| current: route?.settings.name, |
| previous: previousRoute?.settings.name, |
| operation: 'didPush' |
| ) |
| ); |
| }; |
| final List<NavigatorObservation> observations2 = <NavigatorObservation>[]; |
| final HeroControllerSpy spy2 = HeroControllerSpy() |
| ..onPushed = (Route<dynamic>? route, Route<dynamic>? previousRoute) { |
| observations2.add( |
| NavigatorObservation( |
| current: route?.settings.name, |
| previous: previousRoute?.settings.name, |
| operation: 'didPush' |
| ) |
| ); |
| }; |
| await tester.pumpWidget( |
| Directionality( |
| textDirection: TextDirection.ltr, |
| child: Stack( |
| children: <Widget>[ |
| HeroControllerScope( |
| controller: spy1, |
| child: Navigator( |
| key: key1, |
| initialRoute: 'navigator1', |
| onGenerateRoute: (RouteSettings s) { |
| return MaterialPageRoute<void>( |
| builder: (BuildContext c) { |
| return const Placeholder(); |
| }, |
| settings: s, |
| ); |
| }, |
| ) |
| ), |
| HeroControllerScope( |
| controller: spy2, |
| child: Navigator( |
| key: key2, |
| initialRoute: 'navigator2', |
| onGenerateRoute: (RouteSettings s) { |
| return MaterialPageRoute<void>( |
| builder: (BuildContext c) { |
| return const Placeholder(); |
| }, |
| settings: s, |
| ); |
| }, |
| ) |
| ), |
| ], |
| ), |
| ), |
| ); |
| expect(observations1.length, 1); |
| expect(observations1[0].current, 'navigator1'); |
| expect(observations1[0].previous, isNull); |
| expect(observations2.length, 1); |
| expect(observations2[0].current, 'navigator2'); |
| expect(observations2[0].previous, isNull); |
| |
| // Swaps the spies. |
| await tester.pumpWidget( |
| Directionality( |
| textDirection: TextDirection.ltr, |
| child: Stack( |
| children: <Widget>[ |
| HeroControllerScope( |
| controller: spy2, |
| child: Navigator( |
| key: key1, |
| initialRoute: 'navigator1', |
| onGenerateRoute: (RouteSettings s) { |
| return MaterialPageRoute<void>( |
| builder: (BuildContext c) { |
| return const Placeholder(); |
| }, |
| settings: s, |
| ); |
| }, |
| ) |
| ), |
| HeroControllerScope( |
| controller: spy1, |
| child: Navigator( |
| key: key2, |
| initialRoute: 'navigator2', |
| onGenerateRoute: (RouteSettings s) { |
| return MaterialPageRoute<void>( |
| builder: (BuildContext c) { |
| return const Placeholder(); |
| }, |
| settings: s, |
| ); |
| }, |
| ) |
| ), |
| ], |
| ), |
| ), |
| ); |
| |
| // Pushes a route to navigator2. |
| key2.currentState!.push(MaterialPageRoute<void>( |
| settings: const RouteSettings(name:'new route2'), |
| builder: (BuildContext context) => const Text('new route2') |
| )); |
| await tester.pumpAndSettle(); |
| expect(find.text('new route2'), findsOneWidget); |
| // The spy1 should record the push in navigator2. |
| expect(observations1.length, 2); |
| expect(observations1[1].current, 'new route2'); |
| expect(observations1[1].previous, 'navigator2'); |
| // The spy2 should not record anything. |
| expect(observations2.length, 1); |
| |
| // Pushes a route to navigator1 |
| key1.currentState!.push(MaterialPageRoute<void>( |
| settings: const RouteSettings(name:'new route1'), |
| builder: (BuildContext context) => const Text('new route1') |
| )); |
| await tester.pumpAndSettle(); |
| expect(find.text('new route1'), findsOneWidget); |
| // The spy1 should not record anything. |
| expect(observations1.length, 2); |
| // The spy2 should record the push in navigator1. |
| expect(observations2.length, 2); |
| expect(observations2[1].current, 'new route1'); |
| expect(observations2[1].previous, 'navigator1'); |
| }); |
| |
| testWidgets('hero controller subscribes to multiple navigators does throw', (WidgetTester tester) async { |
| final HeroControllerSpy spy = HeroControllerSpy(); |
| await tester.pumpWidget( |
| HeroControllerScope( |
| controller: spy, |
| child: Directionality( |
| textDirection: TextDirection.ltr, |
| child: Stack( |
| children: <Widget>[ |
| Navigator( |
| initialRoute: 'navigator1', |
| onGenerateRoute: (RouteSettings s) { |
| return MaterialPageRoute<void>( |
| builder: (BuildContext c) { |
| return const Placeholder(); |
| }, |
| settings: s, |
| ); |
| }, |
| ), |
| Navigator( |
| initialRoute: 'navigator2', |
| onGenerateRoute: (RouteSettings s) { |
| return MaterialPageRoute<void>( |
| builder: (BuildContext c) { |
| return const Placeholder(); |
| }, |
| settings: s, |
| ); |
| }, |
| ), |
| ], |
| ), |
| ), |
| ), |
| ); |
| expect(tester.takeException(), isAssertionError); |
| }); |
| |
| testWidgets('hero controller throws has correct error message', (WidgetTester tester) async { |
| final HeroControllerSpy spy = HeroControllerSpy(); |
| await tester.pumpWidget( |
| HeroControllerScope( |
| controller: spy, |
| child: Directionality( |
| textDirection: TextDirection.ltr, |
| child: Stack( |
| children: <Widget>[ |
| Navigator( |
| initialRoute: 'navigator1', |
| onGenerateRoute: (RouteSettings s) { |
| return MaterialPageRoute<void>( |
| builder: (BuildContext c) { |
| return const Placeholder(); |
| }, |
| settings: s, |
| ); |
| }, |
| ), |
| Navigator( |
| initialRoute: 'navigator2', |
| onGenerateRoute: (RouteSettings s) { |
| return MaterialPageRoute<void>( |
| builder: (BuildContext c) { |
| return const Placeholder(); |
| }, |
| settings: s, |
| ); |
| }, |
| ), |
| ], |
| ), |
| ), |
| ), |
| ); |
| |
| final dynamic exception = tester.takeException(); |
| expect(exception, isFlutterError); |
| final FlutterError error = exception as FlutterError; |
| expect( |
| error.toStringDeep(), |
| equalsIgnoringHashCodes( |
| 'FlutterError\n' |
| ' A HeroController can not be shared by multiple Navigators. The\n' |
| ' Navigators that share the same HeroController are:\n' |
| ' - NavigatorState#00000(tickers: tracking 1 ticker)\n' |
| ' - NavigatorState#00000(tickers: tracking 1 ticker)\n' |
| ' Please create a HeroControllerScope for each Navigator or use a\n' |
| ' HeroControllerScope.none to prevent subtree from receiving a\n' |
| ' HeroController.\n' |
| '' |
| ), |
| ); |
| }); |
| |
| group('Page api', (){ |
| Widget buildNavigator({ |
| required List<Page<dynamic>> pages, |
| required PopPageCallback onPopPage, |
| GlobalKey<NavigatorState>? key, |
| TransitionDelegate<dynamic>? transitionDelegate, |
| List<NavigatorObserver> observers = const <NavigatorObserver>[], |
| }) { |
| 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, |
| observers: observers, |
| transitionDelegate: transitionDelegate ?? const DefaultTransitionDelegate<dynamic>(), |
| ), |
| ), |
| ), |
| ); |
| } |
| |
| testWidgets('can initialize with pages list', (WidgetTester tester) async { |
| final GlobalKey<NavigatorState> navigator = GlobalKey<NavigatorState>(); |
| final List<TestPage> myPages = <TestPage>[ |
| const TestPage(key: ValueKey<String>('1'), name:'initial'), |
| const TestPage(key: ValueKey<String>('2'), name:'second'), |
| const TestPage(key: ValueKey<String>('3'), name:'third'), |
| ]; |
| |
| bool onPopPage(Route<dynamic> route, dynamic result) { |
| myPages.removeWhere((Page<dynamic> page) => route.settings == page); |
| return route.didPop(result); |
| } |
| |
| await tester.pumpWidget( |
| buildNavigator(pages: myPages, onPopPage: onPopPage, key: navigator) |
| ); |
| expect(find.text('third'), findsOneWidget); |
| expect(find.text('second'), findsNothing); |
| expect(find.text('initial'), findsNothing); |
| |
| navigator.currentState!.pop(); |
| await tester.pumpAndSettle(); |
| expect(find.text('third'), findsNothing); |
| expect(find.text('second'), findsOneWidget); |
| expect(find.text('initial'), findsNothing); |
| |
| navigator.currentState!.pop(); |
| await tester.pumpAndSettle(); |
| expect(find.text('third'), findsNothing); |
| expect(find.text('second'), findsNothing); |
| expect(find.text('initial'), findsOneWidget); |
| }); |
| |
| testWidgets('throw if onPopPage callback is not provided', (WidgetTester tester) async { |
| final List<TestPage> myPages = <TestPage>[ |
| const TestPage(key: ValueKey<String>('1'), name:'initial'), |
| const TestPage(key: ValueKey<String>('2'), name:'second'), |
| const TestPage(key: ValueKey<String>('3'), name:'third'), |
| ]; |
| |
| await tester.pumpWidget( |
| 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( |
| pages: myPages, |
| ), |
| ), |
| ), |
| ) |
| ); |
| |
| final dynamic exception = tester.takeException(); |
| expect(exception, isFlutterError); |
| final FlutterError error = exception as FlutterError; |
| expect( |
| error.toStringDeep(), |
| equalsIgnoringHashCodes( |
| 'FlutterError\n' |
| ' The Navigator.onPopPage must be provided to use the\n' |
| ' Navigator.pages API\n' |
| '' |
| ), |
| ); |
| }); |
| |
| Widget _buildFrame(String action) { |
| const TestPage myPage = TestPage(key: ValueKey<String>('1'), name:'initial'); |
| final Map<String, WidgetBuilder> routes = <String, WidgetBuilder>{ |
| '/' : (BuildContext context) => OnTapPage( |
| id: action, |
| onTap: (){ |
| if (action == 'push') { |
| Navigator.of(context).push(myPage.createRoute(context)); |
| } else if (action == 'pushReplacement') { |
| Navigator.of(context).pushReplacement(myPage.createRoute(context)); |
| } else if (action == 'pushAndRemoveUntil') { |
| Navigator.of(context).pushAndRemoveUntil(myPage.createRoute(context), (_) => true); |
| } |
| }, |
| ), |
| }; |
| |
| return MaterialApp(routes: routes); |
| } |
| |
| void _checkException(WidgetTester tester) { |
| final dynamic exception = tester.takeException(); |
| expect(exception, isFlutterError); |
| final FlutterError error = exception as FlutterError; |
| expect( |
| error.toStringDeep(), |
| equalsIgnoringHashCodes( |
| 'FlutterError\n' |
| ' A page-based route should not be added using the imperative api.\n' |
| ' Provide a new list with the corresponding Page to Navigator.pages\n' |
| ' instead.\n' |
| '' |
| ), |
| ); |
| } |
| |
| testWidgets('throw if add page-based route usi
|