blob: 8316597458c8e2d9546dc069fb45a8cf4d828182 [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
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:flutter/src/foundation/diagnostics.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:go_router/go_router.dart';
import 'package:go_router/src/go_route_match.dart';
import 'package:go_router/src/go_router_delegate.dart';
import 'package:go_router/src/go_router_error_page.dart';
import 'package:go_router/src/typedefs.dart';
import 'package:logging/logging.dart';
const bool enableLogs = true;
final Logger log = Logger('GoRouter tests');
void main() {
if (enableLogs)
Logger.root.onRecord.listen((LogRecord e) => debugPrint('$e'));
group('path routes', () {
test('match home route', () {
final List<GoRoute> routes = <GoRoute>[
GoRoute(
path: '/',
builder: (BuildContext context, GoRouterState state) =>
const HomeScreen()),
];
final GoRouter router = _router(routes);
final List<GoRouteMatch> matches = router.routerDelegate.matches;
expect(matches, hasLength(1));
expect(matches.first.fullpath, '/');
expect(router.screenFor(matches.first).runtimeType, HomeScreen);
});
test('If there is more than one route to match, use the first match', () {
final List<GoRoute> routes = <GoRoute>[
GoRoute(path: '/', builder: _dummy),
GoRoute(path: '/', builder: _dummy),
];
final GoRouter router = _router(routes);
router.go('/');
final List<GoRouteMatch> matches = router.routerDelegate.matches;
expect(matches, hasLength(1));
expect(matches.first.fullpath, '/');
expect(router.screenFor(matches.first).runtimeType, DummyScreen);
});
test('empty path', () {
expect(() {
GoRoute(path: '');
}, throwsA(isAssertionError));
});
test('leading / on sub-route', () {
expect(() {
GoRoute(
path: '/',
builder: _dummy,
routes: <GoRoute>[
GoRoute(
path: '/foo',
builder: _dummy,
),
],
);
}, throwsA(isAssertionError));
});
test('trailing / on sub-route', () {
expect(() {
GoRoute(
path: '/',
builder: _dummy,
routes: <GoRoute>[
GoRoute(
path: 'foo/',
builder: _dummy,
),
],
);
}, throwsA(isAssertionError));
});
test('lack of leading / on top-level route', () {
expect(() {
final List<GoRoute> routes = <GoRoute>[
GoRoute(path: 'foo', builder: _dummy),
];
_router(routes);
}, throwsA(isAssertionError));
});
test('match no routes', () {
final List<GoRoute> routes = <GoRoute>[
GoRoute(path: '/', builder: _dummy),
];
final GoRouter router = _router(routes);
router.go('/foo');
final List<GoRouteMatch> matches = router.routerDelegate.matches;
expect(matches, hasLength(1));
expect(router.screenFor(matches.first).runtimeType, ErrorScreen);
});
test('match 2nd top level route', () {
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 = _router(routes);
router.go('/login');
final List<GoRouteMatch> matches = router.routerDelegate.matches;
expect(matches, hasLength(1));
expect(matches.first.subloc, '/login');
expect(router.screenFor(matches.first).runtimeType, LoginScreen);
});
test('match top level route when location has trailing /', () {
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 = _router(routes);
router.go('/login/');
final List<GoRouteMatch> matches = router.routerDelegate.matches;
expect(matches, hasLength(1));
expect(matches.first.subloc, '/login');
expect(router.screenFor(matches.first).runtimeType, LoginScreen);
});
test('match top level route when location has trailing / (2)', () {
final List<GoRoute> routes = <GoRoute>[
GoRoute(path: '/profile', redirect: (_) => '/profile/foo'),
GoRoute(path: '/profile/:kind', builder: _dummy),
];
final GoRouter router = _router(routes);
router.go('/profile/');
final List<GoRouteMatch> matches = router.routerDelegate.matches;
expect(matches, hasLength(1));
expect(matches.first.subloc, '/profile/foo');
expect(router.screenFor(matches.first).runtimeType, DummyScreen);
});
test('match top level route when location has trailing / (3)', () {
final List<GoRoute> routes = <GoRoute>[
GoRoute(path: '/profile', redirect: (_) => '/profile/foo'),
GoRoute(path: '/profile/:kind', builder: _dummy),
];
final GoRouter router = _router(routes);
router.go('/profile/?bar=baz');
final List<GoRouteMatch> matches = router.routerDelegate.matches;
expect(matches, hasLength(1));
expect(matches.first.subloc, '/profile/foo');
expect(router.screenFor(matches.first).runtimeType, DummyScreen);
});
test('match sub-route', () {
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 = _router(routes);
router.go('/login');
final List<GoRouteMatch> matches = router.routerDelegate.matches;
expect(matches.length, 2);
expect(matches.first.subloc, '/');
expect(router.screenFor(matches.first).runtimeType, HomeScreen);
expect(matches[1].subloc, '/login');
expect(router.screenFor(matches[1]).runtimeType, LoginScreen);
});
test('match sub-routes', () {
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 = _router(routes);
{
final List<GoRouteMatch> matches = router.routerDelegate.matches;
expect(matches, hasLength(1));
expect(matches.first.fullpath, '/');
expect(router.screenFor(matches.first).runtimeType, HomeScreen);
}
router.go('/login');
{
final List<GoRouteMatch> matches = router.routerDelegate.matches;
expect(matches.length, 2);
expect(matches.first.subloc, '/');
expect(router.screenFor(matches.first).runtimeType, HomeScreen);
expect(matches[1].subloc, '/login');
expect(router.screenFor(matches[1]).runtimeType, LoginScreen);
}
router.go('/family/f2');
{
final List<GoRouteMatch> matches = router.routerDelegate.matches;
expect(matches.length, 2);
expect(matches.first.subloc, '/');
expect(router.screenFor(matches.first).runtimeType, HomeScreen);
expect(matches[1].subloc, '/family/f2');
expect(router.screenFor(matches[1]).runtimeType, FamilyScreen);
}
router.go('/family/f2/person/p1');
{
final List<GoRouteMatch> matches = router.routerDelegate.matches;
expect(matches.length, 3);
expect(matches.first.subloc, '/');
expect(router.screenFor(matches.first).runtimeType, HomeScreen);
expect(matches[1].subloc, '/family/f2');
expect(router.screenFor(matches[1]).runtimeType, FamilyScreen);
expect(matches[2].subloc, '/family/f2/person/p1');
expect(router.screenFor(matches[2]).runtimeType, PersonScreen);
}
});
test('return first matching route if too many subroutes', () {
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 = _router(routes);
router.go('/bar');
List<GoRouteMatch> matches = router.routerDelegate.matches;
expect(matches, hasLength(2));
expect(router.screenFor(matches[1]).runtimeType, Page1Screen);
router.go('/foo/bar');
matches = router.routerDelegate.matches;
expect(matches, hasLength(2));
expect(router.screenFor(matches[1]).runtimeType, FamilyScreen);
router.go('/foo');
matches = router.routerDelegate.matches;
expect(matches, hasLength(2));
expect(router.screenFor(matches[1]).runtimeType, Page2Screen);
});
test('router state', () {
final List<GoRoute> routes = <GoRoute>[
GoRoute(
name: 'home',
path: '/',
builder: (BuildContext context, GoRouterState state) {
expect(
state.location,
anyOf(<String>[
'/',
'/login',
'/family/f2',
'/family/f2/person/p1'
]),
);
expect(state.subloc, '/');
expect(state.name, 'home');
expect(state.path, '/');
expect(state.fullpath, '/');
expect(state.params, <String, String>{});
expect(state.error, 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 = _router(routes);
router.go('/', extra: 1);
router.go('/login', extra: 2);
router.go('/family/f2', extra: 3);
router.go('/family/f2/person/p1', extra: 4);
});
test('match path case insensitively', () {
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 = _router(routes);
const String loc = '/FaMiLy/f2';
router.go(loc);
final List<GoRouteMatch> matches = router.routerDelegate.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(router.screenFor(matches.first).runtimeType, FamilyScreen);
});
test('If there is more than one route to match, use the first match.', () {
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 = _router(routes);
router.go('/user');
final List<GoRouteMatch> matches = router.routerDelegate.matches;
expect(matches, hasLength(1));
expect(router.screenFor(matches.first).runtimeType, DummyScreen);
});
});
group('named routes', () {
test('match home route', () {
final List<GoRoute> routes = <GoRoute>[
GoRoute(
name: 'home',
path: '/',
builder: (BuildContext context, GoRouterState state) =>
const HomeScreen()),
];
final GoRouter router = _router(routes);
router.goNamed('home');
});
test('match too many routes', () {
final List<GoRoute> routes = <GoRoute>[
GoRoute(name: 'home', path: '/', builder: _dummy),
GoRoute(name: 'home', path: '/', builder: _dummy),
];
expect(() {
_router(routes);
}, throwsA(isAssertionError));
});
test('empty name', () {
expect(() {
GoRoute(name: '', path: '/');
}, throwsA(isAssertionError));
});
test('match no routes', () {
expect(() {
final List<GoRoute> routes = <GoRoute>[
GoRoute(name: 'home', path: '/', builder: _dummy),
];
final GoRouter router = _router(routes);
router.goNamed('work');
}, throwsA(isAssertionError));
});
test('match 2nd top level route', () {
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 = _router(routes);
router.goNamed('login');
});
test('match sub-route', () {
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 = _router(routes);
router.goNamed('login');
});
test('match sub-route case insensitive', () {
final List<GoRoute> routes = <GoRoute>[
GoRoute(
name: 'home',
path: '/',
builder: (BuildContext context, GoRouterState state) =>
const HomeScreen(),
routes: <GoRoute>[
GoRoute(
name: 'page1',
path: 'page1',
builder: (BuildContext context, GoRouterState state) =>
const Page1Screen(),
),
GoRoute(
name: 'page2',
path: 'Page2',
builder: (BuildContext context, GoRouterState state) =>
const Page2Screen(),
),
],
),
];
final GoRouter router = _router(routes);
router.goNamed('Page1');
router.goNamed('page2');
});
test('match w/ params', () {
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 = _router(routes);
router.goNamed('person',
params: <String, String>{'fid': 'f2', 'pid': 'p1'});
});
test('too few params', () {
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'),
),
],
),
],
),
];
expect(() {
final GoRouter router = _router(routes);
router.goNamed('person', params: <String, String>{'fid': 'f2'});
}, throwsA(isAssertionError));
});
test('match case insensitive w/ params', () {
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 = _router(routes);
router.goNamed('person',
params: <String, String>{'fid': 'f2', 'pid': 'p1'});
});
test('too few params', () {
final List<GoRoute> routes = <GoRoute>[
GoRoute(
name: 'family',
path: '/family/:fid',
builder: (BuildContext context, GoRouterState state) =>
const FamilyScreen('dummy'),
),
];
expect(() {
final GoRouter router = _router(routes);
router.goNamed('family');
}, throwsA(isAssertionError));
});
test('too many params', () {
final List<GoRoute> routes = <GoRoute>[
GoRoute(
name: 'family',
path: '/family/:fid',
builder: (BuildContext context, GoRouterState state) =>
const FamilyScreen('dummy'),
),
];
expect(() {
final GoRouter router = _router(routes);
router.goNamed('family',
params: <String, String>{'fid': 'f2', 'pid': 'p1'});
}, throwsA(isAssertionError));
});
test('sparsely named routes', () {
final List<GoRoute> routes = <GoRoute>[
GoRoute(
path: '/',
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 = _router(routes);
router.goNamed('person',
params: <String, String>{'fid': 'f2', 'pid': 'p1'});
final List<GoRouteMatch> matches = router.routerDelegate.matches;
expect(router.screenFor(matches.last).runtimeType, PersonScreen);
});
test('preserve path param spaces and slashes', () {
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 = _router(routes);
final String loc = router
.namedLocation('page1', params: <String, String>{'param1': param1});
log.info('loc= $loc');
router.go(loc);
final List<GoRouteMatch> matches = router.routerDelegate.matches;
log.info('param1= ${matches.first.decodedParams['param1']}');
expect(router.screenFor(matches.first).runtimeType, DummyScreen);
expect(matches.first.decodedParams['param1'], param1);
});
test('preserve query param spaces and slashes', () {
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 = _router(routes);
final String loc = router.namedLocation('page1',
queryParams: <String, String>{'param1': param1});
log.info('loc= $loc');
router.go(loc);
final List<GoRouteMatch> matches = router.routerDelegate.matches;
log.info('param1= ${matches.first.queryParams['param1']}');
expect(router.screenFor(matches.first).runtimeType, DummyScreen);
expect(matches.first.queryParams['param1'], param1);
});
});
group('redirects', () {
test('top-level redirect', () {
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()),
],
),
];
final GoRouter router = GoRouter(
routes: routes,
errorBuilder: _dummy,
redirect: (GoRouterState state) =>
state.subloc == '/login' ? null : '/login',
);
expect(router.location, '/login');
});
test('top-level redirect w/ named routes', () {
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 = GoRouter(
debugLogDiagnostics: true,
routes: routes,
errorBuilder: _dummy,
redirect: (GoRouterState state) =>
state.subloc == '/login' ? null : state.namedLocation('login'),
);
expect(router.location, '/login');
});
test('route-level redirect', () {
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: (GoRouterState state) => '/login',
),
GoRoute(
path: 'login',
builder: (BuildContext context, GoRouterState state) =>
const LoginScreen(),
),
],
),
];
final GoRouter router = GoRouter(
routes: routes,
errorBuilder: _dummy,
);
router.go('/dummy');
expect(router.location, '/login');
});
test('route-level redirect w/ named routes', () {
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: (GoRouterState state) => state.namedLocation('login'),
),
GoRoute(
name: 'login',
path: 'login',
builder: (BuildContext context, GoRouterState state) =>
const LoginScreen(),
),
],
),
];
final GoRouter router = GoRouter(
routes: routes,
errorBuilder: _dummy,
);
router.go('/dummy');
expect(router.location, '/login');
});
test('multiple mixed redirect', () {
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: (GoRouterState state) => '/',
),
],
),
];
final GoRouter router = GoRouter(
routes: routes,
errorBuilder: _dummy,
redirect: (GoRouterState state) =>
state.subloc == '/dummy1' ? '/dummy2' : null,
);
router.go('/dummy1');
expect(router.location, '/');
});
test('top-level redirect loop', () {
final GoRouter router = GoRouter(
routes: <GoRoute>[],
errorBuilder: (BuildContext context, GoRouterState state) =>
ErrorScreen(state.error!),
redirect: (GoRouterState state) => state.subloc == '/'
? '/login'
: state.subloc == '/login'
? '/'
: null,
);
final List<GoRouteMatch> matches = router.routerDelegate.matches;
expect(matches, hasLength(1));
expect(router.screenFor(matches.first).runtimeType, ErrorScreen);
expect((router.screenFor(matches.first) as ErrorScreen).ex, isNotNull);
log.info((router.screenFor(matches.first) as ErrorScreen).ex);
});
test('route-level redirect loop', () {
final GoRouter router = GoRouter(
routes: <GoRoute>[
GoRoute(
path: '/',
redirect: (GoRouterState state) => '/login',
),
GoRoute(
path: '/login',
redirect: (GoRouterState state) => '/',
),
],
errorBuilder: (BuildContext context, GoRouterState state) =>
ErrorScreen(state.error!),
);
final List<GoRouteMatch> matches = router.routerDelegate.matches;
expect(matches, hasLength(1));
expect(router.screenFor(matches.first).runtimeType, ErrorScreen);
expect((router.screenFor(matches.first) as ErrorScreen).ex, isNotNull);
log.info((router.screenFor(matches.first) as ErrorScreen).ex);
});
test('mixed redirect loop', () {
final GoRouter router = GoRouter(
routes: <GoRoute>[
GoRoute(
path: '/login',
redirect: (GoRouterState state) => '/',
),
],
errorBuilder: (BuildContext context, GoRouterState state) =>
ErrorScreen(state.error!),
redirect: (GoRouterState state) =>
state.subloc == '/' ? '/login' : null,
);
final List<GoRouteMatch> matches = router.routerDelegate.matches;
expect(matches, hasLength(1));
expect(router.screenFor(matches.first).runtimeType, ErrorScreen);
expect((router.screenFor(matches.first) as ErrorScreen).ex, isNotNull);
log.info((router.screenFor(matches.first) as ErrorScreen).ex);
});
test('top-level redirect loop w/ query params', () {
final GoRouter router = GoRouter(
routes: <GoRoute>[],
errorBuilder: (BuildContext context, GoRouterState state) =>
ErrorScreen(state.error!),
redirect: (GoRouterState state) => state.subloc == '/'
? '/login?from=${state.location}'
: state.subloc == '/login'
? '/'
: null,
);
final List<GoRouteMatch> matches = router.routerDelegate.matches;
expect(matches, hasLength(1));
expect(router.screenFor(matches.first).runtimeType, ErrorScreen);
expect((router.screenFor(matches.first) as ErrorScreen).ex, isNotNull);
log.info((router.screenFor(matches.first) as ErrorScreen).ex);
});
test('expect null path/fullpath on top-level redirect', () {
final List<GoRoute> routes = <GoRoute>[
GoRoute(
path: '/',
builder: (BuildContext context, GoRouterState state) =>
const HomeScreen(),
),
GoRoute(
path: '/dummy',
redirect: (GoRouterState state) => '/',
),
];
final GoRouter router = GoRouter(
routes: routes,
errorBuilder: _dummy,
initialLocation: '/dummy',
);
expect(router.location, '/');
});
test('top-level redirect state', () {
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 = GoRouter(
routes: routes,
errorBuilder: _dummy,
initialLocation: '/login?from=/',
debugLogDiagnostics: true,
redirect: (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<GoRouteMatch> matches = router.routerDelegate.matches;
expect(matches, hasLength(1));
expect(router.screenFor(matches.first).runtimeType, LoginScreen);
});
test('route-level redirect state', () {
const String loc = '/book/0';
final List<GoRoute> routes = <GoRoute>[
GoRoute(
path: '/book/:bookId',
redirect: (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 = GoRouter(
routes: routes,
errorBuilder: _dummy,
initialLocation: loc,
debugLogDiagnostics: true,
);
final List<GoRouteMatch> matches = router.routerDelegate.matches;
expect(matches, hasLength(1));
expect(router.screenFor(matches.first).runtimeType, HomeScreen);
});
test('sub-sub-route-level redirect params', () {
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: (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 = GoRouter(
routes: routes,
errorBuilder: _dummy,
initialLocation: '/family/f2/person/p1',
debugLogDiagnostics: true,
);
final List<GoRouteMatch> matches = router.routerDelegate.matches;
expect(matches.length, 3);
expect(router.screenFor(matches.first).runtimeType, HomeScreen);
expect(router.screenFor(matches[1]).runtimeType, FamilyScreen);
final PersonScreen page = router.screenFor(matches[2]) as PersonScreen;
expect(page.fid, 'f2');
expect(page.pid, 'p1');
});
test('redirect limit', () {
final GoRouter router = GoRouter(
routes: <GoRoute>[],
errorBuilder: (BuildContext context, GoRouterState state) =>
ErrorScreen(state.error!),
debugLogDiagnostics: true,
redirect: (GoRouterState state) => '${state.location}+',
redirectLimit: 10,
);
final List<GoRouteMatch> matches = router.routerDelegate.matches;
expect(matches, hasLength(1));
expect(router.screenFor(matches.first).runtimeType, ErrorScreen);
expect((router.screenFor(matches.first) as ErrorScreen).ex, isNotNull);
log.info((router.screenFor(matches.first) as ErrorScreen).ex);
});
});
group('initial location', () {
test('initial location', () {
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 = GoRouter(
routes: routes,
errorBuilder: _dummy,
initialLocation: '/dummy',
);
expect(router.location, '/dummy');
});
test('initial location w/ redirection', () {
final List<GoRoute> routes = <GoRoute>[
GoRoute(
path: '/',
builder: (BuildContext context, GoRouterState state) =>
const HomeScreen(),
),
GoRoute(
path: '/dummy',
redirect: (GoRouterState state) => '/',
),
];
final GoRouter router = GoRouter(
routes: routes,
errorBuilder: _dummy,
initialLocation: '/dummy',
);
expect(router.location, '/');
});
});
group('params', () {
test('preserve path param case', () {
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 = _router(routes);
for (final String fid in <String>['f2', 'F2']) {
final String loc = '/family/$fid';
router.go(loc);
final List<GoRouteMatch> matches = router.routerDelegate.matches;
expect(router.location, loc);
expect(matches, hasLength(1));
expect(router.screenFor(matches.first).runtimeType, FamilyScreen);
expect(matches.first.decodedParams['fid'], fid);
}
});
test('preserve query param case', () {
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 = _router(routes);
for (final String fid in <String>['f2', 'F2']) {
final String loc = '/family?fid=$fid';
router.go(loc);
final List<GoRouteMatch> matches = router.routerDelegate.matches;
expect(router.location, loc);
expect(matches, hasLength(1));
expect(router.screenFor(matches.first).runtimeType, FamilyScreen);
expect(matches.first.queryParams['fid'], fid);
}
});
test('preserve path param spaces and slashes', () {
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 = _router(routes);
final String loc = '/page1/${Uri.encodeComponent(param1)}';
router.go(loc);
final List<GoRouteMatch> matches = router.routerDelegate.matches;
log.info('param1= ${matches.first.decodedParams['param1']}');
expect(router.screenFor(matches.first).runtimeType, DummyScreen);
expect(matches.first.decodedParams['param1'], param1);
});
test('preserve query param spaces and slashes', () {
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 = _router(routes);
router.go('/page1?param1=$param1');
final List<GoRouteMatch> matches = router.routerDelegate.matches;
expect(router.screenFor(matches.first).runtimeType, DummyScreen);
expect(matches.first.queryParams['param1'], param1);
final String loc = '/page1?param1=${Uri.encodeQueryComponent(param1)}';
router.go(loc);
final List<GoRouteMatch> matches2 = router.routerDelegate.matches;
expect(router.screenFor(matches2[0]).runtimeType, DummyScreen);
expect(matches2[0].queryParams['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) =>
ErrorScreen(state.error!),
initialLocation: '/0/1/2/0/1',
);
expect(false, true);
} on Exception catch (ex) {
log.info(ex);
}
});
test('duplicate query param', () {
final GoRouter router = GoRouter(
routes: <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();
},
),
],
errorBuilder: (BuildContext context, GoRouterState state) =>
ErrorScreen(state.error!),
);
router.go('/?id=0&id=1');
final List<GoRouteMatch> matches = router.routerDelegate.matches;
expect(matches, hasLength(1));
expect(matches.first.fullpath, '/');
expect(router.screenFor(matches.first).runtimeType, HomeScreen);
});
test('duplicate path + query param', () {
final GoRouter router = GoRouter(
routes: <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();
},
),
],
errorBuilder: _dummy,
);
router.go('/0?id=1');
final List<GoRouteMatch> matches = router.routerDelegate.matches;
expect(matches, hasLength(1));
expect(matches.first.fullpath, '/:id');
expect(router.screenFor(matches.first).runtimeType, HomeScreen);
});
test('push + query param', () {
final GoRouter router = GoRouter(
routes: <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']!,
),
),
],
errorBuilder: _dummy,
);
router.go('/family?fid=f2');
router.push('/person?fid=f2&pid=p1');
final FamilyScreen page1 =
router.screenFor(router.routerDelegate.matches.first) as FamilyScreen;
expect(page1.fid, 'f2');
final PersonScreen page2 =
router.screenFor(router.routerDelegate.matches[1]) as PersonScreen;
expect(page2.fid, 'f2');
expect(page2.pid, 'p1');
});
test('push + extra param', () {
final GoRouter router = GoRouter(
routes: <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']!,
),
),
],
errorBuilder: _dummy,
);
router.go('/family', extra: <String, String>{'fid': 'f2'});
router.push('/person', extra: <String, String>{'fid': 'f2', 'pid': 'p1'});
final FamilyScreen page1 =
router.screenFor(router.routerDelegate.matches.first) as FamilyScreen;
expect(page1.fid, 'f2');
final PersonScreen page2 =
router.screenFor(router.routerDelegate.matches[1]) as PersonScreen;
expect(page2.fid, 'f2');
expect(page2.pid, 'p1');
});
test('keep param in nested route', () {
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 = _router(routes);
const String fid = 'f1';
const String pid = 'p2';
const String loc = '/family/$fid/person/$pid';
router.push(loc);
final List<GoRouteMatch> matches = router.routerDelegate.matches;
expect(router.location, loc);
expect(matches, hasLength(2));
expect(router.screenFor(matches.last).runtimeType, PersonScreen);
expect(matches.last.decodedParams['fid'], fid);
expect(matches.last.decodedParams['pid'], pid);
});
});
group('refresh listenable', () {
late StreamController<int> streamController;
setUpAll(() async {
streamController = StreamController<int>.broadcast();
await streamController.addStream(Stream<int>.value(0));
});
tearDownAll(() {
streamController.close();
});
group('stream', () {
test('no stream emits', () async {
// Act
final GoRouterRefreshStreamSpy notifyListener =
GoRouterRefreshStreamSpy(
streamController.stream,
);
// Assert
expect(notifyListener.notifyCount, equals(1));
// Cleanup
notifyListener.dispose();
});
test('three stream emits', () async {
// Arrange
final List<int> toEmit = <int>[1, 2, 3];
// Act
final GoRouterRefreshStreamSpy notifyListener =
GoRouterRefreshStreamSpy(
streamController.stream,
);
await streamController.addStream(Stream<int>.fromIterable(toEmit));
// Assert
expect(notifyListener.notifyCount, equals(toEmit.length + 1));
// Cleanup
notifyListener.dispose();
});
});
});
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(
routeInformationParser: router.routeInformationParser,
routerDelegate: router.routerDelegate,
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(
routeInformationParser: router.routeInformationParser,
routerDelegate: router.routerDelegate,
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(
routeInformationParser: router.routeInformationParser,
routerDelegate: router.routerDelegate,
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(
routeInformationParser: router.routeInformationParser,
routerDelegate: router.routerDelegate,
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(
routeInformationParser: router.routeInformationParser,
routerDelegate: router.routerDelegate,
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(
routeInformationParser: router.routeInformationParser,
routerDelegate: router.routerDelegate,
title: 'GoRouter Example',
),
);
key.currentContext!.pop();
expect(router.popped, true);
});
});
test('pop triggers pop on routerDelegate', () {
final GoRouter router = createGoRouter()..push('/error');
router.routerDelegate.addListener(expectAsync0(() {}));
router.pop();
});
test('refresh triggers refresh on routerDelegate', () {
final GoRouter router = createGoRouter();
router.routerDelegate.addListener(expectAsync0(() {}));
router.refresh();
});
test('didPush notifies listeners', () {
createGoRouter()
..addListener(expectAsync0(() {}))
..didPush(
MaterialPageRoute<void>(builder: (_) => const Text('Current route')),
MaterialPageRoute<void>(builder: (_) => const Text('Previous route')),
);
});
test('didPop notifies listeners', () {
createGoRouter()
..addListener(expectAsync0(() {}))
..didPop(
MaterialPageRoute<void>(builder: (_) => const Text('Current route')),
MaterialPageRoute<void>(builder: (_) => const Text('Previous route')),
);
});
test('didRemove notifies listeners', () {
createGoRouter()
..addListener(expectAsync0(() {}))
..didRemove(
MaterialPageRoute<void>(builder: (_) => const Text('Current route')),
MaterialPageRoute<void>(builder: (_) => const Text('Previous route')),
);
});
test('didReplace notifies listeners', () {
createGoRouter()
..addListener(expectAsync0(() {}))
..didReplace(
newRoute: MaterialPageRoute<void>(
builder: (_) => const Text('Current route'),
),
oldRoute: MaterialPageRoute<void>(
builder: (_) => const Text('Previous route'),
),
);
});
test('uses navigatorBuilder when provided', () {
final Func3<Widget, BuildContext, GoRouterState, Widget> navigationBuilder =
expectAsync3(fakeNavigationBuilder);
final GoRouter router = createGoRouter(navigatorBuilder: navigationBuilder);
final GoRouterDelegate delegate = router.routerDelegate;
delegate.builderWithNav(
DummyBuildContext(),
GoRouterState(delegate, location: '/foo', subloc: '/bar', name: 'baz'),
const Navigator(),
);
});
}
GoRouter createGoRouter({
GoRouterNavigatorBuilder? navigatorBuilder,
}) =>
GoRouter(
initialLocation: '/',
routes: <GoRoute>[
GoRoute(path: '/', builder: (_, __) => const DummyStatefulWidget()),
GoRoute(
path: '/error',
builder: (_, __) => const GoRouterErrorScreen(null),
),
],
navigatorBuilder: navigatorBuilder,
);
Widget fakeNavigationBuilder(
BuildContext context,
GoRouterState state,
Widget child,
) =>
child;
class GoRouterNamedLocationSpy extends GoRouter {
GoRouterNamedLocationSpy({required List<GoRoute> routes})
: super(routes: routes);
String? name;
Map<String, String>? params;
Map<String, String>? queryParams;
@override
String namedLocation(
String name, {
Map<String, String> params = const <String, String>{},
Map<String, String> queryParams = const <String, String>{},
}) {
this.name = name;
this.params = params;
this.queryParams = queryParams;
return '';
}
}
class GoRouterGoSpy extends GoRouter {
GoRouterGoSpy({required List<GoRoute> routes}) : super(routes: routes);
String? myLocation;
Object? extra;
@override
void go(String location, {Object? extra}) {
myLocation = location;
this.extra = extra;
}
}
class GoRouterGoNamedSpy extends GoRouter {
GoRouterGoNamedSpy({required List<GoRoute> routes}) : super(routes: routes);
String? name;
Map<String, String>? params;
Map<String, String>? queryParams;
Object? extra;
@override
void goNamed(
String name, {
Map<String, String> params = const <String, String>{},
Map<String, String> queryParams = const <String, String>{},
Object? extra,
}) {
this.name = name;
this.params = params;
this.queryParams = queryParams;
this.extra = extra;
}
}
class GoRouterPushSpy extends GoRouter {
GoRouterPushSpy({required List<GoRoute> routes}) : super(routes: routes);
String? myLocation;
Object? extra;
@override
void push(String location, {Object? extra}) {
myLocation = location;
this.extra = extra;
}
}
class GoRouterPushNamedSpy extends GoRouter {
GoRouterPushNamedSpy({required List<GoRoute> routes}) : super(routes: routes);
String? name;
Map<String, String>? params;
Map<String, String>? queryParams;
Object? extra;
@override
void pushNamed(
String name, {
Map<String, String> params = const <String, String>{},
Map<String, String> queryParams = const <String, String>{},
Object? extra,
}) {
this.name = name;
this.params = params;
this.queryParams = queryParams;
this.extra = extra;
}
}
class GoRouterPopSpy extends GoRouter {
GoRouterPopSpy({required List<GoRoute> routes}) : super(routes: routes);
bool popped = false;
@override
void pop() {
popped = true;
}
}
class GoRouterRefreshStreamSpy extends GoRouterRefreshStream {
GoRouterRefreshStreamSpy(
Stream<dynamic> stream,
) : notifyCount = 0,
super(stream);
late int notifyCount;
@override
void notifyListeners() {
notifyCount++;
super.notifyListeners();
}
}
GoRouter _router(List<GoRoute> routes) => GoRouter(
routes: routes,
errorBuilder: (BuildContext context, GoRouterState state) =>
ErrorScreen(state.error!),
debugLogDiagnostics: true,
);
class ErrorScreen extends DummyScreen {
const ErrorScreen(this.ex, {Key? key}) : super(key: key);
final Exception ex;
}
class HomeScreen extends DummyScreen {
const HomeScreen({Key? key}) : super(key: key);
}
class Page1Screen extends DummyScreen {
const Page1Screen({Key? key}) : super(key: key);
}
class Page2Screen extends DummyScreen {
const Page2Screen({Key? key}) : super(key: key);
}
class LoginScreen extends DummyScreen {
const LoginScreen({Key? key}) : super(key: key);
}
class FamilyScreen extends DummyScreen {
const FamilyScreen(this.fid, {Key? key}) : super(key: key);
final String fid;
}
class FamiliesScreen extends DummyScreen {
const FamiliesScreen({required this.selectedFid, Key? key}) : super(key: key);
final String selectedFid;
}
class PersonScreen extends DummyScreen {
const PersonScreen(this.fid, this.pid, {Key? key}) : super(key: key);
final String fid;
final String pid;
}
class DummyScreen extends StatelessWidget {
const DummyScreen({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) => throw UnimplementedError();
}
Widget _dummy(BuildContext context, GoRouterState state) => const DummyScreen();
extension on GoRouter {
Page<dynamic> _pageFor(GoRouteMatch match) {
final List<GoRouteMatch> matches = routerDelegate.matches;
final int i = matches.indexOf(match);
final List<Page<dynamic>> pages =
routerDelegate.getPages(DummyBuildContext(), matches).toList();
return pages[i];
}
Widget screenFor(GoRouteMatch match) =>
(_pageFor(match) as NoTransitionPage<void>).child;
}
class DummyBuildContext implements BuildContext {
@override
bool get debugDoingBuild => throw UnimplementedError();
@override
InheritedWidget dependOnInheritedElement(InheritedElement ancestor,
{Object aspect = 1}) {
throw UnimplementedError();
}
@override
T? dependOnInheritedWidgetOfExactType<T extends InheritedWidget>(
{Object? aspect}) {
throw UnimplementedError();
}
@override
DiagnosticsNode describeElement(String name,
{DiagnosticsTreeStyle style = DiagnosticsTreeStyle.errorProperty}) {
throw UnimplementedError();
}
@override
List<DiagnosticsNode> describeMissingAncestor(
{required Type expectedAncestorType}) {
throw UnimplementedError();
}
@override
DiagnosticsNode describeOwnershipChain(String name) {
throw UnimplementedError();
}
@override
DiagnosticsNode describeWidget(String name,
{DiagnosticsTreeStyle style = DiagnosticsTreeStyle.errorProperty}) {
throw UnimplementedError();
}
// @override
// TODO(dit): Remove ignore below when flutter 2.11.0-0.0.pre.724 becomes stable
// ignore:annotate_overrides
void dispatchNotification(Notification notification) {
throw UnimplementedError();
}
@override
T? findAncestorRenderObjectOfType<T extends RenderObject>() {
throw UnimplementedError();
}
@override
T? findAncestorStateOfType<T extends State<StatefulWidget>>() {
throw UnimplementedError();
}
@override
T? findAncestorWidgetOfExactType<T extends Widget>() {
throw UnimplementedError();
}
@override
RenderObject? findRenderObject() {
throw UnimplementedError();
}
@override
T? findRootAncestorStateOfType<T extends State<StatefulWidget>>() {
throw UnimplementedError();
}
@override
InheritedElement?
getElementForInheritedWidgetOfExactType<T extends InheritedWidget>() {
throw UnimplementedError();
}
@override
BuildOwner? get owner => throw UnimplementedError();
@override
Size? get size => throw UnimplementedError();
@override
void visitAncestorElements(bool Function(Element element) visitor) {}
@override
void visitChildElements(ElementVisitor visitor) {}
@override
Widget get widget => throw UnimplementedError();
}
class DummyStatefulWidget extends StatefulWidget {
const DummyStatefulWidget({Key? key}) : super(key: key);
@override
State<DummyStatefulWidget> createState() => _DummyStatefulWidgetState();
}
class _DummyStatefulWidgetState extends State<DummyStatefulWidget> {
@override
Widget build(BuildContext context) => Container();
}