From a9d6bf14ba011c8389dc2e8db713ae245a7c7293 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Asc=C3=AAnio?= Date: Mon, 21 Mar 2022 14:15:23 -0300 Subject: [PATCH] [go_router] improve coverage (#977) --- packages/go_router/CHANGELOG.md | 4 +- packages/go_router/lib/src/go_route.dart | 25 +- .../lib/src/inherited_go_router.dart | 4 +- packages/go_router/pubspec.yaml | 2 +- .../test/custom_transition_page_test.dart | 114 ++++++ .../go_router/test/error_screen_helpers.dart | 66 ++++ packages/go_router/test/go_route_test.dart | 12 + .../test/go_router_cupertino_test.dart | 105 +++++ .../test/go_router_delegate_test.dart | 79 ++++ .../test/go_router_error_page_test.dart | 65 ++++ .../test/go_router_material_test.dart | 104 +++++ packages/go_router/test/go_router_test.dart | 358 +++++++++++++++++- .../test/inherited_go_router_test.dart | 105 +++++ 13 files changed, 1024 insertions(+), 19 deletions(-) create mode 100644 packages/go_router/test/custom_transition_page_test.dart create mode 100644 packages/go_router/test/error_screen_helpers.dart create mode 100644 packages/go_router/test/go_route_test.dart create mode 100644 packages/go_router/test/go_router_cupertino_test.dart create mode 100644 packages/go_router/test/go_router_delegate_test.dart create mode 100644 packages/go_router/test/go_router_error_page_test.dart create mode 100644 packages/go_router/test/go_router_material_test.dart create mode 100644 packages/go_router/test/inherited_go_router_test.dart diff --git a/packages/go_router/CHANGELOG.md b/packages/go_router/CHANGELOG.md index f60d9f05bbc2..2dd8a6bdd643 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 43c91239b345..dbe588290b2e 100644 --- a/packages/go_router/lib/src/go_route.dart +++ b/packages/go_router/lib/src/go_route.dart @@ -18,9 +18,9 @@ class GoRoute { required this.path, this.name, this.pageBuilder, - this.builder = _builder, + this.builder = _invalidBuilder, this.routes = const [], - this.redirect = _redirect, + this.redirect = _noRedirection, }) { if (path.isEmpty) { throw Exception('GoRoute path cannot be empty'); @@ -30,6 +30,15 @@ class GoRoute { 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 @@ class GoRoute { Map 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 e5c9d73b70bc..b746612116fe 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 @@ class InheritedGoRouter extends InheritedWidget { /// 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 b340fadf08a6..b333bfa8bdc1 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 000000000000..98ac8cd4929e --- /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 transition = CustomTransitionPage( + transitionsBuilder: expectAsync4((_, __, ___, Widget child) => child), + child: child, + ); + final GoRouter router = GoRouter( + routes: [ + 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 showHomeValueNotifier = + ValueNotifier(false); + await tester.pumpWidget( + MaterialApp( + home: ValueListenableBuilder( + valueListenable: showHomeValueNotifier, + builder: (_, bool showHome, __) { + return Navigator( + pages: >[ + const NoTransitionPage( + child: LoginScreen(), + ), + if (showHome) + const NoTransitionPage( + child: HomeScreen(), + ), + ], + onPopPage: (Route 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 000000000000..c8ae7403110f --- /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(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 000000000000..63b2912c08b2 --- /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 000000000000..594a865000e7 --- /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 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 createState() => _DummyStatefulWidgetState(); +} + +class _DummyStatefulWidgetState extends State { + @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 000000000000..d5778569dd66 --- /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(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 createState() => _DummyStatefulWidgetState(); +} + +class _DummyStatefulWidgetState extends State { + @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 000000000000..3e5b2fdc7b10 --- /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( + builder: (BuildContext _) => home, + ); + }, + color: Colors.white, + ); +} + +class DummyStatefulWidget extends StatefulWidget { + const DummyStatefulWidget({Key? key}) : super(key: key); + + @override + State createState() => _DummyStatefulWidgetState(); +} + +class _DummyStatefulWidgetState extends State { + @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 000000000000..8a0c595474fa --- /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 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 createState() => _DummyStatefulWidgetState(); +} + +class _DummyStatefulWidgetState extends State { + @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 25fd04f20292..718aa6e4b988 100644 --- a/packages/go_router/test/go_router_test.dart +++ b/packages/go_router/test/go_router_test.dart @@ -8,10 +8,12 @@ import 'dart:async'; 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 @@ void main() { group('stream', () { test('no stream emits', () async { // Act - final MockGoRouterRefreshStream notifyListener = - MockGoRouterRefreshStream( + final GoRouterRefreshStreamSpy notifyListener = + GoRouterRefreshStreamSpy( streamController.stream, ); @@ -1513,8 +1515,8 @@ void main() { final List toEmit = [1, 2, 3]; // Act - final MockGoRouterRefreshStream notifyListener = - MockGoRouterRefreshStream( + final GoRouterRefreshStreamSpy notifyListener = + GoRouterRefreshStreamSpy( streamController.stream, ); @@ -1528,10 +1530,340 @@ void main() { }); }); }); + + group('GoRouterHelper extensions', () { + final GlobalKey<_DummyStatefulWidgetState> key = + GlobalKey<_DummyStatefulWidgetState>(); + final List routes = [ + 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 params = { + 'a-param-key': 'a-param-value', + }; + final Map queryParams = { + '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(builder: (_) => const Text('Current route')), + MaterialPageRoute(builder: (_) => const Text('Previous route')), + ); + }); + + test('didPop notifies listeners', () { + createGoRouter() + ..addListener(expectAsync0(() {})) + ..didPop( + MaterialPageRoute(builder: (_) => const Text('Current route')), + MaterialPageRoute(builder: (_) => const Text('Previous route')), + ); + }); + + test('didRemove notifies listeners', () { + createGoRouter() + ..addListener(expectAsync0(() {})) + ..didRemove( + MaterialPageRoute(builder: (_) => const Text('Current route')), + MaterialPageRoute(builder: (_) => const Text('Previous route')), + ); + }); + + test('didReplace notifies listeners', () { + createGoRouter() + ..addListener(expectAsync0(() {})) + ..didReplace( + newRoute: MaterialPageRoute( + builder: (_) => const Text('Current route'), + ), + oldRoute: MaterialPageRoute( + builder: (_) => const Text('Previous route'), + ), + ); + }); + + test('uses navigatorBuilder when provided', () { + final Func3 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(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 routes}) + : super(routes: routes); + + String? name; + Map? params; + Map? queryParams; + + @override + String namedLocation( + String name, { + Map params = const {}, + Map queryParams = const {}, + }) { + this.name = name; + this.params = params; + this.queryParams = queryParams; + return ''; + } +} + +class GoRouterGoSpy extends GoRouter { + GoRouterGoSpy({required List 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 routes}) : super(routes: routes); + + String? name; + Map? params; + Map? queryParams; + Object? extra; + + @override + void goNamed( + String name, { + Map params = const {}, + Map queryParams = const {}, + Object? extra, + }) { + this.name = name; + this.params = params; + this.queryParams = queryParams; + this.extra = extra; + } +} + +class GoRouterPushSpy extends GoRouter { + GoRouterPushSpy({required List 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 routes}) : super(routes: routes); + + String? name; + Map? params; + Map? queryParams; + Object? extra; + + @override + void pushNamed( + String name, { + Map params = const {}, + Map queryParams = const {}, + Object? extra, + }) { + this.name = name; + this.params = params; + this.queryParams = queryParams; + this.extra = extra; + } +} + +class GoRouterPopSpy extends GoRouter { + GoRouterPopSpy({required List routes}) : super(routes: routes); + + bool popped = false; + + @override + void pop() { + popped = true; + } +} + +class GoRouterRefreshStreamSpy extends GoRouterRefreshStream { + GoRouterRefreshStreamSpy( Stream stream, ) : notifyCount = 0, super(stream); @@ -1703,3 +2035,15 @@ class DummyBuildContext implements BuildContext { @override Widget get widget => throw UnimplementedError(); } + +class DummyStatefulWidget extends StatefulWidget { + const DummyStatefulWidget({Key? key}) : super(key: key); + + @override + State createState() => _DummyStatefulWidgetState(); +} + +class _DummyStatefulWidgetState extends State { + @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 000000000000..cadf9553533f --- /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( + 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( + path: '/', + builder: (_, __) => const Page1(), + ), + ], + ); + final GoRouter newGoRouter = GoRouter( + routes: [ + 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( + 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>()); + 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(); +}