[go_router] improve coverage (#977)
diff --git a/packages/go_router/CHANGELOG.md b/packages/go_router/CHANGELOG.md
index f60d9f0..2dd8a6b 100644
--- a/packages/go_router/CHANGELOG.md
+++ b/packages/go_router/CHANGELOG.md
@@ -1,7 +1,9 @@
-## NEXT
+## 3.0.5
- Add `dispatchNotification` method to `DummyBuildContext` in tests. (This
should be revisited when Flutter `2.11.0` becomes stable.)
+- Improves code coverage.
+- `GoRoute` now warns about requiring either `pageBuilder`, `builder` or `redirect` at instantiation.
## 3.0.4
diff --git a/packages/go_router/lib/src/go_route.dart b/packages/go_router/lib/src/go_route.dart
index 43c9123..dbe5882 100644
--- a/packages/go_router/lib/src/go_route.dart
+++ b/packages/go_router/lib/src/go_route.dart
@@ -18,9 +18,9 @@
required this.path,
this.name,
this.pageBuilder,
- this.builder = _builder,
+ this.builder = _invalidBuilder,
this.routes = const <GoRoute>[],
- this.redirect = _redirect,
+ this.redirect = _noRedirection,
}) {
if (path.isEmpty) {
throw Exception('GoRoute path cannot be empty');
@@ -30,6 +30,15 @@
throw Exception('GoRoute name cannot be empty');
}
+ if (pageBuilder == null &&
+ builder == _invalidBuilder &&
+ redirect == _noRedirection) {
+ throw Exception(
+ 'GoRoute builder parameter not set\n'
+ 'See gorouter.dev/redirection#considerations for details',
+ );
+ }
+
// cache the path regexp and parameters
_pathRE = patternToRegExp(path, _pathParams);
@@ -199,11 +208,11 @@
Map<String, String> extractPathParams(RegExpMatch match) =>
extractPathParameters(_pathParams, match);
- static String? _redirect(GoRouterState state) => null;
+ static String? _noRedirection(GoRouterState state) => null;
- static Widget _builder(BuildContext context, GoRouterState state) =>
- throw Exception(
- 'GoRoute builder parameter not set\n'
- 'See gorouter.dev/redirection#considerations for details',
- );
+ static Widget _invalidBuilder(
+ BuildContext context,
+ GoRouterState state,
+ ) =>
+ const SizedBox.shrink();
}
diff --git a/packages/go_router/lib/src/inherited_go_router.dart b/packages/go_router/lib/src/inherited_go_router.dart
index e5c9d73..b746612 100644
--- a/packages/go_router/lib/src/inherited_go_router.dart
+++ b/packages/go_router/lib/src/inherited_go_router.dart
@@ -25,9 +25,9 @@
/// Used by the Router architecture as part of the InheritedWidget.
@override
// ignore: prefer_expression_function_bodies
- bool updateShouldNotify(covariant InheritedWidget oldWidget) {
+ bool updateShouldNotify(covariant InheritedGoRouter oldWidget) {
// avoid rebuilding the widget tree if the router has not changed
- return false;
+ return goRouter != oldWidget.goRouter;
}
@override
diff --git a/packages/go_router/pubspec.yaml b/packages/go_router/pubspec.yaml
index b340fad..b333bfa 100644
--- a/packages/go_router/pubspec.yaml
+++ b/packages/go_router/pubspec.yaml
@@ -1,7 +1,7 @@
name: go_router
description: A declarative router for Flutter based on Navigation 2 supporting
deep linking, data-driven routes and more
-version: 3.0.4
+version: 3.0.5
repository: https://github.com/flutter/packages/tree/main/packages/go_router
issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+go_router%22
diff --git a/packages/go_router/test/custom_transition_page_test.dart b/packages/go_router/test/custom_transition_page_test.dart
new file mode 100644
index 0000000..98ac8cd
--- /dev/null
+++ b/packages/go_router/test/custom_transition_page_test.dart
@@ -0,0 +1,114 @@
+// 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.
+
+import 'package:flutter/material.dart';
+import 'package:flutter_test/flutter_test.dart';
+import 'package:go_router/go_router.dart';
+
+void main() {
+ testWidgets('CustomTransitionPage builds its child using transitionsBuilder',
+ (WidgetTester tester) async {
+ const HomeScreen child = HomeScreen();
+ final CustomTransitionPage<void> transition = CustomTransitionPage<void>(
+ transitionsBuilder: expectAsync4((_, __, ___, Widget child) => child),
+ child: child,
+ );
+ final GoRouter router = GoRouter(
+ routes: <GoRoute>[
+ GoRoute(
+ path: '/',
+ pageBuilder: (_, __) => transition,
+ ),
+ ],
+ );
+ await tester.pumpWidget(
+ MaterialApp.router(
+ routeInformationParser: router.routeInformationParser,
+ routerDelegate: router.routerDelegate,
+ title: 'GoRouter Example',
+ ),
+ );
+ expect(find.byWidget(child), findsOneWidget);
+ });
+
+ testWidgets('NoTransitionPage does not apply any transition',
+ (WidgetTester tester) async {
+ final ValueNotifier<bool> showHomeValueNotifier =
+ ValueNotifier<bool>(false);
+ await tester.pumpWidget(
+ MaterialApp(
+ home: ValueListenableBuilder<bool>(
+ valueListenable: showHomeValueNotifier,
+ builder: (_, bool showHome, __) {
+ return Navigator(
+ pages: <Page<void>>[
+ const NoTransitionPage<void>(
+ child: LoginScreen(),
+ ),
+ if (showHome)
+ const NoTransitionPage<void>(
+ child: HomeScreen(),
+ ),
+ ],
+ onPopPage: (Route<dynamic> route, dynamic result) {
+ return route.didPop(result);
+ },
+ );
+ },
+ ),
+ ),
+ );
+
+ final Finder homeScreenFinder = find.byType(HomeScreen);
+
+ showHomeValueNotifier.value = true;
+ await tester.pump();
+ final Offset homeScreenPositionInTheMiddleOfAddition =
+ tester.getTopLeft(homeScreenFinder);
+ await tester.pumpAndSettle();
+ final Offset homeScreenPositionAfterAddition =
+ tester.getTopLeft(homeScreenFinder);
+
+ showHomeValueNotifier.value = false;
+ await tester.pump();
+ final Offset homeScreenPositionInTheMiddleOfRemoval =
+ tester.getTopLeft(homeScreenFinder);
+ await tester.pumpAndSettle();
+
+ expect(
+ homeScreenPositionInTheMiddleOfAddition,
+ homeScreenPositionAfterAddition,
+ );
+ expect(
+ homeScreenPositionAfterAddition,
+ homeScreenPositionInTheMiddleOfRemoval,
+ );
+ });
+}
+
+class HomeScreen extends StatelessWidget {
+ const HomeScreen({Key? key}) : super(key: key);
+
+ @override
+ Widget build(BuildContext context) {
+ return const Scaffold(
+ body: Center(
+ child: Text('HomeScreen'),
+ ),
+ );
+ }
+}
+
+class LoginScreen extends StatelessWidget {
+ const LoginScreen({Key? key}) : super(key: key);
+
+ @override
+ Widget build(BuildContext context) {
+ return const Scaffold(
+ body: Center(
+ child: Text('LoginScreen'),
+ ),
+ );
+ }
+}
diff --git a/packages/go_router/test/error_screen_helpers.dart b/packages/go_router/test/error_screen_helpers.dart
new file mode 100644
index 0000000..c8ae740
--- /dev/null
+++ b/packages/go_router/test/error_screen_helpers.dart
@@ -0,0 +1,66 @@
+// 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.
+
+import 'package:flutter/cupertino.dart';
+import 'package:flutter/material.dart';
+import 'package:flutter_test/flutter_test.dart';
+import 'package:go_router/go_router.dart';
+
+import 'go_router_test.dart';
+
+WidgetTesterCallback testPageNotFound({required Widget widget}) {
+ return (WidgetTester tester) async {
+ await tester.pumpWidget(widget);
+ expect(find.text('page not found'), findsOneWidget);
+ };
+}
+
+WidgetTesterCallback testPageShowsExceptionMessage({
+ required Exception exception,
+ required Widget widget,
+}) {
+ return (WidgetTester tester) async {
+ await tester.pumpWidget(widget);
+ expect(find.text('$exception'), findsOneWidget);
+ };
+}
+
+WidgetTesterCallback testClickingTheButtonRedirectsToRoot({
+ required Finder buttonFinder,
+ required Widget widget,
+ Widget Function(GoRouter router) appRouterBuilder = materialAppRouterBuilder,
+}) {
+ return (WidgetTester tester) async {
+ final GoRouter router = GoRouter(
+ initialLocation: '/error',
+ routes: <GoRoute>[
+ GoRoute(path: '/', builder: (_, __) => const DummyStatefulWidget()),
+ GoRoute(
+ path: '/error',
+ builder: (_, __) => widget,
+ ),
+ ],
+ );
+ await tester.pumpWidget(appRouterBuilder(router));
+ await tester.tap(buttonFinder);
+ await tester.pumpAndSettle();
+ expect(find.byType(DummyStatefulWidget), findsOneWidget);
+ };
+}
+
+Widget materialAppRouterBuilder(GoRouter router) {
+ return MaterialApp.router(
+ routeInformationParser: router.routeInformationParser,
+ routerDelegate: router.routerDelegate,
+ title: 'GoRouter Example',
+ );
+}
+
+Widget cupertinoAppRouterBuilder(GoRouter router) {
+ return CupertinoApp.router(
+ routeInformationParser: router.routeInformationParser,
+ routerDelegate: router.routerDelegate,
+ title: 'GoRouter Example',
+ );
+}
diff --git a/packages/go_router/test/go_route_test.dart b/packages/go_router/test/go_route_test.dart
new file mode 100644
index 0000000..63b2912
--- /dev/null
+++ b/packages/go_router/test/go_route_test.dart
@@ -0,0 +1,12 @@
+// 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.
+
+import 'package:flutter_test/flutter_test.dart';
+import 'package:go_router/go_router.dart';
+
+void main() {
+ test('throws when a builder is not set', () {
+ expect(() => GoRoute(path: '/'), throwsException);
+ });
+}
diff --git a/packages/go_router/test/go_router_cupertino_test.dart b/packages/go_router/test/go_router_cupertino_test.dart
new file mode 100644
index 0000000..594a865
--- /dev/null
+++ b/packages/go_router/test/go_router_cupertino_test.dart
@@ -0,0 +1,105 @@
+// 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.
+
+import 'package:flutter/cupertino.dart';
+import 'package:flutter/material.dart';
+import 'package:flutter_test/flutter_test.dart';
+import 'package:go_router/src/go_router_cupertino.dart';
+
+import 'error_screen_helpers.dart';
+
+void main() {
+ group('isCupertinoApp', () {
+ testWidgets('returns [true] when CupertinoApp is present',
+ (WidgetTester tester) async {
+ final GlobalKey<_DummyStatefulWidgetState> key =
+ GlobalKey<_DummyStatefulWidgetState>();
+ await tester.pumpWidget(
+ CupertinoApp(
+ home: DummyStatefulWidget(key: key),
+ ),
+ );
+ final bool isCupertino = isCupertinoApp(key.currentContext! as Element);
+ expect(isCupertino, true);
+ });
+
+ testWidgets('returns [false] when MaterialApp is present',
+ (WidgetTester tester) async {
+ final GlobalKey<_DummyStatefulWidgetState> key =
+ GlobalKey<_DummyStatefulWidgetState>();
+ await tester.pumpWidget(
+ MaterialApp(
+ home: DummyStatefulWidget(key: key),
+ ),
+ );
+ final bool isCupertino = isCupertinoApp(key.currentContext! as Element);
+ expect(isCupertino, false);
+ });
+ });
+
+ test('pageBuilderForCupertinoApp creates a [CupertinoPage] accordingly', () {
+ final UniqueKey key = UniqueKey();
+ const String name = 'name';
+ const String arguments = 'arguments';
+ const String restorationId = 'restorationId';
+ const DummyStatefulWidget child = DummyStatefulWidget();
+ final CupertinoPage<void> page = pageBuilderForCupertinoApp(
+ key: key,
+ name: name,
+ arguments: arguments,
+ restorationId: restorationId,
+ child: child,
+ );
+ expect(page.key, key);
+ expect(page.name, name);
+ expect(page.arguments, arguments);
+ expect(page.restorationId, restorationId);
+ expect(page.child, child);
+ });
+
+ group('GoRouterCupertinoErrorScreen', () {
+ testWidgets(
+ 'shows "page not found" by default',
+ testPageNotFound(
+ widget: const CupertinoApp(
+ home: GoRouterCupertinoErrorScreen(null),
+ ),
+ ),
+ );
+
+ final Exception exception = Exception('Something went wrong!');
+ testWidgets(
+ 'shows the exception message when provided',
+ testPageShowsExceptionMessage(
+ exception: exception,
+ widget: CupertinoApp(
+ home: GoRouterCupertinoErrorScreen(exception),
+ ),
+ ),
+ );
+
+ testWidgets(
+ 'clicking the CupertinoButton should redirect to /',
+ testClickingTheButtonRedirectsToRoot(
+ buttonFinder: find.byType(CupertinoButton),
+ appRouterBuilder: cupertinoAppRouterBuilder,
+ widget: const CupertinoApp(
+ home: GoRouterCupertinoErrorScreen(null),
+ ),
+ ),
+ );
+ });
+}
+
+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();
+}
diff --git a/packages/go_router/test/go_router_delegate_test.dart b/packages/go_router/test/go_router_delegate_test.dart
new file mode 100644
index 0000000..d577856
--- /dev/null
+++ b/packages/go_router/test/go_router_delegate_test.dart
@@ -0,0 +1,79 @@
+// 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.
+
+import 'package:flutter/material.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';
+
+GoRouterDelegate createGoRouterDelegate({
+ Listenable? refreshListenable,
+}) {
+ final GoRouter router = GoRouter(
+ initialLocation: '/',
+ routes: <GoRoute>[
+ GoRoute(path: '/', builder: (_, __) => const DummyStatefulWidget()),
+ GoRoute(
+ path: '/error',
+ builder: (_, __) => const GoRouterErrorScreen(null),
+ ),
+ ],
+ refreshListenable: refreshListenable,
+ );
+ return router.routerDelegate;
+}
+
+void main() {
+ group('pop', () {
+ test('removes the last element', () {
+ final GoRouterDelegate delegate = createGoRouterDelegate()
+ ..push('/error')
+ ..addListener(expectAsync0(() {}));
+ final GoRouteMatch last = delegate.matches.last;
+ delegate.pop();
+ expect(delegate.matches.length, 1);
+ expect(delegate.matches.contains(last), false);
+ });
+
+ test('throws when it pops more than matches count', () {
+ final GoRouterDelegate delegate = createGoRouterDelegate()
+ ..push('/error');
+ expect(
+ () => delegate
+ ..pop()
+ ..pop(),
+ throwsException,
+ );
+ });
+ });
+
+ test('dispose unsubscribes from refreshListenable', () {
+ final FakeRefreshListenable refreshListenable = FakeRefreshListenable();
+ createGoRouterDelegate(refreshListenable: refreshListenable).dispose();
+ expect(refreshListenable.unsubscribed, true);
+ });
+}
+
+class FakeRefreshListenable extends ChangeNotifier {
+ bool unsubscribed = false;
+ @override
+ void removeListener(VoidCallback listener) {
+ unsubscribed = true;
+ super.removeListener(listener);
+ }
+}
+
+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();
+}
diff --git a/packages/go_router/test/go_router_error_page_test.dart b/packages/go_router/test/go_router_error_page_test.dart
new file mode 100644
index 0000000..3e5b2fd
--- /dev/null
+++ b/packages/go_router/test/go_router_error_page_test.dart
@@ -0,0 +1,65 @@
+// 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.
+
+import 'package:flutter/material.dart';
+import 'package:flutter_test/flutter_test.dart';
+import 'package:go_router/src/go_router_error_page.dart';
+
+import 'error_screen_helpers.dart';
+
+void main() {
+ testWidgets(
+ 'shows "page not found" by default',
+ testPageNotFound(
+ widget: widgetsAppBuilder(
+ home: const GoRouterErrorScreen(null),
+ ),
+ ),
+ );
+
+ final Exception exception = Exception('Something went wrong!');
+ testWidgets(
+ 'shows the exception message when provided',
+ testPageShowsExceptionMessage(
+ exception: exception,
+ widget: widgetsAppBuilder(
+ home: GoRouterErrorScreen(exception),
+ ),
+ ),
+ );
+
+ testWidgets(
+ 'clicking the button should redirect to /',
+ testClickingTheButtonRedirectsToRoot(
+ buttonFinder:
+ find.byWidgetPredicate((Widget widget) => widget is GestureDetector),
+ widget: widgetsAppBuilder(
+ home: const GoRouterErrorScreen(null),
+ ),
+ ),
+ );
+}
+
+Widget widgetsAppBuilder({required Widget home}) {
+ return WidgetsApp(
+ onGenerateRoute: (_) {
+ return MaterialPageRoute<void>(
+ builder: (BuildContext _) => home,
+ );
+ },
+ color: Colors.white,
+ );
+}
+
+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();
+}
diff --git a/packages/go_router/test/go_router_material_test.dart b/packages/go_router/test/go_router_material_test.dart
new file mode 100644
index 0000000..8a0c595
--- /dev/null
+++ b/packages/go_router/test/go_router_material_test.dart
@@ -0,0 +1,104 @@
+// 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.
+
+import 'package:flutter/cupertino.dart';
+import 'package:flutter/material.dart';
+import 'package:flutter_test/flutter_test.dart';
+import 'package:go_router/src/go_router_material.dart';
+
+import 'error_screen_helpers.dart';
+
+void main() {
+ group('isMaterialApp', () {
+ testWidgets('returns [true] when MaterialApp is present',
+ (WidgetTester tester) async {
+ final GlobalKey<_DummyStatefulWidgetState> key =
+ GlobalKey<_DummyStatefulWidgetState>();
+ await tester.pumpWidget(
+ MaterialApp(
+ home: DummyStatefulWidget(key: key),
+ ),
+ );
+ final bool isMaterial = isMaterialApp(key.currentContext! as Element);
+ expect(isMaterial, true);
+ });
+
+ testWidgets('returns [false] when CupertinoApp is present',
+ (WidgetTester tester) async {
+ final GlobalKey<_DummyStatefulWidgetState> key =
+ GlobalKey<_DummyStatefulWidgetState>();
+ await tester.pumpWidget(
+ CupertinoApp(
+ home: DummyStatefulWidget(key: key),
+ ),
+ );
+ final bool isMaterial = isMaterialApp(key.currentContext! as Element);
+ expect(isMaterial, false);
+ });
+ });
+
+ test('pageBuilderForMaterialApp creates a [MaterialPage] accordingly', () {
+ final UniqueKey key = UniqueKey();
+ const String name = 'name';
+ const String arguments = 'arguments';
+ const String restorationId = 'restorationId';
+ const DummyStatefulWidget child = DummyStatefulWidget();
+ final MaterialPage<void> page = pageBuilderForMaterialApp(
+ key: key,
+ name: name,
+ arguments: arguments,
+ restorationId: restorationId,
+ child: child,
+ );
+ expect(page.key, key);
+ expect(page.name, name);
+ expect(page.arguments, arguments);
+ expect(page.restorationId, restorationId);
+ expect(page.child, child);
+ });
+
+ group('GoRouterMaterialErrorScreen', () {
+ testWidgets(
+ 'shows "page not found" by default',
+ testPageNotFound(
+ widget: const MaterialApp(
+ home: GoRouterMaterialErrorScreen(null),
+ ),
+ ),
+ );
+
+ final Exception exception = Exception('Something went wrong!');
+ testWidgets(
+ 'shows the exception message when provided',
+ testPageShowsExceptionMessage(
+ exception: exception,
+ widget: MaterialApp(
+ home: GoRouterMaterialErrorScreen(exception),
+ ),
+ ),
+ );
+
+ testWidgets(
+ 'clicking the TextButton should redirect to /',
+ testClickingTheButtonRedirectsToRoot(
+ buttonFinder: find.byType(TextButton),
+ widget: const MaterialApp(
+ home: GoRouterMaterialErrorScreen(null),
+ ),
+ ),
+ );
+ });
+}
+
+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();
+}
diff --git a/packages/go_router/test/go_router_test.dart b/packages/go_router/test/go_router_test.dart
index 25fd04f..718aa6e 100644
--- a/packages/go_router/test/go_router_test.dart
+++ b/packages/go_router/test/go_router_test.dart
@@ -8,10 +8,12 @@
import 'package:flutter/material.dart';
import 'package:flutter/src/foundation/diagnostics.dart';
-import 'package:flutter/widgets.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;
@@ -1496,8 +1498,8 @@
group('stream', () {
test('no stream emits', () async {
// Act
- final MockGoRouterRefreshStream notifyListener =
- MockGoRouterRefreshStream(
+ final GoRouterRefreshStreamSpy notifyListener =
+ GoRouterRefreshStreamSpy(
streamController.stream,
);
@@ -1513,8 +1515,8 @@
final List<int> toEmit = <int>[1, 2, 3];
// Act
- final MockGoRouterRefreshStream notifyListener =
- MockGoRouterRefreshStream(
+ final GoRouterRefreshStreamSpy notifyListener =
+ GoRouterRefreshStreamSpy(
streamController.stream,
);
@@ -1528,10 +1530,340 @@
});
});
});
+
+ 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(),
+ );
+ });
}
-class MockGoRouterRefreshStream extends GoRouterRefreshStream {
- MockGoRouterRefreshStream(
+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);
@@ -1703,3 +2035,15 @@
@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();
+}
diff --git a/packages/go_router/test/inherited_go_router_test.dart b/packages/go_router/test/inherited_go_router_test.dart
new file mode 100644
index 0000000..cadf955
--- /dev/null
+++ b/packages/go_router/test/inherited_go_router_test.dart
@@ -0,0 +1,105 @@
+// 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.
+
+import 'package:flutter/foundation.dart';
+import 'package:flutter/material.dart';
+import 'package:flutter_test/flutter_test.dart';
+import 'package:go_router/go_router.dart';
+import 'package:go_router/src/inherited_go_router.dart';
+
+void main() {
+ group('updateShouldNotify', () {
+ test('does not update when goRouter does not change', () {
+ final GoRouter goRouter = GoRouter(
+ routes: <GoRoute>[
+ GoRoute(
+ path: '/',
+ builder: (_, __) => const Page1(),
+ ),
+ ],
+ );
+ final bool shouldNotify = setupInheritedGoRouterChange(
+ oldGoRouter: goRouter,
+ newGoRouter: goRouter,
+ );
+ expect(shouldNotify, false);
+ });
+
+ test('updates when goRouter changes', () {
+ final GoRouter oldGoRouter = GoRouter(
+ routes: <GoRoute>[
+ GoRoute(
+ path: '/',
+ builder: (_, __) => const Page1(),
+ ),
+ ],
+ );
+ final GoRouter newGoRouter = GoRouter(
+ routes: <GoRoute>[
+ GoRoute(
+ path: '/',
+ builder: (_, __) => const Page2(),
+ ),
+ ],
+ );
+ final bool shouldNotify = setupInheritedGoRouterChange(
+ oldGoRouter: oldGoRouter,
+ newGoRouter: newGoRouter,
+ );
+ expect(shouldNotify, true);
+ });
+ });
+
+ test('adds [goRouter] as a diagnostics property', () {
+ final GoRouter goRouter = GoRouter(
+ routes: <GoRoute>[
+ GoRoute(
+ path: '/',
+ builder: (_, __) => const Page1(),
+ ),
+ ],
+ );
+ final InheritedGoRouter inheritedGoRouter = InheritedGoRouter(
+ goRouter: goRouter,
+ child: Container(),
+ );
+ final DiagnosticPropertiesBuilder properties =
+ DiagnosticPropertiesBuilder();
+ inheritedGoRouter.debugFillProperties(properties);
+ expect(properties.properties.length, 1);
+ expect(properties.properties.first, isA<DiagnosticsProperty<GoRouter>>());
+ expect(properties.properties.first.value, goRouter);
+ });
+}
+
+bool setupInheritedGoRouterChange({
+ required GoRouter oldGoRouter,
+ required GoRouter newGoRouter,
+}) {
+ final InheritedGoRouter oldInheritedGoRouter = InheritedGoRouter(
+ goRouter: oldGoRouter,
+ child: Container(),
+ );
+ final InheritedGoRouter newInheritedGoRouter = InheritedGoRouter(
+ goRouter: newGoRouter,
+ child: Container(),
+ );
+ return newInheritedGoRouter.updateShouldNotify(
+ oldInheritedGoRouter,
+ );
+}
+
+class Page1 extends StatelessWidget {
+ const Page1({Key? key}) : super(key: key);
+
+ @override
+ Widget build(BuildContext context) => Container();
+}
+
+class Page2 extends StatelessWidget {
+ const Page2({Key? key}) : super(key: key);
+
+ @override
+ Widget build(BuildContext context) => Container();
+}