[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();
+}