diff --git a/packages/go_router/CHANGELOG.md b/packages/go_router/CHANGELOG.md index b530a9a0a69..3dc0e2fff33 100644 --- a/packages/go_router/CHANGELOG.md +++ b/packages/go_router/CHANGELOG.md @@ -1,3 +1,7 @@ +## 8.2.0 + +- Adds onException to GoRouter constructor. + ## 8.1.0 - Adds parent navigator key to ShellRoute and StatefulShellRoute. @@ -8,7 +12,7 @@ ## 8.0.4 -- Updates documentations around `GoRouter.of`, `GoRouter.maybeOf`, and `BuildContext` extension. +- Updates documentations around `GoRouter.of`, `GoRouter.maybeOf`, and `BuildContext` extension. ## 8.0.3 diff --git a/packages/go_router/doc/error-handling.md b/packages/go_router/doc/error-handling.md index 10211894e77..0dd1de89f4c 100644 --- a/packages/go_router/doc/error-handling.md +++ b/packages/go_router/doc/error-handling.md @@ -1,12 +1,34 @@ -By default, go_router comes with default error screens for both `MaterialApp` -and `CupertinoApp` as well as a default error screen in the case that none is -used. You can also replace the default error screen by using the -[errorBuilder](https://pub.dev/documentation/go_router/latest/go_router/GoRouter/GoRouter.html) -parameter: +There are several kinds of errors or exceptions in go_router. + +* GoError and AssertionError + +This kind of errors are thrown when go_router is used incorrectly, for example, if the root +[GoRoute.path](https://pub.dev/documentation/go_router/latest/go_router/GoRoute/path.html) does +not start with `/` or a builder in GoRoute is not provided. These errors should not be caught and +must be fixed in code in order to use go_router. + +* GoException + +This kind of exception are thrown when the configuration of go_router cannot handle incoming requests +from users or other part of the code. For example, an GoException is thrown when user enter url that +can't be parsed according to pattern specified in the `GoRouter.routes`. These exceptions can be +handled in various callbacks. +Once can provide a callback to `GoRouter.onException` to handle this exception. In this callback, +one can choose to ignore, redirect, or push different pages depending on the situation. +See [Exception Handling](https://github.com/flutter/packages/blob/main/packages/go_router/example/lib/exception_handling.dart) +on a runnable example. + +The `GoRouter.errorBuilder` and `GoRouter.errorPageBuilder` can also be used to handle exceptions. ```dart GoRouter( /* ... */ errorBuilder: (context, state) => ErrorScreen(state.error), ); ``` + +By default, go_router comes with default error screens for both `MaterialApp` +and `CupertinoApp` as well as a default error screen in the case that none is +used. + +**Note** the `GoRouter.onException` supersedes other exception handling APIs. \ No newline at end of file diff --git a/packages/go_router/example/README.md b/packages/go_router/example/README.md index f4c1c0f48d3..55f75bca6a6 100644 --- a/packages/go_router/example/README.md +++ b/packages/go_router/example/README.md @@ -36,6 +36,11 @@ An example to demonstrate how to use handle a sign-in flow with a stream authent An example to demonstrate how to use a `StatefulShellRoute` to create stateful nested navigation, with a `BottomNavigationBar`. +## [Exception Handling](https://github.com/flutter/packages/blob/main/packages/go_router/example/lib/exception_handling.dart) +`flutter run lib/exception_handling.dart` + +An example to demonstrate how to handle exception in go_router. + ## [Books app](https://github.com/flutter/packages/blob/main/packages/go_router/example/lib/books) `flutter run lib/books/main.dart` diff --git a/packages/go_router/example/lib/exception_handling.dart b/packages/go_router/example/lib/exception_handling.dart new file mode 100644 index 00000000000..583e35e0bf7 --- /dev/null +++ b/packages/go_router/example/lib/exception_handling.dart @@ -0,0 +1,87 @@ +// 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:go_router/go_router.dart'; + +/// This sample app shows how to use `GoRouter.onException` to redirect on +/// exception. +/// +/// The first route '/' is mapped to [HomeScreen], and the second route +/// '/404' is mapped to [NotFoundScreen]. +/// +/// Any other unknown route or exception is redirected to `/404`. +void main() => runApp(const MyApp()); + +/// The route configuration. +final GoRouter _router = GoRouter( + onException: (_, GoRouterState state, GoRouter router) { + router.go('/404', extra: state.location); + }, + routes: [ + GoRoute( + path: '/', + builder: (BuildContext context, GoRouterState state) { + return const HomeScreen(); + }, + ), + GoRoute( + path: '/404', + builder: (BuildContext context, GoRouterState state) { + return NotFoundScreen(uri: state.extra as String? ?? ''); + }, + ), + ], +); + +/// The main app. +class MyApp extends StatelessWidget { + /// Constructs a [MyApp] + const MyApp({super.key}); + + @override + Widget build(BuildContext context) { + return MaterialApp.router( + routerConfig: _router, + ); + } +} + +/// The home screen +class HomeScreen extends StatelessWidget { + /// Constructs a [HomeScreen] + const HomeScreen({super.key}); + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar(title: const Text('Home Screen')), + body: Center( + child: ElevatedButton( + onPressed: () => context.go('/some-unknown-route'), + child: const Text('Simulates user entering unknown url'), + ), + ), + ); + } +} + +/// The not found screen +class NotFoundScreen extends StatelessWidget { + /// Constructs a [HomeScreen] + const NotFoundScreen({super.key, required this.uri}); + + /// The uri that can not be found. + final String uri; + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar(title: const Text('Page Not Found')), + body: Center( + child: Text("Can't find a page for: $uri"), + ), + ); + } +} diff --git a/packages/go_router/example/lib/main.dart b/packages/go_router/example/lib/main.dart index e4524711b62..d1596485034 100644 --- a/packages/go_router/example/lib/main.dart +++ b/packages/go_router/example/lib/main.dart @@ -58,14 +58,9 @@ class HomeScreen extends StatelessWidget { return Scaffold( appBar: AppBar(title: const Text('Home Screen')), body: Center( - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - ElevatedButton( - onPressed: () => context.go('/details'), - child: const Text('Go to the Details screen'), - ), - ], + child: ElevatedButton( + onPressed: () => context.go('/details'), + child: const Text('Go to the Details screen'), ), ), ); @@ -82,14 +77,9 @@ class DetailsScreen extends StatelessWidget { return Scaffold( appBar: AppBar(title: const Text('Details Screen')), body: Center( - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - ElevatedButton( - onPressed: () => context.go('/'), - child: const Text('Go back to the Home screen'), - ), - ], + child: ElevatedButton( + onPressed: () => context.go('/'), + child: const Text('Go back to the Home screen'), ), ), ); diff --git a/packages/go_router/example/lib/redirection.dart b/packages/go_router/example/lib/redirection.dart index 868944c0b63..aded4b759b4 100644 --- a/packages/go_router/example/lib/redirection.dart +++ b/packages/go_router/example/lib/redirection.dart @@ -104,20 +104,15 @@ class LoginScreen extends StatelessWidget { Widget build(BuildContext context) => Scaffold( appBar: AppBar(title: const Text(App.title)), body: Center( - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - ElevatedButton( - onPressed: () { - // log a user in, letting all the listeners know - context.read().login('test-user'); - - // router will automatically redirect from /login to / using - // refreshListenable - }, - child: const Text('Login'), - ), - ], + child: ElevatedButton( + onPressed: () { + // log a user in, letting all the listeners know + context.read().login('test-user'); + + // router will automatically redirect from /login to / using + // refreshListenable + }, + child: const Text('Login'), ), ), ); diff --git a/packages/go_router/example/test/exception_handling_test.dart b/packages/go_router/example/test/exception_handling_test.dart new file mode 100644 index 00000000000..68a731032c7 --- /dev/null +++ b/packages/go_router/example/test/exception_handling_test.dart @@ -0,0 +1,18 @@ +// 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_examples/exception_handling.dart' as example; + +void main() { + testWidgets('example works', (WidgetTester tester) async { + await tester.pumpWidget(const example.MyApp()); + expect(find.text('Simulates user entering unknown url'), findsOneWidget); + + await tester.tap(find.text('Simulates user entering unknown url')); + await tester.pumpAndSettle(); + expect(find.text("Can't find a page for: /some-unknown-route"), + findsOneWidget); + }); +} diff --git a/packages/go_router/lib/src/configuration.dart b/packages/go_router/lib/src/configuration.dart index 865007d7659..a1969d65794 100644 --- a/packages/go_router/lib/src/configuration.dart +++ b/packages/go_router/lib/src/configuration.dart @@ -184,16 +184,33 @@ class RouteConfiguration { } /// The match used when there is an error during parsing. - static RouteMatchList _errorRouteMatchList(Uri uri, String errorMessage) { - final Exception error = Exception(errorMessage); + static RouteMatchList _errorRouteMatchList(Uri uri, GoException exception) { return RouteMatchList( matches: const [], - error: error, + error: exception, uri: uri, pathParameters: const {}, ); } + /// Builds a [GoRouterState] suitable for top level callback such as + /// `GoRouter.redirect` or `GoRouter.onException`. + GoRouterState buildTopLevelGoRouterState(RouteMatchList matchList) { + return GoRouterState( + this, + location: matchList.uri.toString(), + // No name available at the top level trim the query params off the + // sub-location to match route.redirect + fullPath: matchList.fullPath, + pathParameters: matchList.pathParameters, + matchedLocation: matchList.uri.path, + queryParameters: matchList.uri.queryParameters, + queryParametersAll: matchList.uri.queryParametersAll, + extra: matchList.extra, + pageKey: const ValueKey('topLevel'), + ); + } + /// The list of top level routes used by [GoRouterDelegate]. final List routes; @@ -257,7 +274,8 @@ class RouteConfiguration { final List? matches = _getLocRouteMatches(uri, pathParameters); if (matches == null) { - return _errorRouteMatchList(uri, 'no routes for location: $uri'); + return _errorRouteMatchList( + uri, GoException('no routes for location: $uri')); } return RouteMatchList( matches: matches, @@ -411,19 +429,7 @@ class RouteConfiguration { // Check for top-level redirect final FutureOr topRedirectResult = topRedirect( context, - GoRouterState( - this, - location: prevLocation, - // No name available at the top level trim the query params off the - // sub-location to match route.redirect - fullPath: prevMatchList.fullPath, - pathParameters: prevMatchList.pathParameters, - matchedLocation: prevMatchList.uri.path, - queryParameters: prevMatchList.uri.queryParameters, - queryParametersAll: prevMatchList.uri.queryParametersAll, - extra: prevMatchList.extra, - pageKey: const ValueKey('topLevel'), - ), + buildTopLevelGoRouterState(prevMatchList), ); if (topRedirectResult is String?) { @@ -485,9 +491,9 @@ class RouteConfiguration { final RouteMatchList newMatch = findMatch(newLocation); _addRedirect(redirectHistory, newMatch, previousLocation); return newMatch; - } on RedirectionError catch (e) { - log.info('Redirection error: ${e.message}'); - return _errorRouteMatchList(e.location, e.message); + } on GoException catch (e) { + log.info('Redirection exception: ${e.message}'); + return _errorRouteMatchList(previousLocation, e); } } @@ -500,12 +506,18 @@ class RouteConfiguration { Uri prevLocation, ) { if (redirects.contains(newMatch)) { - throw RedirectionError('redirect loop detected', - [...redirects, newMatch], prevLocation); + throw GoException( + 'redirect loop detected ${_formatRedirectionHistory([ + ...redirects, + newMatch + ])}'); } if (redirects.length > redirectLimit) { - throw RedirectionError('too many redirects', - [...redirects, newMatch], prevLocation); + throw GoException( + 'too many redirects ${_formatRedirectionHistory([ + ...redirects, + newMatch + ])}'); } redirects.add(newMatch); @@ -513,6 +525,13 @@ class RouteConfiguration { log.info('redirecting to $newMatch'); } + String _formatRedirectionHistory(List redirections) { + return redirections + .map( + (RouteMatchList routeMatches) => routeMatches.uri.toString()) + .join(' => '); + } + /// Get the location for the provided route. /// /// Builds the absolute path for the route, by concatenating the paths of the diff --git a/packages/go_router/lib/src/delegate.dart b/packages/go_router/lib/src/delegate.dart index 37986714a3c..1d382db2d9a 100644 --- a/packages/go_router/lib/src/delegate.dart +++ b/packages/go_router/lib/src/delegate.dart @@ -7,6 +7,7 @@ import 'dart:async'; import 'package:flutter/foundation.dart'; import 'package:flutter/widgets.dart'; +import '../go_router.dart'; import 'builder.dart'; import 'configuration.dart'; import 'match.dart'; @@ -25,6 +26,7 @@ class GoRouterDelegate extends RouterDelegate required GoRouterWidgetBuilder? errorBuilder, required List observers, required this.routerNeglect, + required this.onException, String? restorationScopeId, }) : _configuration = configuration { builder = RouteBuilder( @@ -45,6 +47,13 @@ class GoRouterDelegate extends RouterDelegate /// Set to true to disable creating history entries on the web. final bool routerNeglect; + /// The exception handler that is called when parser can't handle the incoming + /// uri. + /// + /// If this is null, the exception is handled in the + /// [RouteBuilder.errorPageBuilder] or [RouteBuilder.errorBuilder]. + final GoExceptionHandler? onException; + final RouteConfiguration _configuration; _NavigatorStateIterator _createNavigatorStateIterator() => @@ -131,9 +140,11 @@ class GoRouterDelegate extends RouterDelegate /// For use by the Router architecture as part of the RouterDelegate. @override Future setNewRoutePath(RouteMatchList configuration) { - currentConfiguration = configuration; + if (currentConfiguration != configuration) { + currentConfiguration = configuration; + notifyListeners(); + } assert(currentConfiguration.isNotEmpty || currentConfiguration.isError); - notifyListeners(); // Use [SynchronousFuture] so that the initial url is processed // synchronously and remove unwanted initial animations on deep-linking return SynchronousFuture(null); diff --git a/packages/go_router/lib/src/match.dart b/packages/go_router/lib/src/match.dart index e04833132e7..865ab0b4108 100644 --- a/packages/go_router/lib/src/match.dart +++ b/packages/go_router/lib/src/match.dart @@ -10,6 +10,7 @@ import 'package:flutter/foundation.dart'; import 'package:flutter/widgets.dart'; import 'configuration.dart'; +import 'misc/errors.dart'; import 'path_utils.dart'; /// An matched result by matching a [RouteBase] against a location. @@ -183,7 +184,7 @@ class RouteMatchList { final Object? extra; /// An exception if there was an error during matching. - final Exception? error; + final GoException? error; /// the full path pattern that matches the uri. /// diff --git a/packages/go_router/lib/src/misc/errors.dart b/packages/go_router/lib/src/misc/errors.dart index 045e2ea04d9..7f163780f77 100644 --- a/packages/go_router/lib/src/misc/errors.dart +++ b/packages/go_router/lib/src/misc/errors.dart @@ -2,8 +2,6 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -import '../match.dart'; - /// Thrown when [GoRouter] is used incorrectly. class GoError extends Error { /// Constructs a [GoError] @@ -16,23 +14,14 @@ class GoError extends Error { String toString() => 'GoError: $message'; } -/// A configuration error detected while processing redirects. -class RedirectionError extends Error implements UnsupportedError { - /// RedirectionError constructor. - RedirectionError(this.message, this.matches, this.location); - - /// The matches that were found while processing redirects. - final List matches; +/// Thrown when [GoRouter] can not handle a user request. +class GoException implements Exception { + /// Creates an exception with message describing the reason. + GoException(this.message); - @override + /// The reason that causes this exception. final String message; - /// The location that was originally navigated to, before redirection began. - final Uri location; - @override - String toString() => '${super.toString()} ${[ - ...matches - .map((RouteMatchList routeMatches) => routeMatches.uri.toString()), - ].join(' => ')}'; + String toString() => 'GoException: $message'; } diff --git a/packages/go_router/lib/src/parser.dart b/packages/go_router/lib/src/parser.dart index 17902413cfb..1a9dccae36d 100644 --- a/packages/go_router/lib/src/parser.dart +++ b/packages/go_router/lib/src/parser.dart @@ -14,17 +14,36 @@ import 'information_provider.dart'; import 'logging.dart'; import 'match.dart'; +/// The function signature of [GoRouteInformationParser.onParserException]. +/// +/// The `routeMatchList` parameter contains the exception explains the issue +/// occurred. +/// +/// The returned [RouteMatchList] is used as parsed result for the +/// [GoRouterDelegate]. +typedef ParserExceptionHandler = RouteMatchList Function( + BuildContext context, + RouteMatchList routeMatchList, +); + /// Converts between incoming URLs and a [RouteMatchList] using [RouteMatcher]. /// Also performs redirection using [RouteRedirector]. class GoRouteInformationParser extends RouteInformationParser { /// Creates a [GoRouteInformationParser]. GoRouteInformationParser({ required this.configuration, + required this.onParserException, }) : _routeMatchListCodec = RouteMatchListCodec(configuration); /// The route configuration used for parsing [RouteInformation]s. final RouteConfiguration configuration; + /// The exception handler that is called when parser can't handle the incoming + /// uri. + /// + /// This method must return a [RouteMatchList] for the parsed result. + final ParserExceptionHandler? onParserException; + final RouteMatchListCodec _routeMatchListCodec; final Random _random = Random(); @@ -50,7 +69,13 @@ class GoRouteInformationParser extends RouteInformationParser { // the state. final RouteMatchList matchList = _routeMatchListCodec.decode(state as Map); - return debugParserFuture = _redirect(context, matchList); + return debugParserFuture = _redirect(context, matchList) + .then((RouteMatchList value) { + if (value.isError && onParserException != null) { + return onParserException!(context, value); + } + return value; + }); } late final RouteMatchList initialMatches; @@ -70,6 +95,9 @@ class GoRouteInformationParser extends RouteInformationParser { context, initialMatches, ).then((RouteMatchList matchList) { + if (matchList.isError && onParserException != null) { + return onParserException!(context, matchList); + } return _updateRouteMatchList( matchList, baseRouteMatchList: state.baseRouteMatchList, diff --git a/packages/go_router/lib/src/router.dart b/packages/go_router/lib/src/router.dart index 15ceea63440..745fc3a0145 100644 --- a/packages/go_router/lib/src/router.dart +++ b/packages/go_router/lib/src/router.dart @@ -14,6 +14,15 @@ import 'misc/inherited_router.dart'; import 'parser.dart'; import 'typedefs.dart'; +/// The function signature of [GoRouter.onException]. +/// +/// Use `state.error` to access the exception. +typedef GoExceptionHandler = void Function( + BuildContext context, + GoRouterState state, + GoRouter router, +); + /// The route configuration for the app. /// /// The `routes` list specifies the top-level routes for the app. It must not be @@ -30,6 +39,15 @@ import 'typedefs.dart'; /// implemented), a re-evaluation will be triggered when the [InheritedWidget] /// changes. /// +/// To handle exceptions, use one of `onException`, `errorBuilder`, or +/// `errorPageBuilder`. The `onException` is called when an exception is thrown. +/// If `onException` is not provided, the exception is passed to +/// `errorPageBuilder` to build a page for the Router if it is not null; +/// otherwise, it is passed to `errorBuilder` instead. If none of them are +/// provided, go_router builds a default error screen to show the exception. +/// See [Error handling](https://pub.dev/documentation/go_router/latest/topics/error-handling.html) +/// for more details. +/// /// See also: /// * [Configuration](https://pub.dev/documentation/go_router/latest/topics/Configuration-topic.html) /// * [GoRoute], which provides APIs to define the routing table. @@ -51,8 +69,7 @@ class GoRouter extends ChangeNotifier implements RouterConfig { /// The `routes` must not be null and must contain an [GoRouter] to match `/`. GoRouter({ required List routes, - // TODO(johnpryan): Change to a route, improve error API - // See https://github.com/flutter/flutter/issues/108144 + GoExceptionHandler? onException, GoRouterPageBuilder? errorPageBuilder, GoRouterWidgetBuilder? errorBuilder, GoRouterRedirect? redirect, @@ -70,6 +87,12 @@ class GoRouter extends ChangeNotifier implements RouterConfig { initialExtra == null || initialLocation != null, 'initialLocation must be set in order to use initialExtra', ), + assert( + (onException == null ? 0 : 1) + + (errorPageBuilder == null ? 0 : 1) + + (errorBuilder == null ? 0 : 1) < + 2, + 'Only one of onException, errorPageBuilder, or errorBuilder can be provided.'), assert(_debugCheckPath(routes, true)), assert( _debugVerifyNoDuplicatePathParameter(routes, {})), @@ -90,7 +113,21 @@ class GoRouter extends ChangeNotifier implements RouterConfig { navigatorKey: navigatorKey, ); + final ParserExceptionHandler? parserExceptionHandler; + if (onException != null) { + parserExceptionHandler = + (BuildContext context, RouteMatchList routeMatchList) { + onException(context, + configuration.buildTopLevelGoRouterState(routeMatchList), this); + // Avoid updating GoRouterDelegate if onException is provided. + return routerDelegate.currentConfiguration; + }; + } else { + parserExceptionHandler = null; + } + routeInformationParser = GoRouteInformationParser( + onParserException: parserExceptionHandler, configuration: configuration, ); @@ -102,6 +139,7 @@ class GoRouter extends ChangeNotifier implements RouterConfig { routerDelegate = GoRouterDelegate( configuration: configuration, + onException: onException, errorPageBuilder: errorPageBuilder, errorBuilder: errorBuilder, routerNeglect: routerNeglect, diff --git a/packages/go_router/lib/src/state.dart b/packages/go_router/lib/src/state.dart index 13ee9add91e..89400e36d10 100644 --- a/packages/go_router/lib/src/state.dart +++ b/packages/go_router/lib/src/state.dart @@ -72,8 +72,8 @@ class GoRouterState { /// An extra object to pass along with the navigation. final Object? extra; - /// The error associated with this match. - final Exception? error; + /// The error associated with this sub-route. + final GoException? error; /// A unique string key for this sub-route. /// E.g. diff --git a/packages/go_router/pubspec.yaml b/packages/go_router/pubspec.yaml index ded370a7c29..77370874a60 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: 8.1.0 +version: 8.2.0 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/exception_handling_test.dart b/packages/go_router/test/exception_handling_test.dart new file mode 100644 index 00000000000..5afd1039dfd --- /dev/null +++ b/packages/go_router/test/exception_handling_test.dart @@ -0,0 +1,97 @@ +// 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 'test_helpers.dart'; + +void main() { + testWidgets('throws if more than one exception handlers are provided.', + (WidgetTester tester) async { + bool thrown = false; + try { + GoRouter( + routes: [ + GoRoute( + path: '/', + builder: (_, GoRouterState state) => const Text('home')), + ], + errorBuilder: (_, __) => const Text(''), + onException: (_, __, ___) {}, + ); + } on Error { + thrown = true; + } + expect(thrown, true); + + thrown = false; + try { + GoRouter( + routes: [ + GoRoute( + path: '/', + builder: (_, GoRouterState state) => const Text('home')), + ], + errorBuilder: (_, __) => const Text(''), + errorPageBuilder: (_, __) => const MaterialPage(child: Text('')), + ); + } on Error { + thrown = true; + } + expect(thrown, true); + + thrown = false; + try { + GoRouter( + routes: [ + GoRoute( + path: '/', + builder: (_, GoRouterState state) => const Text('home')), + ], + onException: (_, __, ___) {}, + errorPageBuilder: (_, __) => const MaterialPage(child: Text('')), + ); + } on Error { + thrown = true; + } + expect(thrown, true); + }); + + group('onException', () { + testWidgets('can redirect.', (WidgetTester tester) async { + final GoRouter router = await createRouter([ + GoRoute( + path: '/error', + builder: (_, GoRouterState state) => + Text('redirected ${state.extra}')), + ], tester, + onException: (_, GoRouterState state, GoRouter router) => + router.go('/error', extra: state.location)); + expect(find.text('redirected /'), findsOneWidget); + + router.go('/some-other-location'); + await tester.pumpAndSettle(); + expect(find.text('redirected /some-other-location'), findsOneWidget); + }); + + testWidgets('stays on the same page if noop.', (WidgetTester tester) async { + final GoRouter router = await createRouter( + [ + GoRoute( + path: '/', + builder: (_, GoRouterState state) => const Text('home')), + ], + tester, + onException: (_, __, ___) {}, + ); + expect(find.text('home'), findsOneWidget); + + router.go('/some-other-location'); + await tester.pumpAndSettle(); + expect(find.text('home'), findsOneWidget); + }); + }); +} diff --git a/packages/go_router/test/go_router_test.dart b/packages/go_router/test/go_router_test.dart index 2ebb4afca77..1d4b059f03a 100644 --- a/packages/go_router/test/go_router_test.dart +++ b/packages/go_router/test/go_router_test.dart @@ -127,7 +127,12 @@ void main() { GoRoute(path: '/', builder: dummy), ]; - final GoRouter router = await createRouter(routes, tester); + final GoRouter router = await createRouter( + routes, + tester, + errorBuilder: (BuildContext context, GoRouterState state) => + TestErrorScreen(state.error!), + ); router.go('/foo'); await tester.pumpAndSettle(); final List matches = @@ -1889,14 +1894,18 @@ void main() { }); testWidgets('top-level redirect loop', (WidgetTester tester) async { - final GoRouter router = await createRouter([], tester, - redirect: (BuildContext context, GoRouterState state) => - state.matchedLocation == '/' - ? '/login' - : state.matchedLocation == '/login' - ? '/' - : null); - + final GoRouter router = await createRouter( + [], + tester, + redirect: (BuildContext context, GoRouterState state) => + state.matchedLocation == '/' + ? '/login' + : state.matchedLocation == '/login' + ? '/' + : null, + errorBuilder: (BuildContext context, GoRouterState state) => + TestErrorScreen(state.error!), + ); final List matches = router.routerDelegate.currentConfiguration.matches; expect(matches, hasLength(0)); @@ -1921,6 +1930,8 @@ void main() { ), ], tester, + errorBuilder: (BuildContext context, GoRouterState state) => + TestErrorScreen(state.error!), ); final List matches = @@ -1944,6 +1955,8 @@ void main() { tester, redirect: (BuildContext context, GoRouterState state) => state.matchedLocation == '/' ? '/login' : null, + errorBuilder: (BuildContext context, GoRouterState state) => + TestErrorScreen(state.error!), ); final List matches = @@ -1966,6 +1979,8 @@ void main() { : state.matchedLocation == '/login' ? '/' : null, + errorBuilder: (BuildContext context, GoRouterState state) => + TestErrorScreen(state.error!), ); final List matches = @@ -2153,6 +2168,8 @@ void main() { tester, redirect: (BuildContext context, GoRouterState state) => '/${state.location}+', + errorBuilder: (BuildContext context, GoRouterState state) => + TestErrorScreen(state.error!), redirectLimit: 10, ); diff --git a/packages/go_router/test/parser_test.dart b/packages/go_router/test/parser_test.dart index 253790f0116..ad2573f6141 100644 --- a/packages/go_router/test/parser_test.dart +++ b/packages/go_router/test/parser_test.dart @@ -198,7 +198,7 @@ void main() { expect(matchesObj.uri.toString(), '/def'); expect(matchesObj.extra, isNull); expect(matchesObj.error!.toString(), - 'Exception: no routes for location: /def'); + 'GoException: no routes for location: /def'); }); testWidgets( diff --git a/packages/go_router/test/test_helpers.dart b/packages/go_router/test/test_helpers.dart index 53838382bf0..229452cad63 100644 --- a/packages/go_router/test/test_helpers.dart +++ b/packages/go_router/test/test_helpers.dart @@ -149,16 +149,16 @@ Future createRouter( GlobalKey? navigatorKey, GoRouterWidgetBuilder? errorBuilder, String? restorationScopeId, + GoExceptionHandler? onException, }) async { final GoRouter goRouter = GoRouter( routes: routes, redirect: redirect, initialLocation: initialLocation, + onException: onException, initialExtra: initialExtra, redirectLimit: redirectLimit, - errorBuilder: errorBuilder ?? - (BuildContext context, GoRouterState state) => - TestErrorScreen(state.error!), + errorBuilder: errorBuilder, navigatorKey: navigatorKey, restorationScopeId: restorationScopeId, );