blob: 2d670296dae3f9e75a66d42fef03387aa8f6cbbe [file] [log] [blame]
// 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, unawaited_futures
import 'dart:async';
import 'package:collection/collection.dart';
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/match.dart';
import 'package:logging/logging.dart';
import 'test_helpers.dart';
const bool enableLogs = false;
final Logger log = Logger('GoRouter tests');
Future<void> sendPlatformUrl(String url, WidgetTester tester) async {
final Map<String, dynamic> testRouteInformation = <String, dynamic>{
'location': url,
};
final ByteData message = const JSONMethodCodec().encodeMethodCall(
MethodCall('pushRouteInformation', testRouteInformation),
);
await tester.binding.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.currentConfiguration;
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.currentConfiguration.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,
errorBuilder: (BuildContext context, GoRouterState state) =>
TestErrorScreen(state.error!),
);
router.go('/foo');
await tester.pumpAndSettle();
final List<RouteMatch> matches =
router.routerDelegate.currentConfiguration.matches;
expect(matches, hasLength(0));
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.currentConfiguration.matches;
expect(matches, hasLength(1));
expect(matches.first.matchedLocation, '/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.currentConfiguration.matches;
expect(matches, hasLength(1));
expect(matches.first.matchedLocation, '/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.currentConfiguration.matches;
expect(matches, hasLength(1));
expect(matches.first.matchedLocation, '/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.currentConfiguration.matches;
expect(matches, hasLength(1));
expect(matches.first.matchedLocation, '/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.currentConfiguration.matches;
expect(matches, hasLength(1));
expect(matches.first.matchedLocation, '/profile/foo');
expect(find.byType(DummyScreen), findsOneWidget);
});
testWidgets('repeatedly pops imperative route does not crash',
(WidgetTester tester) async {
// Regression test for https://github.com/flutter/flutter/issues/123369.
final UniqueKey home = UniqueKey();
final UniqueKey settings = UniqueKey();
final UniqueKey dialog = UniqueKey();
final GlobalKey<NavigatorState> navKey = GlobalKey<NavigatorState>();
final List<GoRoute> routes = <GoRoute>[
GoRoute(
path: '/',
builder: (_, __) => DummyScreen(key: home),
),
GoRoute(
path: '/settings',
builder: (_, __) => DummyScreen(key: settings),
),
];
final GoRouter router =
await createRouter(routes, tester, navigatorKey: navKey);
expect(find.byKey(home), findsOneWidget);
router.push('/settings');
await tester.pumpAndSettle();
expect(find.byKey(home), findsNothing);
expect(find.byKey(settings), findsOneWidget);
showDialog(
context: navKey.currentContext!,
builder: (_) => DummyScreen(key: dialog),
);
await tester.pumpAndSettle();
expect(find.byKey(dialog), findsOneWidget);
router.pop();
await tester.pumpAndSettle();
expect(find.byKey(dialog), findsNothing);
expect(find.byKey(settings), findsOneWidget);
showDialog(
context: navKey.currentContext!,
builder: (_) => DummyScreen(key: dialog),
);
await tester.pumpAndSettle();
expect(find.byKey(dialog), findsOneWidget);
router.pop();
await tester.pumpAndSettle();
expect(find.byKey(dialog), findsNothing);
expect(find.byKey(settings), findsOneWidget);
});
testWidgets('can correctly pop stacks of repeated pages',
(WidgetTester tester) async {
// Regression test for https://github.com/flutter/flutter/issues/#132229.
final GlobalKey<NavigatorState> navKey = GlobalKey<NavigatorState>();
final List<GoRoute> routes = <GoRoute>[
GoRoute(
path: '/',
pageBuilder: (_, __) =>
const MaterialPage<Object>(child: HomeScreen()),
),
GoRoute(
path: '/page1',
pageBuilder: (_, __) =>
const MaterialPage<Object>(child: Page1Screen()),
),
GoRoute(
path: '/page2',
pageBuilder: (_, __) =>
const MaterialPage<Object>(child: Page2Screen()),
),
];
final GoRouter router =
await createRouter(routes, tester, navigatorKey: navKey);
expect(find.byType(HomeScreen), findsOneWidget);
router.push('/page1');
router.push('/page2');
router.push('/page1');
router.push('/page2');
await tester.pumpAndSettle();
expect(find.byType(HomeScreen), findsNothing);
expect(find.byType(Page1Screen), findsNothing);
expect(find.byType(Page2Screen), findsOneWidget);
router.pop();
await tester.pumpAndSettle();
final List<RouteMatch> matches =
router.routerDelegate.currentConfiguration.matches;
expect(matches.length, 4);
expect(find.byType(HomeScreen), findsNothing);
expect(find.byType(Page1Screen), findsOneWidget);
expect(find.byType(Page2Screen), findsNothing);
});
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.currentConfiguration.matches;
expect(matches.length, 2);
expect(matches.first.matchedLocation, '/');
expect(find.byType(HomeScreen, skipOffstage: false), findsOneWidget);
expect(matches[1].matchedLocation, '/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.currentConfiguration;
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.currentConfiguration;
expect(matches.matches.length, 2);
expect(matches.matches.first.matchedLocation, '/');
expect(find.byType(HomeScreen, skipOffstage: false), findsOneWidget);
expect(matches.matches[1].matchedLocation, '/login');
expect(find.byType(LoginScreen), findsOneWidget);
}
router.go('/family/f2');
await tester.pumpAndSettle();
{
final RouteMatchList matches =
router.routerDelegate.currentConfiguration;
expect(matches.matches.length, 2);
expect(matches.matches.first.matchedLocation, '/');
expect(find.byType(HomeScreen, skipOffstage: false), findsOneWidget);
expect(matches.matches[1].matchedLocation, '/family/f2');
expect(find.byType(FamilyScreen), findsOneWidget);
}
router.go('/family/f2/person/p1');
await tester.pumpAndSettle();
{
final RouteMatchList matches =
router.routerDelegate.currentConfiguration;
expect(matches.matches.length, 3);
expect(matches.matches.first.matchedLocation, '/');
expect(find.byType(HomeScreen, skipOffstage: false), findsOneWidget);
expect(matches.matches[1].matchedLocation, '/family/f2');
expect(find.byType(FamilyScreen, skipOffstage: false), findsOneWidget);
expect(matches.matches[2].matchedLocation, '/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.currentConfiguration.matches;
expect(matches, hasLength(2));
expect(find.byType(Page1Screen), findsOneWidget);
router.go('/foo/bar');
await tester.pumpAndSettle();
matches = router.routerDelegate.currentConfiguration.matches;
expect(matches, hasLength(2));
expect(find.byType(FamilyScreen), findsOneWidget);
router.go('/foo');
await tester.pumpAndSettle();
matches = router.routerDelegate.currentConfiguration.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.uri.toString(), '/');
expect(state.matchedLocation, '/');
expect(state.name, 'home');
expect(state.path, '/');
expect(state.fullPath, '/');
expect(state.pathParameters, <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.uri.toString(), '/login');
expect(state.matchedLocation, '/login');
expect(state.name, 'login');
expect(state.path, 'login');
expect(state.fullPath, '/login');
expect(state.pathParameters, <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.uri.toString(),
anyOf(<String>['/family/f2', '/family/f2/person/p1']),
);
expect(state.matchedLocation, '/family/f2');
expect(state.name, 'family');
expect(state.path, 'family/:fid');
expect(state.fullPath, '/family/:fid');
expect(state.pathParameters, <String, String>{'fid': 'f2'});
expect(state.error, null);
expect(state.extra! as int, 3);
return FamilyScreen(state.pathParameters['fid']!);
},
routes: <GoRoute>[
GoRoute(
name: 'person',
path: 'person/:pid',
builder: (BuildContext context, GoRouterState state) {
expect(state.uri.toString(), '/family/f2/person/p1');
expect(state.matchedLocation, '/family/f2/person/p1');
expect(state.name, 'person');
expect(state.path, 'person/:pid');
expect(state.fullPath, '/family/:fid/person/:pid');
expect(
state.pathParameters,
<String, String>{'fid': 'f2', 'pid': 'p1'},
);
expect(state.error, null);
expect(state.extra! as int, 4);
return PersonScreen(state.pathParameters['fid']!,
state.pathParameters['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.pathParameters['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.currentConfiguration.matches;
// NOTE: match the lower case, since location 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.routerDelegate.currentConfiguration.uri
.toString()
.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.currentConfiguration.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>[];
_ambiguate(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>[];
_ambiguate(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('does not crash when inherited widget changes',
(WidgetTester tester) async {
final ValueNotifier<String> notifier = ValueNotifier<String>('initial');
final List<GoRoute> routes = <GoRoute>[
GoRoute(
path: '/',
pageBuilder: (BuildContext context, GoRouterState state) {
final String value = context
.dependOnInheritedWidgetOfExactType<TestInheritedNotifier>()!
.notifier!
.value;
return MaterialPage<void>(
key: state.pageKey,
child: Text(value),
);
}),
];
final GoRouter router = GoRouter(
routes: routes,
);
await tester.pumpWidget(
MaterialApp.router(
routerConfig: router,
builder: (BuildContext context, Widget? child) {
return TestInheritedNotifier(notifier: notifier, child: child!);
},
),
);
expect(find.text(notifier.value), findsOneWidget);
notifier.value = 'updated';
await tester.pump();
expect(find.text(notifier.value), findsOneWidget);
});
testWidgets(
'Handles the Android back button when a second Shell has a GoRoute with parentNavigator key',
(WidgetTester tester) async {
final List<MethodCall> log = <MethodCall>[];
_ambiguate(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(() {
GoRouter.optionURLReflectsImperativeAPIs = false;
_ambiguate(TestDefaultBinaryMessengerBinding.instance)!
.defaultBinaryMessenger
.setMockMethodCallHandler(SystemChannels.navigation,
(MethodCall methodCall) async {
log.add(methodCall);
return null;
});
});
tearDown(() {
GoRouter.optionURLReflectsImperativeAPIs = false;
_ambiguate(TestDefaultBinaryMessengerBinding.instance)!
.defaultBinaryMessenger
.setMockMethodCallHandler(SystemChannels.navigation, null);
log.clear();
});
testWidgets('on push with optionURLReflectImperativeAPIs = true',
(WidgetTester tester) async {
GoRouter.optionURLReflectsImperativeAPIs = true;
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');
final RouteMatchListCodec codec =
RouteMatchListCodec(router.configuration);
await tester.pumpAndSettle();
expect(log, <Object>[
isMethodCall('selectMultiEntryHistory', arguments: null),
IsRouteUpdateCall('/settings', false,
codec.encode(router.routerDelegate.currentConfiguration)),
]);
GoRouter.optionURLReflectsImperativeAPIs = false;
});
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');
final RouteMatchListCodec codec =
RouteMatchListCodec(router.configuration);
await tester.pumpAndSettle();
expect(log, <Object>[
isMethodCall('selectMultiEntryHistory', arguments: null),
IsRouteUpdateCall('/', false,
codec.encode(router.routerDelegate.currentConfiguration)),
]);
});
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');
final RouteMatchListCodec codec =
RouteMatchListCodec(router.configuration);
log.clear();
router.pop();
await tester.pumpAndSettle();
expect(log, <Object>[
isMethodCall('selectMultiEntryHistory', arguments: null),
IsRouteUpdateCall('/', false,
codec.encode(router.routerDelegate.currentConfiguration)),
]);
});
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');
final RouteMatchListCodec codec =
RouteMatchListCodec(router.configuration);
log.clear();
router.pop();
router.pop();
await tester.pumpAndSettle();
expect(log, <Object>[
isMethodCall('selectMultiEntryHistory', arguments: null),
IsRouteUpdateCall('/', false,
codec.encode(router.routerDelegate.currentConfiguration)),
]);
});
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');
final RouteMatchListCodec codec =
RouteMatchListCodec(router.configuration);
log.clear();
router.pop();
await tester.pumpAndSettle();
expect(log, <Object>[
isMethodCall('selectMultiEntryHistory', arguments: null),
IsRouteUpdateCall('/', false,
codec.encode(router.routerDelegate.currentConfiguration)),
]);
});
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/');
final RouteMatchListCodec codec =
RouteMatchListCodec(router.configuration);
log.clear();
router.pop();
await tester.pumpAndSettle();
expect(log, <Object>[
isMethodCall('selectMultiEntryHistory', arguments: null),
IsRouteUpdateCall('/', false,
codec.encode(router.routerDelegate.currentConfiguration)),
]);
});
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'),
);
},
),
],
),
],
),
],
),
];
final GoRouter router = await createRouter(routes, tester,
initialLocation: '/b/c', navigatorKey: rootNavigatorKey);
final RouteMatchListCodec codec =
RouteMatchListCodec(router.configuration);
expect(find.text('Screen C'), findsOneWidget);
expect(log, <Object>[
isMethodCall('selectMultiEntryHistory', arguments: null),
IsRouteUpdateCall('/b/c', true,
codec.encode(router.routerDelegate.currentConfiguration)),
]);
log.clear();
rootNavigatorKey.currentState!.pop();
await tester.pumpAndSettle();
expect(find.text('Home'), findsOneWidget);
expect(log, <Object>[
isMethodCall('selectMultiEntryHistory', arguments: null),
IsRouteUpdateCall('/', false,
codec.encode(router.routerDelegate.currentConfiguration)),
]);
});
testWidgets('can handle route information update from browser',
(WidgetTester tester) async {
final List<GoRoute> routes = <GoRoute>[
GoRoute(
path: '/',
builder: (_, __) => const DummyScreen(key: ValueKey<String>('home')),
routes: <RouteBase>[
GoRoute(
path: 'settings',
builder: (_, GoRouterState state) =>
DummyScreen(key: ValueKey<String>('settings-${state.extra}')),
),
],
),
];
final GoRouter router = await createRouter(routes, tester);
expect(find.byKey(const ValueKey<String>('home')), findsOneWidget);
router.push('/settings', extra: 0);
await tester.pumpAndSettle();
expect(find.byKey(const ValueKey<String>('settings-0')), findsOneWidget);
log.clear();
router.push('/settings', extra: 1);
await tester.pumpAndSettle();
expect(find.byKey(const ValueKey<String>('settings-1')), findsOneWidget);
final Map<Object?, Object?> arguments =
log.last.arguments as Map<Object?, Object?>;
// Stores the state after the last push. This should contain the encoded
// RouteMatchList.
final Object? state =
(log.last.arguments as Map<Object?, Object?>)['state'];
final String location =
(arguments['location'] ?? arguments['uri']!) as String;
router.go('/');
await tester.pumpAndSettle();
expect(find.byKey(const ValueKey<String>('home')), findsOneWidget);
router.routeInformationProvider.didPushRouteInformation(
// TODO(chunhtai): remove this ignore and migrate the code
// https://github.com/flutter/flutter/issues/124045.
// ignore: deprecated_member_use
RouteInformation(location: location, state: state));
await tester.pumpAndSettle();
// Make sure it has all the imperative routes.
expect(find.byKey(const ValueKey<String>('settings-1')), findsOneWidget);
router.pop();
await tester.pumpAndSettle();
expect(find.byKey(const ValueKey<String>('settings-0')), findsOneWidget);
router.pop();
await tester.pumpAndSettle();
expect(find.byKey(const ValueKey<String>('home')), findsOneWidget);
});
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>();
final GoRouter router =
await createRouter(routes, tester, redirect: (_, __) async {
await completer.future;
return '/login';
});
final RouteMatchListCodec codec =
RouteMatchListCodec(router.configuration);
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),
IsRouteUpdateCall('/login', true,
codec.encode(router.routerDelegate.currentConfiguration)),
]);
});
});
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.pathParameters,
<String, String>{'fid': 'f2', 'pid': 'p1'});
return const PersonScreen('dummy', 'dummy');
},
),
],
),
],
),
];
final GoRouter router = await createRouter(routes, tester);
router.goNamed('person',
pathParameters: <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', pathParameters: <String, String>{'fid': 'f2'});
await tester.pump();
}, throwsA(isAssertionError));
});
testWidgets('cannot match case insensitive', (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.pathParameters,
<String, String>{'fid': 'f2', 'pid': 'p1'});
return const PersonScreen('dummy', 'dummy');
},
),
],
),
],
),
];
final GoRouter router = await createRouter(routes, tester);
expect(
() {
router.goNamed(
'person',
pathParameters: <String, String>{'fid': 'f2', 'pid': 'p1'},
);
},
throwsAssertionError,
);
});
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',
pathParameters: <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.pathParameters['fid']!,
),
routes: <GoRoute>[
GoRoute(
name: 'person',
path: 'person:pid',
builder: (BuildContext context, GoRouterState state) =>
PersonScreen(
state.pathParameters['fid']!,
state.pathParameters['pid']!,
),
),
],
),
];
final GoRouter router = await createRouter(routes, tester);
router.goNamed('person',
pathParameters: <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.pathParameters['param1'], param1);
return const DummyScreen();
},
),
];
final GoRouter router = await createRouter(routes, tester);
final String loc = router.namedLocation('page1',
pathParameters: <String, String>{'param1': param1});
router.go(loc);
await tester.pumpAndSettle();
final RouteMatchList matches = router.routerDelegate.currentConfiguration;
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.uri.queryParameters['param1'], param1);
return const DummyScreen();
},
),
];
final GoRouter router = await createRouter(routes, tester);
final String loc = router.namedLocation('page1',
queryParameters: <String, String>{'param1': param1});
router.go(loc);
await tester.pumpAndSettle();
final RouteMatchList matches = router.routerDelegate.currentConfiguration;
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.matchedLocation == '/login' ? null : '/login';
});
expect(
router.routerDelegate.currentConfiguration.uri.toString(), '/login');
expect(redirected, isTrue);
redirected = false;
// Directly set the url through platform message.
await sendPlatformUrl('/dummy', tester);
await tester.pumpAndSettle();
expect(
router.routerDelegate.currentConfiguration.uri.toString(), '/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.uri.toString(),
builder: (BuildContext context, GoRouterState state) =>
const DummyScreen()),
],
),
];
final GoRouter router = await createRouter(routes, tester,
redirect: (BuildContext context, GoRouterState state) {
// Return same location.
return state.uri.toString();
});
expect(router.routerDelegate.currentConfiguration.uri.toString(), '/');
// Directly set the url through platform message.
await sendPlatformUrl('/dummy', tester);
await tester.pumpAndSettle();
expect(
router.routerDelegate.currentConfiguration.uri.toString(), '/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.matchedLocation == '/login'
? null
: state.namedLocation('login'),
);
expect(
router.routerDelegate.currentConfiguration.uri.toString(), '/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.routerDelegate.currentConfiguration.uri.toString(), '/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.matchedLocation == '/login' ? null : '/login';
});
redirected = false;
// Directly set the url through platform message.
await sendPlatformUrl('/dummy', tester);
await tester.pumpAndSettle();
expect(
router.routerDelegate.currentConfiguration.uri.toString(), '/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.routerDelegate.currentConfiguration.uri.toString(), '/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.matchedLocation == '/dummy1' ? '/dummy2' : null);
router.go('/dummy1');
await tester.pump();
expect(router.routerDelegate.currentConfiguration.uri.toString(), '/');
});
testWidgets('top-level redirect loop', (WidgetTester tester) async {
final GoRouter router = await createRouter(
<GoRoute>[],
tester,
redirect: (BuildContext context, GoRouterState state) =>
state.matchedLocation == '/'
? '/login'
: state.matchedLocation == '/login'
? '/'
: null,
errorBuilder: (BuildContext context, GoRouterState state) =>
TestErrorScreen(state.error!),
);
final List<RouteMatch> matches =
router.routerDelegate.currentConfiguration.matches;
expect(matches, hasLength(0));
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,
errorBuilder: (BuildContext context, GoRouterState state) =>
TestErrorScreen(state.error!),
);
final List<RouteMatch> matches =
router.routerDelegate.currentConfiguration.matches;
expect(matches, hasLength(0));
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.matchedLocation == '/' ? '/login' : null,
errorBuilder: (BuildContext context, GoRouterState state) =>
TestErrorScreen(state.error!),
);
final List<RouteMatch> matches =
router.routerDelegate.currentConfiguration.matches;
expect(matches, hasLength(0));
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.matchedLocation == '/'
? '/login?from=${state.uri}'
: state.matchedLocation == '/login'
? '/'
: null,
errorBuilder: (BuildContext context, GoRouterState state) =>
TestErrorScreen(state.error!),
);
final List<RouteMatch> matches =
router.routerDelegate.currentConfiguration.matches;
expect(matches, hasLength(0));
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.routerDelegate.currentConfiguration.uri.toString(), '/');
});
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.uri.toString()).queryParameters, isNotEmpty);
expect(Uri.parse(state.matchedLocation).queryParameters, isEmpty);
expect(state.path, isNull);
expect(state.fullPath, '/login');
expect(state.pathParameters.length, 0);
expect(state.uri.queryParameters.length, 1);
expect(state.uri.queryParameters['from'], '/');
return null;
},
);
final List<RouteMatch> matches =
router.routerDelegate.currentConfiguration.matches;
expect(matches, hasLength(1));
expect(find.byType(LoginScreen), findsOneWidget);
});
testWidgets('top-level redirect state contains path parameters',
(WidgetTester tester) async {
final List<GoRoute> routes = <GoRoute>[
GoRoute(
path: '/',
builder: (BuildContext context, GoRouterState state) =>
const DummyScreen(),
routes: <RouteBase>[
GoRoute(
path: ':id',
builder: (BuildContext context, GoRouterState state) =>
const DummyScreen(),
),
]),
];
final GoRouter router = await createRouter(
routes,
tester,
initialLocation: '/123',
redirect: (BuildContext context, GoRouterState state) {
expect(state.path, isNull);
expect(state.fullPath, '/:id');
expect(state.pathParameters.length, 1);
expect(state.pathParameters['id'], '123');
return null;
},
);
final List<RouteMatch> matches =
router.routerDelegate.currentConfiguration.matches;
expect(matches, hasLength(2));
});
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.uri.toString(), loc);
expect(state.matchedLocation, loc);
expect(state.path, '/book/:bookId');
expect(state.fullPath, '/book/:bookId');
expect(state.pathParameters, <String, String>{'bookId': '0'});
expect(state.uri.queryParameters.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.currentConfiguration.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.pathParameters['fid']!),
routes: <GoRoute>[
GoRoute(
path: 'person/:pid',
redirect: (BuildContext context, GoRouterState s) {
expect(s.pathParameters['fid'], 'f2');
expect(s.pathParameters['pid'], 'p1');
return null;
},
builder: (BuildContext c, GoRouterState s) => PersonScreen(
s.pathParameters['fid']!,
s.pathParameters['pid']!,
),
),
],
),
],
),
];
final GoRouter router = await createRouter(
routes,
tester,
initialLocation: '/family/f2/person/p1',
);
final List<RouteMatch> matches =
router.routerDelegate.currentConfiguration.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.uri}+',
errorBuilder: (BuildContext context, GoRouterState state) =>
TestErrorScreen(state.error!),
redirectLimit: 10,
);
final List<RouteMatch> matches =
router.routerDelegate.currentConfiguration.matches;
expect(matches, hasLength(0));
expect(find.byType(TestErrorScreen), findsOneWidget);
final TestErrorScreen screen =
tester.widget<TestErrorScreen>(find.byType(TestErrorScreen));
expect(screen.ex, isNotNull);
});
testWidgets('can push error page', (WidgetTester tester) async {
final GoRouter router = await createRouter(
<GoRoute>[
GoRoute(path: '/', builder: (_, __) => const Text('/')),
],
tester,
errorBuilder: (_, GoRouterState state) {
return Text(state.uri.toString());
},
);
expect(find.text('/'), findsOneWidget);
router.push('/error1');
await tester.pumpAndSettle();
expect(find.text('/'), findsNothing);
expect(find.text('/error1'), findsOneWidget);
router.push('/error2');
await tester.pumpAndSettle();
expect(find.text('/'), findsNothing);
expect(find.text('/error1'), findsNothing);
expect(find.text('/error2'), findsOneWidget);
router.pop();
await tester.pumpAndSettle();
expect(find.text('/'), findsNothing);
expect(find.text('/error1'), findsOneWidget);
expect(find.text('/error2'), findsNothing);
router.pop();
await tester.pumpAndSettle();
expect(find.text('/'), findsOneWidget);
expect(find.text('/error1'), findsNothing);
expect(find.text('/error2'), findsNothing);
});
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.uri.toString() == '/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', tester);
await tester.pumpAndSettle();
expect(
router.routerDelegate.currentConfiguration.uri.toString(), '/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.routerDelegate.currentConfiguration.uri.toString(), '/dummy');
});
testWidgets('initial location with extra', (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) {
return DummyScreen(key: ValueKey<Object?>(state.extra));
},
),
],
),
];
final GoRouter router = await createRouter(
routes,
tester,
initialLocation: '/dummy',
initialExtra: 'extra',
);
expect(
router.routerDelegate.currentConfiguration.uri.toString(), '/dummy');
expect(find.byKey(const ValueKey<Object?>('extra')), findsOneWidget);
});
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.routerDelegate.currentConfiguration.uri.toString(), '/');
});
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,
);
// TODO(chunhtai): remove this ignore and migrate the code
// https://github.com/flutter/flutter/issues/124045.
// ignore: deprecated_member_use
expect(router.routeInformationProvider.value.location, '/dummy');
TestWidgetsFlutterBinding
.instance.platformDispatcher.defaultRouteNameTestValue = '/';
});
test('throws assertion if initialExtra is set w/o initialLocation', () {
expect(
() => GoRouter(
routes: const <GoRoute>[],
initialExtra: 1,
),
throwsA(
isA<AssertionError>().having(
(AssertionError e) => e.message,
'error message',
'initialLocation must be set in order to use initialExtra',
),
),
);
});
});
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.pathParameters['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.currentConfiguration;
expect(router.routerDelegate.currentConfiguration.uri.toString(), 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.uri.queryParameters['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.currentConfiguration;
expect(router.routerDelegate.currentConfiguration.uri.toString(), 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.pathParameters['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.currentConfiguration;
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.uri.queryParameters['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.currentConfiguration;
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.currentConfiguration;
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.pathParameters['id']}');
expect(state.pathParameters.length, 0);
expect(state.uri.queryParameters.length, 1);
expect(state.uri.queryParameters['id'], anyOf('0', '1'));
return const HomeScreen();
},
),
],
tester,
initialLocation: '/?id=0&id=1',
);
final RouteMatchList matches = router.routerDelegate.currentConfiguration;
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.pathParameters, <String, String>{'id': '0'});
expect(state.uri.queryParameters, <String, String>{'id': '1'});
return const HomeScreen();
},
),
],
tester,
);
router.go('/0?id=1');
await tester.pumpAndSettle();
final RouteMatchList matches = router.routerDelegate.currentConfiguration;
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.uri.queryParameters['fid']!,
),
),
GoRoute(
path: '/person',
builder: (BuildContext context, GoRouterState state) =>
PersonScreen(
state.uri.queryParameters['fid']!,
state.uri.queryParameters['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.pathParameters['fid']!),
routes: <GoRoute>[
GoRoute(
path: 'person/:pid',
builder: (BuildContext context, GoRouterState state) {
final String fid = state.pathParameters['fid']!;
final String pid = state.pathParameters['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.currentConfiguration;
expect(matches.matches, hasLength(2));
expect(find.byType(PersonScreen), findsOneWidget);
final ImperativeRouteMatch imperativeRouteMatch =
matches.matches.last as ImperativeRouteMatch;
expect(imperativeRouteMatch.matches.uri.toString(), loc);
expect(imperativeRouteMatch.matches.pathParameters['fid'], fid);
expect(imperativeRouteMatch.matches.pathParameters['pid'], pid);
});
testWidgets('StatefulShellRoute supports nested routes with params',
(WidgetTester tester) async {
StatefulNavigationShell? routeState;
final List<RouteBase> routes = <RouteBase>[
StatefulShellRoute.indexedStack(
builder: (BuildContext context, GoRouterState state,
StatefulNavigationShell navigationShell) {
routeState = navigationShell;
return navigationShell;
},
branches: <StatefulShellBranch>[
StatefulShellBranch(
routes: <RouteBase>[
GoRoute(
path: '/a',
builder: (BuildContext context, GoRouterState state) =>
const Text('Screen A'),
),
],
),
StatefulShellBranch(
routes: <RouteBase>[
GoRoute(
path: '/family',
builder: (BuildContext context, GoRouterState state) =>
const Text('Families'),
routes: <RouteBase>[
GoRoute(
path: ':fid',
builder: (BuildContext context, GoRouterState state) =>
FamilyScreen(state.pathParameters['fid']!),
routes: <GoRoute>[
GoRoute(
path: 'person/:pid',
builder:
(BuildContext context, GoRouterState state) {
final String fid = state.pathParameters['fid']!;
final String pid = state.pathParameters['pid']!;
return PersonScreen(fid, pid);
},
),
],
)
]),
],
),
],
),
];
final GoRouter router =
await createRouter(routes, tester, initialLocation: '/a');
const String fid = 'f1';
const String pid = 'p2';
const String loc = '/family/$fid/person/$pid';
router.go(loc);
await tester.pumpAndSettle();
RouteMatchList matches = router.routerDelegate.currentConfiguration;
expect(router.routerDelegate.currentConfiguration.uri.toString(), loc);
expect(matches.matches, hasLength(4));
expect(find.byType(PersonScreen), findsOneWidget);
expect(matches.pathParameters['fid'], fid);
expect(matches.pathParameters['pid'], pid);
routeState?.goBranch(0);
await tester.pumpAndSettle();
expect(find.text('Screen A'), findsOneWidget);
expect(find.byType(PersonScreen), findsNothing);
routeState?.goBranch(1);
await tester.pumpAndSettle();
expect(find.text('Screen A'), findsNothing);
expect(find.byType(PersonScreen), findsOneWidget);
matches = router.routerDelegate.currentConfiguration;
expect(matches.pathParameters['fid'], fid);
expect(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.uri.queryParametersAll, queryParametersAll);
expectLocationWithQueryParams(state.uri.toString());
return DummyScreen(
queryParametersAll: state.uri.queryParametersAll,
);
},
),
];
final GoRouter router = await createRouter(routes, tester);
router.goNamed('page', queryParameters: const <String, dynamic>{
'q1': 'v1',
'q2': <String>['v2', 'v3'],
});
await tester.pumpAndSettle();
final List<RouteMatch> matches =
router.routerDelegate.currentConfiguration.matches;
expect(matches, hasLength(1));
expectLocationWithQueryParams(
router.routerDelegate.currentConfiguration.uri.toString());
expect(