| // Copyright 2013 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. |
| |
| // ignore_for_file: cascade_invocations, diagnostic_describe_all_properties |
| |
| import 'dart:async'; |
| |
| import 'package:flutter/foundation.dart'; |
| import 'package:flutter/material.dart'; |
| import 'package:flutter/services.dart'; |
| import 'package:flutter_test/flutter_test.dart'; |
| import 'package:go_router/go_router.dart'; |
| import 'package:go_router/src/delegate.dart'; |
| import 'package:go_router/src/match.dart'; |
| import 'package:go_router/src/matching.dart'; |
| import 'package:logging/logging.dart'; |
| |
| import 'test_helpers.dart'; |
| |
| const bool enableLogs = true; |
| final Logger log = Logger('GoRouter tests'); |
| |
| Future<void> sendPlatformUrl(String url) async { |
| final Map<String, dynamic> testRouteInformation = <String, dynamic>{ |
| 'location': url, |
| }; |
| final ByteData message = const JSONMethodCodec().encodeMethodCall( |
| MethodCall('pushRouteInformation', testRouteInformation), |
| ); |
| await ServicesBinding.instance.defaultBinaryMessenger |
| .handlePlatformMessage('flutter/navigation', message, (_) {}); |
| } |
| |
| void main() { |
| if (enableLogs) { |
| Logger.root.onRecord.listen((LogRecord e) => debugPrint('$e')); |
| } |
| |
| group('path routes', () { |
| testWidgets('match home route', (WidgetTester tester) async { |
| final List<GoRoute> routes = <GoRoute>[ |
| GoRoute( |
| path: '/', |
| builder: (BuildContext context, GoRouterState state) => |
| const HomeScreen()), |
| ]; |
| |
| final GoRouter router = await createRouter(routes, tester); |
| final RouteMatchList matches = router.routerDelegate.matches; |
| expect(matches.matches, hasLength(1)); |
| expect(matches.uri.toString(), '/'); |
| expect(find.byType(HomeScreen), findsOneWidget); |
| }); |
| |
| testWidgets('If there is more than one route to match, use the first match', |
| (WidgetTester tester) async { |
| final List<GoRoute> routes = <GoRoute>[ |
| GoRoute(name: '1', path: '/', builder: dummy), |
| GoRoute(name: '2', path: '/', builder: dummy), |
| ]; |
| |
| final GoRouter router = await createRouter(routes, tester); |
| router.go('/'); |
| final List<RouteMatch> matches = router.routerDelegate.matches.matches; |
| expect(matches, hasLength(1)); |
| expect((matches.first.route as GoRoute).name, '1'); |
| expect(find.byType(DummyScreen), findsOneWidget); |
| }); |
| |
| test('empty path', () { |
| expect(() { |
| GoRoute(path: ''); |
| }, throwsA(isAssertionError)); |
| }); |
| |
| test('leading / on sub-route', () { |
| expect(() { |
| GoRouter( |
| routes: <RouteBase>[ |
| GoRoute( |
| path: '/', |
| builder: dummy, |
| routes: <GoRoute>[ |
| GoRoute( |
| path: '/foo', |
| builder: dummy, |
| ), |
| ], |
| ), |
| ], |
| ); |
| }, throwsA(isAssertionError)); |
| }); |
| |
| test('trailing / on sub-route', () { |
| expect(() { |
| GoRouter( |
| routes: <RouteBase>[ |
| GoRoute( |
| path: '/', |
| builder: dummy, |
| routes: <GoRoute>[ |
| GoRoute( |
| path: 'foo/', |
| builder: dummy, |
| ), |
| ], |
| ), |
| ], |
| ); |
| }, throwsA(isAssertionError)); |
| }); |
| |
| testWidgets('lack of leading / on top-level route', |
| (WidgetTester tester) async { |
| await expectLater(() async { |
| final List<GoRoute> routes = <GoRoute>[ |
| GoRoute(path: 'foo', builder: dummy), |
| ]; |
| await createRouter(routes, tester); |
| }, throwsA(isAssertionError)); |
| }); |
| |
| testWidgets('match no routes', (WidgetTester tester) async { |
| final List<GoRoute> routes = <GoRoute>[ |
| GoRoute(path: '/', builder: dummy), |
| ]; |
| |
| final GoRouter router = await createRouter(routes, tester); |
| router.go('/foo'); |
| await tester.pumpAndSettle(); |
| final List<RouteMatch> matches = router.routerDelegate.matches.matches; |
| expect(matches, hasLength(1)); |
| expect(find.byType(TestErrorScreen), findsOneWidget); |
| }); |
| |
| testWidgets('match 2nd top level route', (WidgetTester tester) async { |
| final List<GoRoute> routes = <GoRoute>[ |
| GoRoute( |
| path: '/', |
| builder: (BuildContext context, GoRouterState state) => |
| const HomeScreen()), |
| GoRoute( |
| path: '/login', |
| builder: (BuildContext context, GoRouterState state) => |
| const LoginScreen()), |
| ]; |
| |
| final GoRouter router = await createRouter(routes, tester); |
| router.go('/login'); |
| await tester.pumpAndSettle(); |
| final List<RouteMatch> matches = router.routerDelegate.matches.matches; |
| expect(matches, hasLength(1)); |
| expect(matches.first.subloc, '/login'); |
| expect(find.byType(LoginScreen), findsOneWidget); |
| }); |
| |
| testWidgets('match 2nd top level route with subroutes', |
| (WidgetTester tester) async { |
| final List<GoRoute> routes = <GoRoute>[ |
| GoRoute( |
| path: '/', |
| builder: (BuildContext context, GoRouterState state) => |
| const HomeScreen(), |
| routes: <GoRoute>[ |
| GoRoute( |
| path: 'page1', |
| builder: (BuildContext context, GoRouterState state) => |
| const Page1Screen()) |
| ], |
| ), |
| GoRoute( |
| path: '/login', |
| builder: (BuildContext context, GoRouterState state) => |
| const LoginScreen()), |
| ]; |
| |
| final GoRouter router = await createRouter(routes, tester); |
| router.go('/login'); |
| await tester.pumpAndSettle(); |
| final List<RouteMatch> matches = router.routerDelegate.matches.matches; |
| expect(matches, hasLength(1)); |
| expect(matches.first.subloc, '/login'); |
| expect(find.byType(LoginScreen), findsOneWidget); |
| }); |
| |
| testWidgets('match top level route when location has trailing /', |
| (WidgetTester tester) async { |
| final List<GoRoute> routes = <GoRoute>[ |
| GoRoute( |
| path: '/', |
| builder: (BuildContext context, GoRouterState state) => |
| const HomeScreen(), |
| ), |
| GoRoute( |
| path: '/login', |
| builder: (BuildContext context, GoRouterState state) => |
| const LoginScreen(), |
| ), |
| ]; |
| |
| final GoRouter router = await createRouter(routes, tester); |
| router.go('/login/'); |
| await tester.pumpAndSettle(); |
| final List<RouteMatch> matches = router.routerDelegate.matches.matches; |
| expect(matches, hasLength(1)); |
| expect(matches.first.subloc, '/login'); |
| expect(find.byType(LoginScreen), findsOneWidget); |
| }); |
| |
| testWidgets('match top level route when location has trailing / (2)', |
| (WidgetTester tester) async { |
| final List<GoRoute> routes = <GoRoute>[ |
| GoRoute( |
| path: '/profile', |
| builder: dummy, |
| redirect: (_, __) => '/profile/foo'), |
| GoRoute(path: '/profile/:kind', builder: dummy), |
| ]; |
| |
| final GoRouter router = await createRouter(routes, tester); |
| router.go('/profile/'); |
| await tester.pumpAndSettle(); |
| final List<RouteMatch> matches = router.routerDelegate.matches.matches; |
| expect(matches, hasLength(1)); |
| expect(matches.first.subloc, '/profile/foo'); |
| expect(find.byType(DummyScreen), findsOneWidget); |
| }); |
| |
| testWidgets('match top level route when location has trailing / (3)', |
| (WidgetTester tester) async { |
| final List<GoRoute> routes = <GoRoute>[ |
| GoRoute( |
| path: '/profile', |
| builder: dummy, |
| redirect: (_, __) => '/profile/foo'), |
| GoRoute(path: '/profile/:kind', builder: dummy), |
| ]; |
| |
| final GoRouter router = await createRouter(routes, tester); |
| router.go('/profile/?bar=baz'); |
| await tester.pumpAndSettle(); |
| final List<RouteMatch> matches = router.routerDelegate.matches.matches; |
| expect(matches, hasLength(1)); |
| expect(matches.first.subloc, '/profile/foo'); |
| expect(find.byType(DummyScreen), findsOneWidget); |
| }); |
| |
| testWidgets('can access GoRouter parameters from builder', |
| (WidgetTester tester) async { |
| final List<GoRoute> routes = <GoRoute>[ |
| GoRoute(path: '/', redirect: (_, __) => '/1'), |
| GoRoute( |
| path: '/:id', |
| builder: (BuildContext context, GoRouterState state) { |
| return Text(GoRouter.of(context).location); |
| }), |
| ]; |
| |
| final GoRouter router = await createRouter(routes, tester); |
| expect(find.text('/1'), findsOneWidget); |
| router.go('/123?id=456'); |
| await tester.pumpAndSettle(); |
| expect(find.text('/123?id=456'), findsOneWidget); |
| }); |
| |
| testWidgets('can access GoRouter parameters from error builder', |
| (WidgetTester tester) async { |
| final List<GoRoute> routes = <GoRoute>[ |
| GoRoute(path: '/', builder: dummy), |
| ]; |
| |
| final GoRouter router = await createRouter(routes, tester, |
| errorBuilder: (BuildContext context, GoRouterState state) { |
| return Text(GoRouter.of(context).location); |
| }); |
| router.go('/123?id=456'); |
| await tester.pumpAndSettle(); |
| expect(find.text('/123?id=456'), findsOneWidget); |
| router.go('/1234?id=456'); |
| await tester.pumpAndSettle(); |
| expect(find.text('/1234?id=456'), findsOneWidget); |
| }); |
| |
| testWidgets('match sub-route', (WidgetTester tester) async { |
| final List<GoRoute> routes = <GoRoute>[ |
| GoRoute( |
| path: '/', |
| builder: (BuildContext context, GoRouterState state) => |
| const HomeScreen(), |
| routes: <GoRoute>[ |
| GoRoute( |
| path: 'login', |
| builder: (BuildContext context, GoRouterState state) => |
| const LoginScreen(), |
| ), |
| ], |
| ), |
| ]; |
| |
| final GoRouter router = await createRouter(routes, tester); |
| router.go('/login'); |
| await tester.pumpAndSettle(); |
| final List<RouteMatch> matches = router.routerDelegate.matches.matches; |
| expect(matches.length, 2); |
| expect(matches.first.subloc, '/'); |
| expect(find.byType(HomeScreen, skipOffstage: false), findsOneWidget); |
| expect(matches[1].subloc, '/login'); |
| expect(find.byType(LoginScreen), findsOneWidget); |
| }); |
| |
| testWidgets('match sub-routes', (WidgetTester tester) async { |
| final List<GoRoute> routes = <GoRoute>[ |
| GoRoute( |
| path: '/', |
| builder: (BuildContext context, GoRouterState state) => |
| const HomeScreen(), |
| routes: <GoRoute>[ |
| GoRoute( |
| path: 'family/:fid', |
| builder: (BuildContext context, GoRouterState state) => |
| const FamilyScreen('dummy'), |
| routes: <GoRoute>[ |
| GoRoute( |
| path: 'person/:pid', |
| builder: (BuildContext context, GoRouterState state) => |
| const PersonScreen('dummy', 'dummy'), |
| ), |
| ], |
| ), |
| GoRoute( |
| path: 'login', |
| builder: (BuildContext context, GoRouterState state) => |
| const LoginScreen(), |
| ), |
| ], |
| ), |
| ]; |
| |
| final GoRouter router = await createRouter(routes, tester); |
| { |
| final RouteMatchList matches = router.routerDelegate.matches; |
| expect(matches.matches, hasLength(1)); |
| expect(matches.uri.toString(), '/'); |
| expect(find.byType(HomeScreen), findsOneWidget); |
| } |
| |
| router.go('/login'); |
| await tester.pumpAndSettle(); |
| { |
| final RouteMatchList matches = router.routerDelegate.matches; |
| expect(matches.matches.length, 2); |
| expect(matches.matches.first.subloc, '/'); |
| expect(find.byType(HomeScreen, skipOffstage: false), findsOneWidget); |
| expect(matches.matches[1].subloc, '/login'); |
| expect(find.byType(LoginScreen), findsOneWidget); |
| } |
| |
| router.go('/family/f2'); |
| await tester.pumpAndSettle(); |
| { |
| final RouteMatchList matches = router.routerDelegate.matches; |
| expect(matches.matches.length, 2); |
| expect(matches.matches.first.subloc, '/'); |
| expect(find.byType(HomeScreen, skipOffstage: false), findsOneWidget); |
| expect(matches.matches[1].subloc, '/family/f2'); |
| expect(find.byType(FamilyScreen), findsOneWidget); |
| } |
| |
| router.go('/family/f2/person/p1'); |
| await tester.pumpAndSettle(); |
| { |
| final RouteMatchList matches = router.routerDelegate.matches; |
| expect(matches.matches.length, 3); |
| expect(matches.matches.first.subloc, '/'); |
| expect(find.byType(HomeScreen, skipOffstage: false), findsOneWidget); |
| expect(matches.matches[1].subloc, '/family/f2'); |
| expect(find.byType(FamilyScreen, skipOffstage: false), findsOneWidget); |
| expect(matches.matches[2].subloc, '/family/f2/person/p1'); |
| expect(find.byType(PersonScreen), findsOneWidget); |
| } |
| }); |
| |
| testWidgets('return first matching route if too many subroutes', |
| (WidgetTester tester) async { |
| final List<GoRoute> routes = <GoRoute>[ |
| GoRoute( |
| path: '/', |
| builder: (BuildContext context, GoRouterState state) => |
| const HomeScreen(), |
| routes: <GoRoute>[ |
| GoRoute( |
| path: 'foo/bar', |
| builder: (BuildContext context, GoRouterState state) => |
| const FamilyScreen(''), |
| ), |
| GoRoute( |
| path: 'bar', |
| builder: (BuildContext context, GoRouterState state) => |
| const Page1Screen(), |
| ), |
| GoRoute( |
| path: 'foo', |
| builder: (BuildContext context, GoRouterState state) => |
| const Page2Screen(), |
| routes: <GoRoute>[ |
| GoRoute( |
| path: 'bar', |
| builder: (BuildContext context, GoRouterState state) => |
| const LoginScreen(), |
| ), |
| ], |
| ), |
| ], |
| ), |
| ]; |
| |
| final GoRouter router = await createRouter(routes, tester); |
| router.go('/bar'); |
| await tester.pumpAndSettle(); |
| List<RouteMatch> matches = router.routerDelegate.matches.matches; |
| expect(matches, hasLength(2)); |
| expect(find.byType(Page1Screen), findsOneWidget); |
| |
| router.go('/foo/bar'); |
| await tester.pumpAndSettle(); |
| matches = router.routerDelegate.matches.matches; |
| expect(matches, hasLength(2)); |
| expect(find.byType(FamilyScreen), findsOneWidget); |
| |
| router.go('/foo'); |
| await tester.pumpAndSettle(); |
| matches = router.routerDelegate.matches.matches; |
| expect(matches, hasLength(2)); |
| expect(find.byType(Page2Screen), findsOneWidget); |
| }); |
| |
| testWidgets('router state', (WidgetTester tester) async { |
| final List<GoRoute> routes = <GoRoute>[ |
| GoRoute( |
| name: 'home', |
| path: '/', |
| builder: (BuildContext context, GoRouterState state) { |
| expect(state.location, '/'); |
| expect(state.subloc, '/'); |
| expect(state.name, 'home'); |
| expect(state.path, '/'); |
| expect(state.fullpath, '/'); |
| expect(state.params, <String, String>{}); |
| expect(state.error, null); |
| if (state.extra != null) { |
| expect(state.extra! as int, 1); |
| } |
| return const HomeScreen(); |
| }, |
| routes: <GoRoute>[ |
| GoRoute( |
| name: 'login', |
| path: 'login', |
| builder: (BuildContext context, GoRouterState state) { |
| expect(state.location, '/login'); |
| expect(state.subloc, '/login'); |
| expect(state.name, 'login'); |
| expect(state.path, 'login'); |
| expect(state.fullpath, '/login'); |
| expect(state.params, <String, String>{}); |
| expect(state.error, null); |
| expect(state.extra! as int, 2); |
| return const LoginScreen(); |
| }, |
| ), |
| GoRoute( |
| name: 'family', |
| path: 'family/:fid', |
| builder: (BuildContext context, GoRouterState state) { |
| expect( |
| state.location, |
| anyOf(<String>['/family/f2', '/family/f2/person/p1']), |
| ); |
| expect(state.subloc, '/family/f2'); |
| expect(state.name, 'family'); |
| expect(state.path, 'family/:fid'); |
| expect(state.fullpath, '/family/:fid'); |
| expect(state.params, <String, String>{'fid': 'f2'}); |
| expect(state.error, null); |
| expect(state.extra! as int, 3); |
| return FamilyScreen(state.params['fid']!); |
| }, |
| routes: <GoRoute>[ |
| GoRoute( |
| name: 'person', |
| path: 'person/:pid', |
| builder: (BuildContext context, GoRouterState state) { |
| expect(state.location, '/family/f2/person/p1'); |
| expect(state.subloc, '/family/f2/person/p1'); |
| expect(state.name, 'person'); |
| expect(state.path, 'person/:pid'); |
| expect(state.fullpath, '/family/:fid/person/:pid'); |
| expect( |
| state.params, |
| <String, String>{'fid': 'f2', 'pid': 'p1'}, |
| ); |
| expect(state.error, null); |
| expect(state.extra! as int, 4); |
| return PersonScreen( |
| state.params['fid']!, state.params['pid']!); |
| }, |
| ), |
| ], |
| ), |
| ], |
| ), |
| ]; |
| |
| final GoRouter router = await createRouter(routes, tester); |
| router.go('/', extra: 1); |
| await tester.pump(); |
| router.push('/login', extra: 2); |
| await tester.pump(); |
| router.push('/family/f2', extra: 3); |
| await tester.pump(); |
| router.push('/family/f2/person/p1', extra: 4); |
| await tester.pump(); |
| }); |
| |
| testWidgets('match path case insensitively', (WidgetTester tester) async { |
| final List<GoRoute> routes = <GoRoute>[ |
| GoRoute( |
| path: '/', |
| builder: (BuildContext context, GoRouterState state) => |
| const HomeScreen(), |
| ), |
| GoRoute( |
| path: '/family/:fid', |
| builder: (BuildContext context, GoRouterState state) => |
| FamilyScreen(state.params['fid']!), |
| ), |
| ]; |
| |
| final GoRouter router = await createRouter(routes, tester); |
| const String loc = '/FaMiLy/f2'; |
| router.go(loc); |
| await tester.pumpAndSettle(); |
| final List<RouteMatch> matches = router.routerDelegate.matches.matches; |
| |
| // NOTE: match the lower case, since subloc is canonicalized to match the |
| // path case whereas the location can be any case; so long as the path |
| // produces a match regardless of the location case, we win! |
| expect(router.location.toLowerCase(), loc.toLowerCase()); |
| |
| expect(matches, hasLength(1)); |
| expect(find.byType(FamilyScreen), findsOneWidget); |
| }); |
| |
| testWidgets( |
| 'If there is more than one route to match, use the first match.', |
| (WidgetTester tester) async { |
| final List<GoRoute> routes = <GoRoute>[ |
| GoRoute(path: '/', builder: dummy), |
| GoRoute(path: '/page1', builder: dummy), |
| GoRoute(path: '/page1', builder: dummy), |
| GoRoute(path: '/:ok', builder: dummy), |
| ]; |
| |
| final GoRouter router = await createRouter(routes, tester); |
| router.go('/user'); |
| await tester.pumpAndSettle(); |
| final List<RouteMatch> matches = router.routerDelegate.matches.matches; |
| expect(matches, hasLength(1)); |
| expect(find.byType(DummyScreen), findsOneWidget); |
| }); |
| |
| testWidgets('Handles the Android back button correctly', |
| (WidgetTester tester) async { |
| final List<RouteBase> routes = <RouteBase>[ |
| GoRoute( |
| path: '/', |
| builder: (BuildContext context, GoRouterState state) { |
| return const Scaffold( |
| body: Text('Screen A'), |
| ); |
| }, |
| routes: <RouteBase>[ |
| GoRoute( |
| path: 'b', |
| builder: (BuildContext context, GoRouterState state) { |
| return const Scaffold( |
| body: Text('Screen B'), |
| ); |
| }, |
| ), |
| ], |
| ), |
| ]; |
| |
| await createRouter(routes, tester, initialLocation: '/b'); |
| expect(find.text('Screen A'), findsNothing); |
| expect(find.text('Screen B'), findsOneWidget); |
| |
| await simulateAndroidBackButton(tester); |
| await tester.pumpAndSettle(); |
| expect(find.text('Screen A'), findsOneWidget); |
| expect(find.text('Screen B'), findsNothing); |
| }); |
| |
| testWidgets('Handles the Android back button correctly with ShellRoute', |
| (WidgetTester tester) async { |
| final GlobalKey<NavigatorState> rootNavigatorKey = |
| GlobalKey<NavigatorState>(); |
| |
| final List<RouteBase> routes = <RouteBase>[ |
| ShellRoute( |
| builder: (BuildContext context, GoRouterState state, Widget child) { |
| return Scaffold( |
| appBar: AppBar(title: const Text('Shell')), |
| body: child, |
| ); |
| }, |
| routes: <GoRoute>[ |
| GoRoute( |
| path: '/a', |
| builder: (BuildContext context, GoRouterState state) { |
| return const Scaffold( |
| body: Text('Screen A'), |
| ); |
| }, |
| routes: <GoRoute>[ |
| GoRoute( |
| path: 'b', |
| builder: (BuildContext context, GoRouterState state) { |
| return const Scaffold( |
| body: Text('Screen B'), |
| ); |
| }, |
| routes: <GoRoute>[ |
| GoRoute( |
| path: 'c', |
| builder: (BuildContext context, GoRouterState state) { |
| return const Scaffold( |
| body: Text('Screen C'), |
| ); |
| }, |
| routes: <GoRoute>[ |
| GoRoute( |
| path: 'd', |
| parentNavigatorKey: rootNavigatorKey, |
| builder: (BuildContext context, GoRouterState state) { |
| return const Scaffold( |
| body: Text('Screen D'), |
| ); |
| }, |
| ), |
| ], |
| ), |
| ], |
| ), |
| ], |
| ), |
| ], |
| ), |
| ]; |
| |
| await createRouter(routes, tester, |
| initialLocation: '/a/b/c/d', navigatorKey: rootNavigatorKey); |
| expect(find.text('Shell'), findsNothing); |
| expect(find.text('Screen A'), findsNothing); |
| expect(find.text('Screen B'), findsNothing); |
| expect(find.text('Screen C'), findsNothing); |
| expect(find.text('Screen D'), findsOneWidget); |
| |
| await simulateAndroidBackButton(tester); |
| await tester.pumpAndSettle(); |
| expect(find.text('Shell'), findsOneWidget); |
| expect(find.text('Screen A'), findsNothing); |
| expect(find.text('Screen B'), findsNothing); |
| expect(find.text('Screen C'), findsOneWidget); |
| expect(find.text('Screen D'), findsNothing); |
| |
| await simulateAndroidBackButton(tester); |
| await tester.pumpAndSettle(); |
| expect(find.text('Shell'), findsOneWidget); |
| expect(find.text('Screen A'), findsNothing); |
| expect(find.text('Screen B'), findsOneWidget); |
| expect(find.text('Screen C'), findsNothing); |
| }); |
| |
| testWidgets( |
| 'Handles the Android back button when parentNavigatorKey is set to the root navigator', |
| (WidgetTester tester) async { |
| final List<MethodCall> log = <MethodCall>[]; |
| TestDefaultBinaryMessengerBinding.instance!.defaultBinaryMessenger |
| .setMockMethodCallHandler(SystemChannels.platform, |
| (MethodCall methodCall) async { |
| log.add(methodCall); |
| return null; |
| }); |
| |
| Future<void> verify(AsyncCallback test, List<Object> expectations) async { |
| log.clear(); |
| await test(); |
| expect(log, expectations); |
| } |
| |
| final GlobalKey<NavigatorState> rootNavigatorKey = |
| GlobalKey<NavigatorState>(); |
| |
| final List<RouteBase> routes = <RouteBase>[ |
| GoRoute( |
| parentNavigatorKey: rootNavigatorKey, |
| path: '/a', |
| builder: (BuildContext context, GoRouterState state) { |
| return const Scaffold( |
| body: Text('Screen A'), |
| ); |
| }, |
| ), |
| ]; |
| |
| await createRouter(routes, tester, |
| initialLocation: '/a', navigatorKey: rootNavigatorKey); |
| expect(find.text('Screen A'), findsOneWidget); |
| |
| await tester.runAsync(() async { |
| await verify(() => simulateAndroidBackButton(tester), <Object>[ |
| isMethodCall('SystemNavigator.pop', arguments: null), |
| ]); |
| }); |
| }); |
| |
| testWidgets("Handles the Android back button when ShellRoute can't pop", |
| (WidgetTester tester) async { |
| final List<MethodCall> log = <MethodCall>[]; |
| TestDefaultBinaryMessengerBinding.instance!.defaultBinaryMessenger |
| .setMockMethodCallHandler(SystemChannels.platform, |
| (MethodCall methodCall) async { |
| log.add(methodCall); |
| return null; |
| }); |
| |
| Future<void> verify(AsyncCallback test, List<Object> expectations) async { |
| log.clear(); |
| await test(); |
| expect(log, expectations); |
| } |
| |
| final GlobalKey<NavigatorState> rootNavigatorKey = |
| GlobalKey<NavigatorState>(); |
| |
| final List<RouteBase> routes = <RouteBase>[ |
| GoRoute( |
| parentNavigatorKey: rootNavigatorKey, |
| path: '/a', |
| builder: (BuildContext context, GoRouterState state) { |
| return const Scaffold( |
| body: Text('Screen A'), |
| ); |
| }, |
| ), |
| ShellRoute( |
| builder: (BuildContext context, GoRouterState state, Widget child) { |
| return Scaffold( |
| appBar: AppBar( |
| title: const Text('Shell'), |
| ), |
| body: child, |
| ); |
| }, |
| routes: <RouteBase>[ |
| GoRoute( |
| path: '/b', |
| builder: (BuildContext context, GoRouterState state) { |
| return const Scaffold( |
| body: Text('Screen B'), |
| ); |
| }, |
| ), |
| ], |
| ), |
| ]; |
| |
| await createRouter(routes, tester, |
| initialLocation: '/b', navigatorKey: rootNavigatorKey); |
| expect(find.text('Screen B'), findsOneWidget); |
| |
| await tester.runAsync(() async { |
| await verify(() => simulateAndroidBackButton(tester), <Object>[ |
| isMethodCall('SystemNavigator.pop', arguments: null), |
| ]); |
| }); |
| }); |
| }); |
| |
| testWidgets( |
| 'Handles the Android back button when a second Shell has a GoRoute with parentNavigator key', |
| (WidgetTester tester) async { |
| final List<MethodCall> log = <MethodCall>[]; |
| TestDefaultBinaryMessengerBinding.instance!.defaultBinaryMessenger |
| .setMockMethodCallHandler(SystemChannels.platform, |
| (MethodCall methodCall) async { |
| log.add(methodCall); |
| return null; |
| }); |
| |
| Future<void> verify(AsyncCallback test, List<Object> expectations) async { |
| log.clear(); |
| await test(); |
| expect(log, expectations); |
| } |
| |
| final GlobalKey<NavigatorState> rootNavigatorKey = |
| GlobalKey<NavigatorState>(); |
| final GlobalKey<NavigatorState> shellNavigatorKeyA = |
| GlobalKey<NavigatorState>(); |
| final GlobalKey<NavigatorState> shellNavigatorKeyB = |
| GlobalKey<NavigatorState>(); |
| |
| final List<RouteBase> routes = <RouteBase>[ |
| ShellRoute( |
| navigatorKey: shellNavigatorKeyA, |
| builder: (BuildContext context, GoRouterState state, Widget child) { |
| return Scaffold( |
| appBar: AppBar( |
| title: const Text('Shell'), |
| ), |
| body: child, |
| ); |
| }, |
| routes: <RouteBase>[ |
| GoRoute( |
| path: '/a', |
| builder: (BuildContext context, GoRouterState state) { |
| return const Scaffold( |
| body: Text('Screen A'), |
| ); |
| }, |
| routes: <RouteBase>[ |
| ShellRoute( |
| navigatorKey: shellNavigatorKeyB, |
| builder: |
| (BuildContext context, GoRouterState state, Widget child) { |
| return Scaffold( |
| appBar: AppBar( |
| title: const Text('Shell'), |
| ), |
| body: child, |
| ); |
| }, |
| routes: <RouteBase>[ |
| GoRoute( |
| path: 'b', |
| parentNavigatorKey: shellNavigatorKeyB, |
| builder: (BuildContext context, GoRouterState state) { |
| return const Scaffold( |
| body: Text('Screen B'), |
| ); |
| }, |
| ), |
| ], |
| ), |
| ], |
| ), |
| ], |
| ), |
| ]; |
| |
| await createRouter(routes, tester, |
| initialLocation: '/a/b', navigatorKey: rootNavigatorKey); |
| expect(find.text('Screen B'), findsOneWidget); |
| |
| // The first pop should not exit the app. |
| await tester.runAsync(() async { |
| await verify(() => simulateAndroidBackButton(tester), <Object>[]); |
| }); |
| |
| // The second pop should exit the app. |
| await tester.runAsync(() async { |
| await verify(() => simulateAndroidBackButton(tester), <Object>[ |
| isMethodCall('SystemNavigator.pop', arguments: null), |
| ]); |
| }); |
| }); |
| |
| group('report correct url', () { |
| final List<MethodCall> log = <MethodCall>[]; |
| setUp(() { |
| TestDefaultBinaryMessengerBinding.instance!.defaultBinaryMessenger |
| .setMockMethodCallHandler(SystemChannels.navigation, |
| (MethodCall methodCall) async { |
| log.add(methodCall); |
| return null; |
| }); |
| }); |
| tearDown(() { |
| TestDefaultBinaryMessengerBinding.instance!.defaultBinaryMessenger |
| .setMockMethodCallHandler(SystemChannels.navigation, null); |
| log.clear(); |
| }); |
| |
| testWidgets('on push', (WidgetTester tester) async { |
| final List<GoRoute> routes = <GoRoute>[ |
| GoRoute( |
| path: '/', |
| builder: (_, __) => const DummyScreen(), |
| ), |
| GoRoute( |
| path: '/settings', |
| builder: (_, __) => const DummyScreen(), |
| ), |
| ]; |
| |
| final GoRouter router = await createRouter(routes, tester); |
| |
| log.clear(); |
| router.push('/settings'); |
| await tester.pumpAndSettle(); |
| expect(log, <Object>[ |
| isMethodCall('selectMultiEntryHistory', arguments: null), |
| isMethodCall('routeInformationUpdated', arguments: <String, dynamic>{ |
| 'location': '/settings', |
| 'state': null, |
| 'replace': false |
| }), |
| ]); |
| }); |
| |
| testWidgets('on pop', (WidgetTester tester) async { |
| final List<GoRoute> routes = <GoRoute>[ |
| GoRoute( |
| path: '/', |
| builder: (_, __) => const DummyScreen(), |
| routes: <RouteBase>[ |
| GoRoute( |
| path: 'settings', |
| builder: (_, __) => const DummyScreen(), |
| ), |
| ]), |
| ]; |
| |
| final GoRouter router = |
| await createRouter(routes, tester, initialLocation: '/settings'); |
| |
| log.clear(); |
| router.pop(); |
| await tester.pumpAndSettle(); |
| expect(log, <Object>[ |
| isMethodCall('selectMultiEntryHistory', arguments: null), |
| isMethodCall('routeInformationUpdated', arguments: <String, dynamic>{ |
| 'location': '/', |
| 'state': null, |
| 'replace': false |
| }), |
| ]); |
| }); |
| |
| testWidgets('on pop twice', (WidgetTester tester) async { |
| final List<GoRoute> routes = <GoRoute>[ |
| GoRoute( |
| path: '/', |
| builder: (_, __) => const DummyScreen(), |
| routes: <RouteBase>[ |
| GoRoute( |
| path: 'settings', |
| builder: (_, __) => const DummyScreen(), |
| routes: <RouteBase>[ |
| GoRoute( |
| path: 'profile', |
| builder: (_, __) => const DummyScreen(), |
| ), |
| ]), |
| ]), |
| ]; |
| |
| final GoRouter router = await createRouter(routes, tester, |
| initialLocation: '/settings/profile'); |
| |
| log.clear(); |
| router.pop(); |
| router.pop(); |
| await tester.pumpAndSettle(); |
| expect(log, <Object>[ |
| isMethodCall('selectMultiEntryHistory', arguments: null), |
| isMethodCall('routeInformationUpdated', arguments: <String, dynamic>{ |
| 'location': '/', |
| 'state': null, |
| 'replace': false |
| }), |
| ]); |
| }); |
| |
| testWidgets('on pop with path parameters', (WidgetTester tester) async { |
| final List<GoRoute> routes = <GoRoute>[ |
| GoRoute( |
| path: '/', |
| builder: (_, __) => const DummyScreen(), |
| routes: <RouteBase>[ |
| GoRoute( |
| path: 'settings/:id', |
| builder: (_, __) => const DummyScreen(), |
| ), |
| ]), |
| ]; |
| |
| final GoRouter router = |
| await createRouter(routes, tester, initialLocation: '/settings/123'); |
| |
| log.clear(); |
| router.pop(); |
| await tester.pumpAndSettle(); |
| expect(log, <Object>[ |
| isMethodCall('selectMultiEntryHistory', arguments: null), |
| isMethodCall('routeInformationUpdated', arguments: <String, dynamic>{ |
| 'location': '/', |
| 'state': null, |
| 'replace': false |
| }), |
| ]); |
| }); |
| |
| testWidgets('on pop with path parameters case 2', |
| (WidgetTester tester) async { |
| final List<GoRoute> routes = <GoRoute>[ |
| GoRoute( |
| path: '/', |
| builder: (_, __) => const DummyScreen(), |
| routes: <RouteBase>[ |
| GoRoute( |
| path: ':id', |
| builder: (_, __) => const DummyScreen(), |
| ), |
| ]), |
| ]; |
| |
| final GoRouter router = |
| await createRouter(routes, tester, initialLocation: '/123/'); |
| |
| log.clear(); |
| router.pop(); |
| await tester.pumpAndSettle(); |
| expect(log, <Object>[ |
| isMethodCall('selectMultiEntryHistory', arguments: null), |
| isMethodCall('routeInformationUpdated', arguments: <String, dynamic>{ |
| 'location': '/', |
| 'state': null, |
| 'replace': false |
| }), |
| ]); |
| }); |
| |
| testWidgets('Can manually pop root navigator and display correct url', |
| (WidgetTester tester) async { |
| final GlobalKey<NavigatorState> rootNavigatorKey = |
| GlobalKey<NavigatorState>(); |
| |
| final List<RouteBase> routes = <RouteBase>[ |
| GoRoute( |
| path: '/', |
| builder: (BuildContext context, GoRouterState state) { |
| return const Scaffold( |
| body: Text('Home'), |
| ); |
| }, |
| routes: <RouteBase>[ |
| ShellRoute( |
| builder: |
| (BuildContext context, GoRouterState state, Widget child) { |
| return Scaffold( |
| appBar: AppBar(), |
| body: child, |
| ); |
| }, |
| routes: <RouteBase>[ |
| GoRoute( |
| path: 'b', |
| builder: (BuildContext context, GoRouterState state) { |
| return const Scaffold( |
| body: Text('Screen B'), |
| ); |
| }, |
| routes: <RouteBase>[ |
| GoRoute( |
| path: 'c', |
| builder: (BuildContext context, GoRouterState state) { |
| return const Scaffold( |
| body: Text('Screen C'), |
| ); |
| }, |
| ), |
| ], |
| ), |
| ], |
| ), |
| ], |
| ), |
| ]; |
| |
| await createRouter(routes, tester, |
| initialLocation: '/b/c', navigatorKey: rootNavigatorKey); |
| expect(find.text('Screen C'), findsOneWidget); |
| expect(log, <Object>[ |
| isMethodCall('selectMultiEntryHistory', arguments: null), |
| isMethodCall('routeInformationUpdated', arguments: <String, dynamic>{ |
| 'location': '/b/c', |
| 'state': null, |
| 'replace': false |
| }), |
| ]); |
| |
| log.clear(); |
| rootNavigatorKey.currentState!.pop(); |
| await tester.pumpAndSettle(); |
| |
| expect(find.text('Home'), findsOneWidget); |
| expect(log, <Object>[ |
| isMethodCall('selectMultiEntryHistory', arguments: null), |
| isMethodCall('routeInformationUpdated', arguments: <String, dynamic>{ |
| 'location': '/', |
| 'state': null, |
| 'replace': false |
| }), |
| ]); |
| }); |
| |
| testWidgets('works correctly with async redirect', |
| (WidgetTester tester) async { |
| final UniqueKey login = UniqueKey(); |
| final List<GoRoute> routes = <GoRoute>[ |
| GoRoute( |
| path: '/', |
| builder: (_, __) => const DummyScreen(), |
| ), |
| GoRoute( |
| path: '/login', |
| builder: (_, __) => DummyScreen(key: login), |
| ), |
| ]; |
| final Completer<void> completer = Completer<void>(); |
| await createRouter(routes, tester, redirect: (_, __) async { |
| await completer.future; |
| return '/login'; |
| }); |
| await tester.pumpAndSettle(); |
| expect(find.byKey(login), findsNothing); |
| expect(tester.takeException(), isNull); |
| expect(log, <Object>[]); |
| |
| completer.complete(); |
| await tester.pumpAndSettle(); |
| |
| expect(find.byKey(login), findsOneWidget); |
| expect(tester.takeException(), isNull); |
| expect(log, <Object>[ |
| isMethodCall('selectMultiEntryHistory', arguments: null), |
| isMethodCall('routeInformationUpdated', arguments: <String, dynamic>{ |
| 'location': '/login', |
| 'state': null, |
| 'replace': false |
| }), |
| ]); |
| }); |
| }); |
| |
| group('named routes', () { |
| testWidgets('match home route', (WidgetTester tester) async { |
| final List<GoRoute> routes = <GoRoute>[ |
| GoRoute( |
| name: 'home', |
| path: '/', |
| builder: (BuildContext context, GoRouterState state) => |
| const HomeScreen()), |
| ]; |
| |
| final GoRouter router = await createRouter(routes, tester); |
| router.goNamed('home'); |
| }); |
| |
| testWidgets('match too many routes', (WidgetTester tester) async { |
| final List<GoRoute> routes = <GoRoute>[ |
| GoRoute(name: 'home', path: '/', builder: dummy), |
| GoRoute(name: 'home', path: '/', builder: dummy), |
| ]; |
| |
| await expectLater(() async { |
| await createRouter(routes, tester); |
| }, throwsA(isAssertionError)); |
| }); |
| |
| test('empty name', () { |
| expect(() { |
| GoRoute(name: '', path: '/'); |
| }, throwsA(isAssertionError)); |
| }); |
| |
| testWidgets('match no routes', (WidgetTester tester) async { |
| await expectLater(() async { |
| final List<GoRoute> routes = <GoRoute>[ |
| GoRoute(name: 'home', path: '/', builder: dummy), |
| ]; |
| final GoRouter router = await createRouter(routes, tester); |
| router.goNamed('work'); |
| }, throwsA(isAssertionError)); |
| }); |
| |
| testWidgets('match 2nd top level route', (WidgetTester tester) async { |
| final List<GoRoute> routes = <GoRoute>[ |
| GoRoute( |
| name: 'home', |
| path: '/', |
| builder: (BuildContext context, GoRouterState state) => |
| const HomeScreen(), |
| ), |
| GoRoute( |
| name: 'login', |
| path: '/login', |
| builder: (BuildContext context, GoRouterState state) => |
| const LoginScreen(), |
| ), |
| ]; |
| |
| final GoRouter router = await createRouter(routes, tester); |
| router.goNamed('login'); |
| }); |
| |
| testWidgets('match sub-route', (WidgetTester tester) async { |
| final List<GoRoute> routes = <GoRoute>[ |
| GoRoute( |
| name: 'home', |
| path: '/', |
| builder: (BuildContext context, GoRouterState state) => |
| const HomeScreen(), |
| routes: <GoRoute>[ |
| GoRoute( |
| name: 'login', |
| path: 'login', |
| builder: (BuildContext context, GoRouterState state) => |
| const LoginScreen(), |
| ), |
| ], |
| ), |
| ]; |
| |
| final GoRouter router = await createRouter(routes, tester); |
| router.goNamed('login'); |
| }); |
| |
| testWidgets('match w/ params', (WidgetTester tester) async { |
| final List<GoRoute> routes = <GoRoute>[ |
| GoRoute( |
| name: 'home', |
| path: '/', |
| builder: (BuildContext context, GoRouterState state) => |
| const HomeScreen(), |
| routes: <GoRoute>[ |
| GoRoute( |
| name: 'family', |
| path: 'family/:fid', |
| builder: (BuildContext context, GoRouterState state) => |
| const FamilyScreen('dummy'), |
| routes: <GoRoute>[ |
| GoRoute( |
| name: 'person', |
| path: 'person/:pid', |
| builder: (BuildContext context, GoRouterState state) { |
| expect(state.params, |
| <String, String>{'fid': 'f2', 'pid': 'p1'}); |
| return const PersonScreen('dummy', 'dummy'); |
| }, |
| ), |
| ], |
| ), |
| ], |
| ), |
| ]; |
| |
| final GoRouter router = await createRouter(routes, tester); |
| router.goNamed('person', |
| params: <String, String>{'fid': 'f2', 'pid': 'p1'}); |
| }); |
| |
| testWidgets('too few params', (WidgetTester tester) async { |
| final List<GoRoute> routes = <GoRoute>[ |
| GoRoute( |
| name: 'home', |
| path: '/', |
| builder: (BuildContext context, GoRouterState state) => |
| const HomeScreen(), |
| routes: <GoRoute>[ |
| GoRoute( |
| name: 'family', |
| path: 'family/:fid', |
| builder: (BuildContext context, GoRouterState state) => |
| const FamilyScreen('dummy'), |
| routes: <GoRoute>[ |
| GoRoute( |
| name: 'person', |
| path: 'person/:pid', |
| builder: (BuildContext context, GoRouterState state) => |
| const PersonScreen('dummy', 'dummy'), |
| ), |
| ], |
| ), |
| ], |
| ), |
| ]; |
| await expectLater(() async { |
| final GoRouter router = await createRouter(routes, tester); |
| router.goNamed('person', params: <String, String>{'fid': 'f2'}); |
| await tester.pump(); |
| }, throwsA(isAssertionError)); |
| }); |
| |
| testWidgets('match case insensitive w/ params', |
| (WidgetTester tester) async { |
| final List<GoRoute> routes = <GoRoute>[ |
| GoRoute( |
| name: 'home', |
| path: '/', |
| builder: (BuildContext context, GoRouterState state) => |
| const HomeScreen(), |
| routes: <GoRoute>[ |
| GoRoute( |
| name: 'family', |
| path: 'family/:fid', |
| builder: (BuildContext context, GoRouterState state) => |
| const FamilyScreen('dummy'), |
| routes: <GoRoute>[ |
| GoRoute( |
| name: 'PeRsOn', |
| path: 'person/:pid', |
| builder: (BuildContext context, GoRouterState state) { |
| expect(state.params, |
| <String, String>{'fid': 'f2', 'pid': 'p1'}); |
| return const PersonScreen('dummy', 'dummy'); |
| }, |
| ), |
| ], |
| ), |
| ], |
| ), |
| ]; |
| |
| final GoRouter router = await createRouter(routes, tester); |
| router.goNamed('person', |
| params: <String, String>{'fid': 'f2', 'pid': 'p1'}); |
| }); |
| |
| testWidgets('too few params', (WidgetTester tester) async { |
| final List<GoRoute> routes = <GoRoute>[ |
| GoRoute( |
| name: 'family', |
| path: '/family/:fid', |
| builder: (BuildContext context, GoRouterState state) => |
| const FamilyScreen('dummy'), |
| ), |
| ]; |
| await expectLater(() async { |
| final GoRouter router = await createRouter(routes, tester); |
| router.goNamed('family'); |
| }, throwsA(isAssertionError)); |
| }); |
| |
| testWidgets('too many params', (WidgetTester tester) async { |
| final List<GoRoute> routes = <GoRoute>[ |
| GoRoute( |
| name: 'family', |
| path: '/family/:fid', |
| builder: (BuildContext context, GoRouterState state) => |
| const FamilyScreen('dummy'), |
| ), |
| ]; |
| await expectLater(() async { |
| final GoRouter router = await createRouter(routes, tester); |
| router.goNamed('family', |
| params: <String, String>{'fid': 'f2', 'pid': 'p1'}); |
| }, throwsA(isAssertionError)); |
| }); |
| |
| testWidgets('sparsely named routes', (WidgetTester tester) async { |
| final List<GoRoute> routes = <GoRoute>[ |
| GoRoute( |
| path: '/', |
| builder: dummy, |
| redirect: (_, __) => '/family/f2', |
| ), |
| GoRoute( |
| path: '/family/:fid', |
| builder: (BuildContext context, GoRouterState state) => FamilyScreen( |
| state.params['fid']!, |
| ), |
| routes: <GoRoute>[ |
| GoRoute( |
| name: 'person', |
| path: 'person:pid', |
| builder: (BuildContext context, GoRouterState state) => |
| PersonScreen( |
| state.params['fid']!, |
| state.params['pid']!, |
| ), |
| ), |
| ], |
| ), |
| ]; |
| |
| final GoRouter router = await createRouter(routes, tester); |
| router.goNamed('person', |
| params: <String, String>{'fid': 'f2', 'pid': 'p1'}); |
| await tester.pumpAndSettle(); |
| expect(find.byType(PersonScreen), findsOneWidget); |
| }); |
| |
| testWidgets('preserve path param spaces and slashes', |
| (WidgetTester tester) async { |
| const String param1 = 'param w/ spaces and slashes'; |
| final List<GoRoute> routes = <GoRoute>[ |
| GoRoute( |
| name: 'page1', |
| path: '/page1/:param1', |
| builder: (BuildContext c, GoRouterState s) { |
| expect(s.params['param1'], param1); |
| return const DummyScreen(); |
| }, |
| ), |
| ]; |
| |
| final GoRouter router = await createRouter(routes, tester); |
| final String loc = router |
| .namedLocation('page1', params: <String, String>{'param1': param1}); |
| router.go(loc); |
| await tester.pumpAndSettle(); |
| |
| final RouteMatchList matches = router.routerDelegate.matches; |
| expect(find.byType(DummyScreen), findsOneWidget); |
| expect(matches.pathParameters['param1'], param1); |
| }); |
| |
| testWidgets('preserve query param spaces and slashes', |
| (WidgetTester tester) async { |
| const String param1 = 'param w/ spaces and slashes'; |
| final List<GoRoute> routes = <GoRoute>[ |
| GoRoute( |
| name: 'page1', |
| path: '/page1', |
| builder: (BuildContext c, GoRouterState s) { |
| expect(s.queryParams['param1'], param1); |
| return const DummyScreen(); |
| }, |
| ), |
| ]; |
| |
| final GoRouter router = await createRouter(routes, tester); |
| final String loc = router.namedLocation('page1', |
| queryParams: <String, String>{'param1': param1}); |
| router.go(loc); |
| await tester.pumpAndSettle(); |
| final RouteMatchList matches = router.routerDelegate.matches; |
| expect(find.byType(DummyScreen), findsOneWidget); |
| expect(matches.uri.queryParameters['param1'], param1); |
| }); |
| }); |
| |
| group('redirects', () { |
| testWidgets('top-level redirect', (WidgetTester tester) async { |
| final List<GoRoute> routes = <GoRoute>[ |
| GoRoute( |
| path: '/', |
| builder: (BuildContext context, GoRouterState state) => |
| const HomeScreen(), |
| routes: <GoRoute>[ |
| GoRoute( |
| path: 'dummy', |
| builder: (BuildContext context, GoRouterState state) => |
| const DummyScreen()), |
| GoRoute( |
| path: 'login', |
| builder: (BuildContext context, GoRouterState state) => |
| const LoginScreen()), |
| ], |
| ), |
| ]; |
| bool redirected = false; |
| |
| final GoRouter router = await createRouter(routes, tester, |
| redirect: (BuildContext context, GoRouterState state) { |
| redirected = true; |
| return state.subloc == '/login' ? null : '/login'; |
| }); |
| |
| expect(router.location, '/login'); |
| expect(redirected, isTrue); |
| |
| redirected = false; |
| // Directly set the url through platform message. |
| await sendPlatformUrl('/dummy'); |
| |
| await tester.pumpAndSettle(); |
| expect(router.location, '/login'); |
| expect(redirected, isTrue); |
| }); |
| |
| testWidgets('redirect can redirect to same path', |
| (WidgetTester tester) async { |
| final List<GoRoute> routes = <GoRoute>[ |
| GoRoute( |
| path: '/', |
| builder: (BuildContext context, GoRouterState state) => |
| const HomeScreen(), |
| routes: <GoRoute>[ |
| GoRoute( |
| path: 'dummy', |
| // Return same location. |
| redirect: (_, GoRouterState state) => state.location, |
| builder: (BuildContext context, GoRouterState state) => |
| const DummyScreen()), |
| ], |
| ), |
| ]; |
| |
| final GoRouter router = await createRouter(routes, tester, |
| redirect: (BuildContext context, GoRouterState state) { |
| // Return same location. |
| return state.location; |
| }); |
| |
| expect(router.location, '/'); |
| // Directly set the url through platform message. |
| await sendPlatformUrl('/dummy'); |
| await tester.pumpAndSettle(); |
| expect(router.location, '/dummy'); |
| }); |
| |
| testWidgets('top-level redirect w/ named routes', |
| (WidgetTester tester) async { |
| final List<GoRoute> routes = <GoRoute>[ |
| GoRoute( |
| name: 'home', |
| path: '/', |
| builder: (BuildContext context, GoRouterState state) => |
| const HomeScreen(), |
| routes: <GoRoute>[ |
| GoRoute( |
| name: 'dummy', |
| path: 'dummy', |
| builder: (BuildContext context, GoRouterState state) => |
| const DummyScreen(), |
| ), |
| GoRoute( |
| name: 'login', |
| path: 'login', |
| builder: (BuildContext context, GoRouterState state) => |
| const LoginScreen(), |
| ), |
| ], |
| ), |
| ]; |
| |
| final GoRouter router = await createRouter( |
| routes, |
| tester, |
| redirect: (BuildContext context, GoRouterState state) => |
| state.subloc == '/login' ? null : state.namedLocation('login'), |
| ); |
| expect(router.location, '/login'); |
| }); |
| |
| testWidgets('route-level redirect', (WidgetTester tester) async { |
| final List<GoRoute> routes = <GoRoute>[ |
| GoRoute( |
| path: '/', |
| builder: (BuildContext context, GoRouterState state) => |
| const HomeScreen(), |
| routes: <GoRoute>[ |
| GoRoute( |
| path: 'dummy', |
| builder: (BuildContext context, GoRouterState state) => |
| const DummyScreen(), |
| redirect: (BuildContext context, GoRouterState state) => '/login', |
| ), |
| GoRoute( |
| path: 'login', |
| builder: (BuildContext context, GoRouterState state) => |
| const LoginScreen(), |
| ), |
| ], |
| ), |
| ]; |
| |
| final GoRouter router = await createRouter(routes, tester); |
| router.go('/dummy'); |
| await tester.pump(); |
| expect(router.location, '/login'); |
| }); |
| |
| testWidgets('top-level redirect take priority over route level', |
| (WidgetTester tester) async { |
| final List<GoRoute> routes = <GoRoute>[ |
| GoRoute( |
| path: '/', |
| builder: (BuildContext context, GoRouterState state) => |
| const HomeScreen(), |
| routes: <GoRoute>[ |
| GoRoute( |
| path: 'dummy', |
| builder: (BuildContext context, GoRouterState state) => |
| const DummyScreen(), |
| redirect: (BuildContext context, GoRouterState state) { |
| // should never be reached. |
| assert(false); |
| return '/dummy2'; |
| }), |
| GoRoute( |
| path: 'dummy2', |
| builder: (BuildContext context, GoRouterState state) => |
| const DummyScreen()), |
| GoRoute( |
| path: 'login', |
| builder: (BuildContext context, GoRouterState state) => |
| const LoginScreen()), |
| ], |
| ), |
| ]; |
| bool redirected = false; |
| final GoRouter router = await createRouter(routes, tester, |
| redirect: (BuildContext context, GoRouterState state) { |
| redirected = true; |
| return state.subloc == '/login' ? null : '/login'; |
| }); |
| redirected = false; |
| // Directly set the url through platform message. |
| await sendPlatformUrl('/dummy'); |
| |
| await tester.pumpAndSettle(); |
| expect(router.location, '/login'); |
| expect(redirected, isTrue); |
| }); |
| |
| testWidgets('route-level redirect w/ named routes', |
| (WidgetTester tester) async { |
| final List<GoRoute> routes = <GoRoute>[ |
| GoRoute( |
| name: 'home', |
| path: '/', |
| builder: (BuildContext context, GoRouterState state) => |
| const HomeScreen(), |
| routes: <GoRoute>[ |
| GoRoute( |
| name: 'dummy', |
| path: 'dummy', |
| builder: (BuildContext context, GoRouterState state) => |
| const DummyScreen(), |
| redirect: (BuildContext context, GoRouterState state) => |
| state.namedLocation('login'), |
| ), |
| GoRoute( |
| name: 'login', |
| path: 'login', |
| builder: (BuildContext context, GoRouterState state) => |
| const LoginScreen(), |
| ), |
| ], |
| ), |
| ]; |
| |
| final GoRouter router = await createRouter(routes, tester); |
| router.go('/dummy'); |
| await tester.pump(); |
| expect(router.location, '/login'); |
| }); |
| |
| testWidgets('multiple mixed redirect', (WidgetTester tester) async { |
| final List<GoRoute> routes = <GoRoute>[ |
| GoRoute( |
| path: '/', |
| builder: (BuildContext context, GoRouterState state) => |
| const HomeScreen(), |
| routes: <GoRoute>[ |
| GoRoute( |
| path: 'dummy1', |
| builder: (BuildContext context, GoRouterState state) => |
| const DummyScreen(), |
| ), |
| GoRoute( |
| path: 'dummy2', |
| builder: (BuildContext context, GoRouterState state) => |
| const DummyScreen(), |
| redirect: (BuildContext context, GoRouterState state) => '/', |
| ), |
| ], |
| ), |
| ]; |
| |
| final GoRouter router = await createRouter(routes, tester, |
| redirect: (BuildContext context, GoRouterState state) => |
| state.subloc == '/dummy1' ? '/dummy2' : null); |
| router.go('/dummy1'); |
| await tester.pump(); |
| expect(router.location, '/'); |
| }); |
| |
| testWidgets('top-level redirect loop', (WidgetTester tester) async { |
| final GoRouter router = await createRouter(<GoRoute>[], tester, |
| redirect: (BuildContext context, GoRouterState state) => |
| state.subloc == '/' |
| ? '/login' |
| : state.subloc == '/login' |
| ? '/' |
| : null); |
| |
| final List<RouteMatch> matches = router.routerDelegate.matches.matches; |
| expect(matches, hasLength(1)); |
| expect(find.byType(TestErrorScreen), findsOneWidget); |
| final TestErrorScreen screen = |
| tester.widget<TestErrorScreen>(find.byType(TestErrorScreen)); |
| expect(screen.ex, isNotNull); |
| }); |
| |
| testWidgets('route-level redirect loop', (WidgetTester tester) async { |
| final GoRouter router = await createRouter( |
| <GoRoute>[ |
| GoRoute( |
| path: '/', |
| builder: dummy, |
| redirect: (BuildContext context, GoRouterState state) => '/login', |
| ), |
| GoRoute( |
| path: '/login', |
| builder: dummy, |
| redirect: (BuildContext context, GoRouterState state) => '/', |
| ), |
| ], |
| tester, |
| ); |
| |
| final List<RouteMatch> matches = router.routerDelegate.matches.matches; |
| expect(matches, hasLength(1)); |
| expect(find.byType(TestErrorScreen), findsOneWidget); |
| final TestErrorScreen screen = |
| tester.widget<TestErrorScreen>(find.byType(TestErrorScreen)); |
| expect(screen.ex, isNotNull); |
| }); |
| |
| testWidgets('mixed redirect loop', (WidgetTester tester) async { |
| final GoRouter router = await createRouter( |
| <GoRoute>[ |
| GoRoute( |
| path: '/login', |
| builder: dummy, |
| redirect: (BuildContext context, GoRouterState state) => '/', |
| ), |
| ], |
| tester, |
| redirect: (BuildContext context, GoRouterState state) => |
| state.subloc == '/' ? '/login' : null, |
| ); |
| |
| final List<RouteMatch> matches = router.routerDelegate.matches.matches; |
| expect(matches, hasLength(1)); |
| expect(find.byType(TestErrorScreen), findsOneWidget); |
| final TestErrorScreen screen = |
| tester.widget<TestErrorScreen>(find.byType(TestErrorScreen)); |
| expect(screen.ex, isNotNull); |
| }); |
| |
| testWidgets('top-level redirect loop w/ query params', |
| (WidgetTester tester) async { |
| final GoRouter router = await createRouter( |
| <GoRoute>[], |
| tester, |
| redirect: (BuildContext context, GoRouterState state) => |
| state.subloc == '/' |
| ? '/login?from=${state.location}' |
| : state.subloc == '/login' |
| ? '/' |
| : null, |
| ); |
| |
| final List<RouteMatch> matches = router.routerDelegate.matches.matches; |
| expect(matches, hasLength(1)); |
| expect(find.byType(TestErrorScreen), findsOneWidget); |
| final TestErrorScreen screen = |
| tester.widget<TestErrorScreen>(find.byType(TestErrorScreen)); |
| expect(screen.ex, isNotNull); |
| }); |
| |
| testWidgets('expect null path/fullpath on top-level redirect', |
| (WidgetTester tester) async { |
| final List<GoRoute> routes = <GoRoute>[ |
| GoRoute( |
| path: '/', |
| builder: (BuildContext context, GoRouterState state) => |
| const HomeScreen(), |
| ), |
| GoRoute( |
| path: '/dummy', |
| builder: dummy, |
| redirect: (BuildContext context, GoRouterState state) => '/', |
| ), |
| ]; |
| |
| final GoRouter router = await createRouter( |
| routes, |
| tester, |
| initialLocation: '/dummy', |
| ); |
| expect(router.location, '/'); |
| }); |
| |
| testWidgets('top-level redirect state', (WidgetTester tester) async { |
| final List<GoRoute> routes = <GoRoute>[ |
| GoRoute( |
| path: '/', |
| builder: (BuildContext context, GoRouterState state) => |
| const HomeScreen(), |
| ), |
| GoRoute( |
| path: '/login', |
| builder: (BuildContext context, GoRouterState state) => |
| const LoginScreen(), |
| ), |
| ]; |
| |
| final GoRouter router = await createRouter( |
| routes, |
| tester, |
| initialLocation: '/login?from=/', |
| redirect: (BuildContext context, GoRouterState state) { |
| expect(Uri.parse(state.location).queryParameters, isNotEmpty); |
| expect(Uri.parse(state.subloc).queryParameters, isEmpty); |
| expect(state.path, isNull); |
| expect(state.fullpath, isNull); |
| expect(state.params.length, 0); |
| expect(state.queryParams.length, 1); |
| expect(state.queryParams['from'], '/'); |
| return null; |
| }, |
| ); |
| |
| final List<RouteMatch> matches = router.routerDelegate.matches.matches; |
| expect(matches, hasLength(1)); |
| expect(find.byType(LoginScreen), findsOneWidget); |
| }); |
| |
| testWidgets('route-level redirect state', (WidgetTester tester) async { |
| const String loc = '/book/0'; |
| final List<GoRoute> routes = <GoRoute>[ |
| GoRoute( |
| path: '/book/:bookId', |
| redirect: (BuildContext context, GoRouterState state) { |
| expect(state.location, loc); |
| expect(state.subloc, loc); |
| expect(state.path, '/book/:bookId'); |
| expect(state.fullpath, '/book/:bookId'); |
| expect(state.params, <String, String>{'bookId': '0'}); |
| expect(state.queryParams.length, 0); |
| return null; |
| }, |
| builder: (BuildContext c, GoRouterState s) => const HomeScreen(), |
| ), |
| ]; |
| |
| final GoRouter router = await createRouter( |
| routes, |
| tester, |
| initialLocation: loc, |
| ); |
| |
| final List<RouteMatch> matches = router.routerDelegate.matches.matches; |
| expect(matches, hasLength(1)); |
| expect(find.byType(HomeScreen), findsOneWidget); |
| }); |
| |
| testWidgets('sub-sub-route-level redirect params', |
| (WidgetTester tester) async { |
| final List<GoRoute> routes = <GoRoute>[ |
| GoRoute( |
| path: '/', |
| builder: (BuildContext c, GoRouterState s) => const HomeScreen(), |
| routes: <GoRoute>[ |
| GoRoute( |
| path: 'family/:fid', |
| builder: (BuildContext c, GoRouterState s) => |
| FamilyScreen(s.params['fid']!), |
| routes: <GoRoute>[ |
| GoRoute( |
| path: 'person/:pid', |
| redirect: (BuildContext context, GoRouterState s) { |
| expect(s.params['fid'], 'f2'); |
| expect(s.params['pid'], 'p1'); |
| return null; |
| }, |
| builder: (BuildContext c, GoRouterState s) => PersonScreen( |
| s.params['fid']!, |
| s.params['pid']!, |
| ), |
| ), |
| ], |
| ), |
| ], |
| ), |
| ]; |
| |
| final GoRouter router = await createRouter( |
| routes, |
| tester, |
| initialLocation: '/family/f2/person/p1', |
| ); |
| |
| final List<RouteMatch> matches = router.routerDelegate.matches.matches; |
| expect(matches.length, 3); |
| expect(find.byType(HomeScreen, skipOffstage: false), findsOneWidget); |
| expect(find.byType(FamilyScreen, skipOffstage: false), findsOneWidget); |
| final PersonScreen page = |
| tester.widget<PersonScreen>(find.byType(PersonScreen)); |
| expect(page.fid, 'f2'); |
| expect(page.pid, 'p1'); |
| }); |
| |
| testWidgets('redirect limit', (WidgetTester tester) async { |
| final GoRouter router = await createRouter( |
| <GoRoute>[], |
| tester, |
| redirect: (BuildContext context, GoRouterState state) => |
| '/${state.location}+', |
| redirectLimit: 10, |
| ); |
| |
| final List<RouteMatch> matches = router.routerDelegate.matches.matches; |
| expect(matches, hasLength(1)); |
| expect(find.byType(TestErrorScreen), findsOneWidget); |
| final TestErrorScreen screen = |
| tester.widget<TestErrorScreen>(find.byType(TestErrorScreen)); |
| expect(screen.ex, isNotNull); |
| }); |
| |
| testWidgets('extra not null in redirect', (WidgetTester tester) async { |
| bool isCallTopRedirect = false; |
| bool isCallRouteRedirect = false; |
| |
| final List<GoRoute> routes = <GoRoute>[ |
| GoRoute( |
| name: 'home', |
| path: '/', |
| builder: (BuildContext context, GoRouterState state) => |
| const HomeScreen(), |
| routes: <GoRoute>[ |
| GoRoute( |
| name: 'login', |
| path: 'login', |
| builder: (BuildContext context, GoRouterState state) { |
| return const LoginScreen(); |
| }, |
| redirect: (BuildContext context, GoRouterState state) { |
| isCallRouteRedirect = true; |
| expect(state.extra, isNotNull); |
| return null; |
| }, |
| routes: const <GoRoute>[], |
| ), |
| ], |
| ), |
| ]; |
| |
| final GoRouter router = await createRouter( |
| routes, |
| tester, |
| redirect: (BuildContext context, GoRouterState state) { |
| if (state.location == '/login') { |
| isCallTopRedirect = true; |
| expect(state.extra, isNotNull); |
| } |
| |
| return null; |
| }, |
| ); |
| |
| router.go('/login', extra: 1); |
| await tester.pump(); |
| |
| expect(isCallTopRedirect, true); |
| expect(isCallRouteRedirect, true); |
| }); |
| |
| testWidgets('parent route level redirect take priority over child', |
| (WidgetTester tester) async { |
| final List<GoRoute> routes = <GoRoute>[ |
| GoRoute( |
| path: '/', |
| builder: (BuildContext context, GoRouterState state) => |
| const HomeScreen(), |
| routes: <GoRoute>[ |
| GoRoute( |
| path: 'dummy', |
| builder: (BuildContext context, GoRouterState state) => |
| const DummyScreen(), |
| redirect: (BuildContext context, GoRouterState state) => |
| '/other', |
| routes: <GoRoute>[ |
| GoRoute( |
| path: 'dummy2', |
| builder: (BuildContext context, GoRouterState state) => |
| const DummyScreen(), |
| redirect: (BuildContext context, GoRouterState state) { |
| assert(false); |
| return '/other2'; |
| }, |
| ), |
| ]), |
| GoRoute( |
| path: 'other', |
| builder: (BuildContext context, GoRouterState state) => |
| const DummyScreen()), |
| GoRoute( |
| path: 'other2', |
| builder: (BuildContext context, GoRouterState state) => |
| const DummyScreen()), |
| ], |
| ), |
| ]; |
| |
| final GoRouter router = await createRouter(routes, tester); |
| |
| // Directly set the url through platform message. |
| await sendPlatformUrl('/dummy/dummy2'); |
| |
| await tester.pumpAndSettle(); |
| expect(router.location, '/other'); |
| }); |
| }); |
| |
| group('initial location', () { |
| testWidgets('initial location', (WidgetTester tester) async { |
| final List<GoRoute> routes = <GoRoute>[ |
| GoRoute( |
| path: '/', |
| builder: (BuildContext context, GoRouterState state) => |
| const HomeScreen(), |
| routes: <GoRoute>[ |
| GoRoute( |
| path: 'dummy', |
| builder: (BuildContext context, GoRouterState state) => |
| const DummyScreen(), |
| ), |
| ], |
| ), |
| ]; |
| |
| final GoRouter router = await createRouter( |
| routes, |
| tester, |
| initialLocation: '/dummy', |
| ); |
| expect(router.location, '/dummy'); |
| }); |
| |
| testWidgets('initial location w/ redirection', (WidgetTester tester) async { |
| final List<GoRoute> routes = <GoRoute>[ |
| GoRoute( |
| path: '/', |
| builder: (BuildContext context, GoRouterState state) => |
| const HomeScreen(), |
| ), |
| GoRoute( |
| path: '/dummy', |
| builder: dummy, |
| redirect: (BuildContext context, GoRouterState state) => '/', |
| ), |
| ]; |
| |
| final GoRouter router = await createRouter( |
| routes, |
| tester, |
| initialLocation: '/dummy', |
| ); |
| expect(router.location, '/'); |
| }); |
| |
| testWidgets( |
| 'does not take precedence over platformDispatcher.defaultRouteName', |
| (WidgetTester tester) async { |
| TestWidgetsFlutterBinding |
| .instance.platformDispatcher.defaultRouteNameTestValue = '/dummy'; |
| |
| final List<GoRoute> routes = <GoRoute>[ |
| GoRoute( |
| path: '/', |
| builder: (BuildContext context, GoRouterState state) => |
| const HomeScreen(), |
| routes: <GoRoute>[ |
| GoRoute( |
| path: 'dummy', |
| builder: (BuildContext context, GoRouterState state) => |
| const DummyScreen(), |
| ), |
| ], |
| ), |
| ]; |
| |
| final GoRouter router = await createRouter( |
| routes, |
| tester, |
| ); |
| expect(router.routeInformationProvider.value.location, '/dummy'); |
| TestWidgetsFlutterBinding |
| .instance.platformDispatcher.defaultRouteNameTestValue = '/'; |
| }); |
| }); |
| |
| group('params', () { |
| testWidgets('preserve path param case', (WidgetTester tester) async { |
| final List<GoRoute> routes = <GoRoute>[ |
| GoRoute( |
| path: '/', |
| builder: (BuildContext context, GoRouterState state) => |
| const HomeScreen(), |
| ), |
| GoRoute( |
| path: '/family/:fid', |
| builder: (BuildContext context, GoRouterState state) => |
| FamilyScreen(state.params['fid']!), |
| ), |
| ]; |
| |
| final GoRouter router = await createRouter(routes, tester); |
| for (final String fid in <String>['f2', 'F2']) { |
| final String loc = '/family/$fid'; |
| router.go(loc); |
| await tester.pumpAndSettle(); |
| final RouteMatchList matches = router.routerDelegate.matches; |
| |
| expect(router.location, loc); |
| expect(matches.matches, hasLength(1)); |
| expect(find.byType(FamilyScreen), findsOneWidget); |
| expect(matches.pathParameters['fid'], fid); |
| } |
| }); |
| |
| testWidgets('preserve query param case', (WidgetTester tester) async { |
| final List<GoRoute> routes = <GoRoute>[ |
| GoRoute( |
| path: '/', |
| builder: (BuildContext context, GoRouterState state) => |
| const HomeScreen(), |
| ), |
| GoRoute( |
| path: '/family', |
| builder: (BuildContext context, GoRouterState state) => FamilyScreen( |
| state.queryParams['fid']!, |
| ), |
| ), |
| ]; |
| |
| final GoRouter router = await createRouter(routes, tester); |
| for (final String fid in <String>['f2', 'F2']) { |
| final String loc = '/family?fid=$fid'; |
| router.go(loc); |
| await tester.pumpAndSettle(); |
| final RouteMatchList matches = router.routerDelegate.matches; |
| |
| expect(router.location, loc); |
| expect(matches.matches, hasLength(1)); |
| expect(find.byType(FamilyScreen), findsOneWidget); |
| expect(matches.uri.queryParameters['fid'], fid); |
| } |
| }); |
| |
| testWidgets('preserve path param spaces and slashes', |
| (WidgetTester tester) async { |
| const String param1 = 'param w/ spaces and slashes'; |
| final List<GoRoute> routes = <GoRoute>[ |
| GoRoute( |
| path: '/page1/:param1', |
| builder: (BuildContext c, GoRouterState s) { |
| expect(s.params['param1'], param1); |
| return const DummyScreen(); |
| }, |
| ), |
| ]; |
| |
| final GoRouter router = await createRouter(routes, tester); |
| final String loc = '/page1/${Uri.encodeComponent(param1)}'; |
| router.go(loc); |
| await tester.pumpAndSettle(); |
| |
| final RouteMatchList matches = router.routerDelegate.matches; |
| expect(find.byType(DummyScreen), findsOneWidget); |
| expect(matches.pathParameters['param1'], param1); |
| }); |
| |
| testWidgets('preserve query param spaces and slashes', |
| (WidgetTester tester) async { |
| const String param1 = 'param w/ spaces and slashes'; |
| final List<GoRoute> routes = <GoRoute>[ |
| GoRoute( |
| path: '/page1', |
| builder: (BuildContext c, GoRouterState s) { |
| expect(s.queryParams['param1'], param1); |
| return const DummyScreen(); |
| }, |
| ), |
| ]; |
| |
| final GoRouter router = await createRouter(routes, tester); |
| router.go('/page1?param1=$param1'); |
| await tester.pumpAndSettle(); |
| |
| final RouteMatchList matches = router.routerDelegate.matches; |
| expect(find.byType(DummyScreen), findsOneWidget); |
| expect(matches.uri.queryParameters['param1'], param1); |
| |
| final String loc = '/page1?param1=${Uri.encodeQueryComponent(param1)}'; |
| router.go(loc); |
| await tester.pumpAndSettle(); |
| |
| final RouteMatchList matches2 = router.routerDelegate.matches; |
| expect(find.byType(DummyScreen), findsOneWidget); |
| expect(matches2.uri.queryParameters['param1'], param1); |
| }); |
| |
| test('error: duplicate path param', () { |
| try { |
| GoRouter( |
| routes: <GoRoute>[ |
| GoRoute( |
| path: '/:id/:blah/:bam/:id/:blah', |
| builder: dummy, |
| ), |
| ], |
| errorBuilder: (BuildContext context, GoRouterState state) => |
| TestErrorScreen(state.error!), |
| initialLocation: '/0/1/2/0/1', |
| ); |
| expect(false, true); |
| } on Exception catch (ex) { |
| log.info(ex); |
| } |
| }); |
| |
| testWidgets('duplicate query param', (WidgetTester tester) async { |
| final GoRouter router = await createRouter( |
| <GoRoute>[ |
| GoRoute( |
| path: '/', |
| builder: (BuildContext context, GoRouterState state) { |
| log.info('id= ${state.params['id']}'); |
| expect(state.params.length, 0); |
| expect(state.queryParams.length, 1); |
| expect(state.queryParams['id'], anyOf('0', '1')); |
| return const HomeScreen(); |
| }, |
| ), |
| ], |
| tester, |
| initialLocation: '/?id=0&id=1', |
| ); |
| final RouteMatchList matches = router.routerDelegate.matches; |
| expect(matches.matches, hasLength(1)); |
| expect(matches.fullpath, '/'); |
| expect(find.byType(HomeScreen), findsOneWidget); |
| }); |
| |
| testWidgets('duplicate path + query param', (WidgetTester tester) async { |
| final GoRouter router = await createRouter( |
| <GoRoute>[ |
| GoRoute( |
| path: '/:id', |
| builder: (BuildContext context, GoRouterState state) { |
| expect(state.params, <String, String>{'id': '0'}); |
| expect(state.queryParams, <String, String>{'id': '1'}); |
| return const HomeScreen(); |
| }, |
| ), |
| ], |
| tester, |
| ); |
| |
| router.go('/0?id=1'); |
| await tester.pumpAndSettle(); |
| final RouteMatchList matches = router.routerDelegate.matches; |
| expect(matches.matches, hasLength(1)); |
| expect(matches.fullpath, '/:id'); |
| expect(find.byType(HomeScreen), findsOneWidget); |
| }); |
| |
| testWidgets('push + query param', (WidgetTester tester) async { |
| final GoRouter router = await createRouter( |
| <GoRoute>[ |
| GoRoute(path: '/', builder: dummy), |
| GoRoute( |
| path: '/family', |
| builder: (BuildContext context, GoRouterState state) => |
| FamilyScreen( |
| state.queryParams['fid']!, |
| ), |
| ), |
| GoRoute( |
| path: '/person', |
| builder: (BuildContext context, GoRouterState state) => |
| PersonScreen( |
| state.queryParams['fid']!, |
| state.queryParams['pid']!, |
| ), |
| ), |
| ], |
| tester, |
| ); |
| |
| router.go('/family?fid=f2'); |
| await tester.pumpAndSettle(); |
| router.push('/person?fid=f2&pid=p1'); |
| await tester.pumpAndSettle(); |
| final FamilyScreen page1 = tester |
| .widget<FamilyScreen>(find.byType(FamilyScreen, skipOffstage: false)); |
| expect(page1.fid, 'f2'); |
| |
| final PersonScreen page2 = |
| tester.widget<PersonScreen>(find.byType(PersonScreen)); |
| expect(page2.fid, 'f2'); |
| expect(page2.pid, 'p1'); |
| }); |
| |
| testWidgets('push + extra param', (WidgetTester tester) async { |
| final GoRouter router = await createRouter( |
| <GoRoute>[ |
| GoRoute(path: '/', builder: dummy), |
| GoRoute( |
| path: '/family', |
| builder: (BuildContext context, GoRouterState state) => |
| FamilyScreen( |
| (state.extra! as Map<String, String>)['fid']!, |
| ), |
| ), |
| GoRoute( |
| path: '/person', |
| builder: (BuildContext context, GoRouterState state) => |
| PersonScreen( |
| (state.extra! as Map<String, String>)['fid']!, |
| (state.extra! as Map<String, String>)['pid']!, |
| ), |
| ), |
| ], |
| tester, |
| ); |
| |
| router.go('/family', extra: <String, String>{'fid': 'f2'}); |
| await tester.pumpAndSettle(); |
| router.push('/person', extra: <String, String>{'fid': 'f2', 'pid': 'p1'}); |
| await tester.pumpAndSettle(); |
| final FamilyScreen page1 = tester |
| .widget<FamilyScreen>(find.byType(FamilyScreen, skipOffstage: false)); |
| expect(page1.fid, 'f2'); |
| |
| final PersonScreen page2 = |
| tester.widget<PersonScreen>(find.byType(PersonScreen)); |
| expect(page2.fid, 'f2'); |
| expect(page2.pid, 'p1'); |
| }); |
| |
| testWidgets('keep param in nested route', (WidgetTester tester) async { |
| final List<GoRoute> routes = <GoRoute>[ |
| GoRoute( |
| path: '/', |
| builder: (BuildContext context, GoRouterState state) => |
| const HomeScreen(), |
| ), |
| GoRoute( |
| path: '/family/:fid', |
| builder: (BuildContext context, GoRouterState state) => |
| FamilyScreen(state.params['fid']!), |
| routes: <GoRoute>[ |
| GoRoute( |
| path: 'person/:pid', |
| builder: (BuildContext context, GoRouterState state) { |
| final String fid = state.params['fid']!; |
| final String pid = state.params['pid']!; |
| |
| return PersonScreen(fid, pid); |
| }, |
| ), |
| ], |
| ), |
| ]; |
| |
| final GoRouter router = await createRouter(routes, tester); |
| const String fid = 'f1'; |
| const String pid = 'p2'; |
| const String loc = '/family/$fid/person/$pid'; |
| |
| router.push(loc); |
| await tester.pumpAndSettle(); |
| final RouteMatchList matches = router.routerDelegate.matches; |
| |
| expect(router.location, loc); |
| expect(matches.matches, hasLength(2)); |
| expect(find.byType(PersonScreen), findsOneWidget); |
| final ImperativeRouteMatch imperativeRouteMatch = |
| matches.matches.last as ImperativeRouteMatch; |
| expect(imperativeRouteMatch.matches.pathParameters['fid'], fid); |
| expect(imperativeRouteMatch.matches.pathParameters['pid'], pid); |
| }); |
| |
| testWidgets('goNames should allow dynamics values for queryParams', |
| (WidgetTester tester) async { |
| const Map<String, dynamic> queryParametersAll = <String, List<dynamic>>{ |
| 'q1': <String>['v1'], |
| 'q2': <String>['v2', 'v3'], |
| }; |
| void expectLocationWithQueryParams(String location) { |
| final Uri uri = Uri.parse(location); |
| expect(uri.path, '/page'); |
| expect(uri.queryParametersAll, queryParametersAll); |
| } |
| |
| final List<GoRoute> routes = <GoRoute>[ |
| GoRoute( |
| path: '/', |
| builder: (BuildContext context, GoRouterState state) => |
| const HomeScreen(), |
| ), |
| GoRoute( |
| name: 'page', |
| path: '/page', |
| builder: (BuildContext context, GoRouterState state) { |
| expect(state.queryParametersAll, queryParametersAll); |
| expectLocationWithQueryParams(state.location); |
| return DummyScreen( |
| queryParametersAll: state.queryParametersAll, |
| ); |
| }, |
| ), |
| ]; |
| |
| final GoRouter router = await createRouter(routes, tester); |
| |
| router.goNamed('page', queryParams: const <String, dynamic>{ |
| 'q1': 'v1', |
| 'q2': <String>['v2', 'v3'], |
| }); |
| await tester.pumpAndSettle(); |
| final List<RouteMatch> matches = router.routerDelegate.matches.matches; |
| |
| expect(matches, hasLength(1)); |
| expectLocationWithQueryParams(router.location); |
| expect( |
| tester.widget<DummyScreen>(find.byType(DummyScreen)), |
| isA<DummyScreen>().having( |
| (DummyScreen screen) => screen.queryParametersAll, |
| 'screen.queryParametersAll', |
| queryParametersAll, |
| ), |
| ); |
| }); |
| }); |
| |
| testWidgets('go should preserve the query parameters when navigating', |
| (WidgetTester tester) async { |
| const Map<String, dynamic> queryParametersAll = <String, List<dynamic>>{ |
| 'q1': <String>['v1'], |
| 'q2': <String>['v2', 'v3'], |
| }; |
| void expectLocationWithQueryParams(String location) { |
| final Uri uri = Uri.parse(location); |
| expect(uri.path, '/page'); |
| expect(uri.queryParametersAll, queryParametersAll); |
| } |
| |
| final List<GoRoute> routes = <GoRoute>[ |
| GoRoute( |
| path: '/', |
| builder: (BuildContext context, GoRouterState state) => |
| const HomeScreen(), |
| ), |
| GoRoute( |
| name: 'page', |
| path: '/page', |
| builder: (BuildContext context, GoRouterState state) { |
| expect(state.queryParametersAll, queryParametersAll); |
| expectLocationWithQueryParams(state.location); |
| return DummyScreen( |
| queryParametersAll: state.queryParametersAll, |
| ); |
| }, |
| ), |
| ]; |
| |
| final GoRouter router = await createRouter(routes, tester); |
| |
| router.go('/page?q1=v1&q2=v2&q2=v3'); |
| await tester.pumpAndSettle(); |
| final List<RouteMatch> matches = router.routerDelegate.matches.matches; |
| |
| expect(matches, hasLength(1)); |
| expectLocationWithQueryParams(router.location); |
| expect( |
| tester.widget<DummyScreen>(find.byType(DummyScreen)), |
| isA<DummyScreen>().having( |
| (DummyScreen screen) => screen.queryParametersAll, |
| 'screen.queryParametersAll', |
| queryParametersAll, |
| ), |
| ); |
| }); |
| |
| testWidgets('goRouter should rebuild widget if ', |
| (WidgetTester tester) async { |
| const Map<String, dynamic> queryParametersAll = <String, List<dynamic>>{ |
| 'q1': <String>['v1'], |
| 'q2': <String>['v2', 'v3'], |
| }; |
| void expectLocationWithQueryParams(String location) { |
| final Uri uri = Uri.parse(location); |
| expect(uri.path, '/page'); |
| expect(uri.queryParametersAll, queryParametersAll); |
| } |
| |
| final List<GoRoute> routes = <GoRoute>[ |
| GoRoute( |
| path: '/', |
| builder: (BuildContext context, GoRouterState state) => |
| const HomeScreen(), |
| ), |
| GoRoute( |
| name: 'page', |
| path: '/page', |
| builder: (BuildContext context, GoRouterState state) { |
| expect(state.queryParametersAll, queryParametersAll); |
| expectLocationWithQueryParams(state.location); |
| return DummyScreen( |
| queryParametersAll: state.queryParametersAll, |
| ); |
| }, |
| ), |
| ]; |
| |
| final GoRouter router = await createRouter(routes, tester); |
| |
| router.go('/page?q1=v1&q2=v2&q2=v3'); |
| await tester.pumpAndSettle(); |
| final List<RouteMatch> matches = router.routerDelegate.matches.matches; |
| |
| expect(matches, hasLength(1)); |
| expectLocationWithQueryParams(router.location); |
| expect( |
| tester.widget<DummyScreen>(find.byType(DummyScreen)), |
| isA<DummyScreen>().having( |
| (DummyScreen screen) => screen.queryParametersAll, |
| 'screen.queryParametersAll', |
| queryParametersAll, |
| ), |
| ); |
| }); |
| |
| group('GoRouterHelper extensions', () { |
| final GlobalKey<DummyStatefulWidgetState> key = |
| GlobalKey<DummyStatefulWidgetState>(); |
| final List<GoRoute> routes = <GoRoute>[ |
| GoRoute( |
| path: '/', |
| name: 'home', |
| builder: (BuildContext context, GoRouterState state) => |
| DummyStatefulWidget(key: key), |
| ), |
| GoRoute( |
| path: '/page1', |
| name: 'page1', |
| builder: (BuildContext context, GoRouterState state) => |
| const Page1Screen(), |
| ), |
| ]; |
| |
| const String name = 'page1'; |
| final Map<String, String> params = <String, String>{ |
| 'a-param-key': 'a-param-value', |
| }; |
| final Map<String, String> queryParams = <String, String>{ |
| 'a-query-key': 'a-query-value', |
| }; |
| const String location = '/page1'; |
| const String extra = 'Hello'; |
| |
| testWidgets('calls [namedLocation] on closest GoRouter', |
| (WidgetTester tester) async { |
| final GoRouterNamedLocationSpy router = |
| GoRouterNamedLocationSpy(routes: routes); |
| await tester.pumpWidget( |
| MaterialApp.router( |
| routerConfig: router, |
| title: 'GoRouter Example', |
| ), |
| ); |
| key.currentContext!.namedLocation( |
| name, |
| params: params, |
| queryParams: queryParams, |
| ); |
| expect(router.name, name); |
| expect(router.params, params); |
| expect(router.queryParams, queryParams); |
| }); |
| |
| testWidgets('calls [go] on closest GoRouter', (WidgetTester tester) async { |
| final GoRouterGoSpy router = GoRouterGoSpy(routes: routes); |
| await tester.pumpWidget( |
| MaterialApp.router( |
| routerConfig: router, |
| title: 'GoRouter Example', |
| ), |
| ); |
| key.currentContext!.go( |
| location, |
| extra: extra, |
| ); |
| expect(router.myLocation, location); |
| expect(router.extra, extra); |
| }); |
| |
| testWidgets('calls [goNamed] on closest GoRouter', |
| (WidgetTester tester) async { |
| final GoRouterGoNamedSpy router = GoRouterGoNamedSpy(routes: routes); |
| await tester.pumpWidget( |
| MaterialApp.router( |
| routerConfig: router, |
| title: 'GoRouter Example', |
| ), |
| ); |
| key.currentContext!.goNamed( |
| name, |
| params: params, |
| queryParams: queryParams, |
| extra: extra, |
| ); |
| expect(router.name, name); |
| expect(router.params, params); |
| expect(router.queryParams, queryParams); |
| expect(router.extra, extra); |
| }); |
| |
| testWidgets('calls [push] on closest GoRouter', |
| (WidgetTester tester) async { |
| final GoRouterPushSpy router = GoRouterPushSpy(routes: routes); |
| await tester.pumpWidget( |
| MaterialApp.router( |
| routerConfig: router, |
| title: 'GoRouter Example', |
| ), |
| ); |
| key.currentContext!.push( |
| location, |
| extra: extra, |
| ); |
| expect(router.myLocation, location); |
| expect(router.extra, extra); |
| }); |
| |
| testWidgets('calls [pushNamed] on closest GoRouter', |
| (WidgetTester tester) async { |
| final GoRouterPushNamedSpy router = GoRouterPushNamedSpy(routes: routes); |
| await tester.pumpWidget( |
| MaterialApp.router( |
| routerConfig: router, |
| title: 'GoRouter Example', |
| ), |
| ); |
| key.currentContext!.pushNamed( |
| name, |
| params: params, |
| queryParams: queryParams, |
| extra: extra, |
| ); |
| expect(router.name, name); |
| expect(router.params, params); |
| expect(router.queryParams, queryParams); |
| expect(router.extra, extra); |
| }); |
| |
| testWidgets('calls [pop] on closest GoRouter', (WidgetTester tester) async { |
| final GoRouterPopSpy router = GoRouterPopSpy(routes: routes); |
| await tester.pumpWidget( |
| MaterialApp.router( |
| routerConfig: router, |
| title: 'GoRouter Example', |
| ), |
| ); |
| key.currentContext!.pop(); |
| expect(router.popped, true); |
| }); |
| }); |
| |
| group('ShellRoute', () { |
| testWidgets('defaultRoute', (WidgetTester tester) async { |
| final List<RouteBase> routes = <RouteBase>[ |
| ShellRoute( |
| builder: (BuildContext context, GoRouterState state, Widget child) { |
| return Scaffold( |
| body: child, |
| ); |
| }, |
| routes: <RouteBase>[ |
| GoRoute( |
| path: '/a', |
| builder: (BuildContext context, GoRouterState state) { |
| return const Scaffold( |
| body: Text('Screen A'), |
| ); |
| }, |
| ), |
| GoRoute( |
| path: '/b', |
| builder: (BuildContext context, GoRouterState state) { |
| return const Scaffold( |
| body: Text('Screen B'), |
| ); |
| }, |
| ), |
| ], |
| ), |
| ]; |
| |
| await createRouter(routes, tester, initialLocation: '/b'); |
| expect(find.text('Screen B'), findsOneWidget); |
| }); |
| |
| testWidgets( |
| 'Pops from the correct Navigator when the Android back button is pressed', |
| (WidgetTester tester) async { |
| final List<RouteBase> routes = <RouteBase>[ |
| ShellRoute( |
| builder: (BuildContext context, GoRouterState state, Widget child) { |
| return Scaffold( |
| body: Column( |
| children: <Widget>[ |
| const Text('Screen A'), |
| Expanded(child: child), |
| ], |
| ), |
| ); |
| }, |
| routes: <RouteBase>[ |
| GoRoute( |
| path: '/b', |
| builder: (BuildContext context, GoRouterState state) { |
| return const Scaffold( |
| body: Text('Screen B'), |
| ); |
| }, |
| routes: <RouteBase>[ |
| GoRoute( |
| path: 'c', |
| builder: (BuildContext context, GoRouterState state) { |
| return const Scaffold( |
| body: Text('Screen C'), |
| ); |
| }, |
| ), |
| ], |
| ), |
| ], |
| ), |
| ]; |
| |
| await createRouter(routes, tester, initialLocation: '/b/c'); |
| expect(find.text('Screen A'), findsOneWidget); |
| expect(find.text('Screen B'), findsNothing); |
| expect(find.text('Screen C'), findsOneWidget); |
| |
| await simulateAndroidBackButton(tester); |
| await tester.pumpAndSettle(); |
| |
| expect(find.text('Screen A'), findsOneWidget); |
| expect(find.text('Screen B'), findsOneWidget); |
| expect(find.text('Screen C'), findsNothing); |
| }); |
| |
| testWidgets( |
| 'Pops from the correct navigator when a sub-route is placed on ' |
| 'the root Navigator', (WidgetTester tester) async { |
| final GlobalKey<NavigatorState> rootNavigatorKey = |
| GlobalKey<NavigatorState>(); |
| final GlobalKey<NavigatorState> shellNavigatorKey = |
| GlobalKey<NavigatorState>(); |
| |
| final List<RouteBase> routes = <RouteBase>[ |
| ShellRoute( |
| navigatorKey: shellNavigatorKey, |
| builder: (BuildContext context, GoRouterState state, Widget child) { |
| return Scaffold( |
| body: Column( |
| children: <Widget>[ |
| const Text('Screen A'), |
| Expanded(child: child), |
| ], |
| ), |
| ); |
| }, |
| routes: <RouteBase>[ |
| GoRoute( |
| path: '/b', |
| builder: (BuildContext context, GoRouterState state) { |
| return const Scaffold( |
| body: Text('Screen B'), |
| ); |
| }, |
| routes: <RouteBase>[ |
| GoRoute( |
| path: 'c', |
| parentNavigatorKey: rootNavigatorKey, |
| builder: (BuildContext context, GoRouterState state) { |
| return const Scaffold( |
| body: Text('Screen C'), |
| ); |
| }, |
| ), |
| ], |
| ), |
| ], |
| ), |
| ]; |
| |
| await createRouter(routes, tester, |
| initialLocation: '/b/c', navigatorKey: rootNavigatorKey); |
| expect(find.text('Screen A'), findsNothing); |
| expect(find.text('Screen B'), findsNothing); |
| expect(find.text('Screen C'), findsOneWidget); |
| |
| await simulateAndroidBackButton(tester); |
| await tester.pumpAndSettle(); |
| |
| expect(find.text('Screen A'), findsOneWidget); |
| expect(find.text('Screen B'), findsOneWidget); |
| expect(find.text('Screen C'), findsNothing); |
| }); |
| }); |
| |
| group('Imperative navigation', () { |
| group('canPop', () { |
| testWidgets( |
| 'It should return false if Navigator.canPop() returns false.', |
| (WidgetTester tester) async { |
| final GlobalKey<NavigatorState> navigatorKey = |
| GlobalKey<NavigatorState>(); |
| final GoRouter router = GoRouter( |
| initialLocation: '/', |
| navigatorKey: navigatorKey, |
| routes: <GoRoute>[ |
| GoRoute( |
| path: '/', |
| builder: (BuildContext context, _) { |
| return Scaffold( |
| body: TextButton( |
| onPressed: () async { |
| navigatorKey.currentState!.push( |
| MaterialPageRoute<void>( |
| builder: (BuildContext context) { |
| return const Scaffold( |
| body: Text('pageless route'), |
| ); |
| }, |
| ), |
| ); |
| }, |
| child: const Text('Push'), |
| ), |
| ); |
| }, |
| ), |
| GoRoute(path: '/a', builder: (_, __) => const DummyScreen()), |
| ], |
| ); |
| |
| await tester.pumpWidget( |
| MaterialApp.router( |
| routeInformationProvider: router.routeInformationProvider, |
| routeInformationParser: router.routeInformationParser, |
| routerDelegate: router.routerDelegate), |
| ); |
| |
| expect(router.canPop(), false); |
| |
| await tester.tap(find.text('Push')); |
| await tester.pumpAndSettle(); |
| |
| expect( |
| find.text('pageless route', skipOffstage: false), findsOneWidget); |
| expect(router.canPop(), true); |
| }, |
| ); |
| |
| testWidgets( |
| 'It checks if ShellRoute navigators can pop', |
| (WidgetTester tester) async { |
| final GlobalKey<NavigatorState> shellNavigatorKey = |
| GlobalKey<NavigatorState>(); |
| final GoRouter router = GoRouter( |
| initialLocation: '/a', |
| routes: <RouteBase>[ |
| ShellRoute( |
| navigatorKey: shellNavigatorKey, |
| builder: |
| (BuildContext context, GoRouterState state, Widget child) { |
| return Scaffold( |
| appBar: AppBar(title: const Text('Shell')), |
| body: child, |
| ); |
| }, |
| routes: <GoRoute>[ |
| GoRoute( |
| path: '/a', |
| builder: (BuildContext context, _) { |
| return Scaffold( |
| body: TextButton( |
| onPressed: () async { |
| shellN
|