diff --git a/packages/go_router/CHANGELOG.md b/packages/go_router/CHANGELOG.md index 19a6cdf7061..2c80a43d6e1 100644 --- a/packages/go_router/CHANGELOG.md +++ b/packages/go_router/CHANGELOG.md @@ -1,3 +1,10 @@ +## 15.0.0 + +- **BREAKING CHANGE** + - URLs are now case sensitive. + - Adds `caseSensitive` parameter to `GoRouter` (default to `true`). + - See [Migrating to 15.0.0](https://flutter.dev/go/go-router-v15-breaking-changes) + ## 14.8.1 - Secured canPop method for the lack of matches in routerDelegate's configuration. diff --git a/packages/go_router/README.md b/packages/go_router/README.md index 12da5815576..8f54d8e10e1 100644 --- a/packages/go_router/README.md +++ b/packages/go_router/README.md @@ -37,6 +37,7 @@ See the API documentation for details on the following topics: - [Error handling](https://pub.dev/documentation/go_router/latest/topics/Error%20handling-topic.html) ## Migration Guides +- [Migrating to 15.0.0](https://flutter.dev/go/go-router-v15-breaking-changes). - [Migrating to 14.0.0](https://flutter.dev/go/go-router-v14-breaking-changes). - [Migrating to 13.0.0](https://flutter.dev/go/go-router-v13-breaking-changes). - [Migrating to 12.0.0](https://flutter.dev/go/go-router-v12-breaking-changes). diff --git a/packages/go_router/lib/src/match.dart b/packages/go_router/lib/src/match.dart index 696f9d4c50d..051c8b74960 100644 --- a/packages/go_router/lib/src/match.dart +++ b/packages/go_router/lib/src/match.dart @@ -218,7 +218,17 @@ abstract class RouteMatchBase with Diagnosticable { final String newMatchedLocation = concatenatePaths(matchedLocation, pathLoc); final String newMatchedPath = concatenatePaths(matchedPath, route.path); - if (newMatchedLocation.toLowerCase() == uri.path.toLowerCase()) { + + final String newMatchedLocationToCompare; + final String uriPathToCompare; + if (route.caseSensitive) { + newMatchedLocationToCompare = newMatchedLocation; + uriPathToCompare = uri.path; + } else { + newMatchedLocationToCompare = newMatchedLocation.toLowerCase(); + uriPathToCompare = uri.path.toLowerCase(); + } + if (newMatchedLocationToCompare == uriPathToCompare) { // A complete match. pathParameters.addAll(currentPathParameter); @@ -232,7 +242,7 @@ abstract class RouteMatchBase with Diagnosticable { ], }; } - assert(uri.path.startsWith(newMatchedLocation)); + assert(uriPathToCompare.startsWith(newMatchedLocationToCompare)); assert(remainingLocation.isNotEmpty); final String childRestLoc = uri.path.substring( diff --git a/packages/go_router/lib/src/route.dart b/packages/go_router/lib/src/route.dart index 703e470e9a6..d16e1dfd658 100644 --- a/packages/go_router/lib/src/route.dart +++ b/packages/go_router/lib/src/route.dart @@ -275,6 +275,7 @@ class GoRoute extends RouteBase { super.parentNavigatorKey, super.redirect, this.onExit, + this.caseSensitive = true, super.routes = const [], }) : assert(path.isNotEmpty, 'GoRoute path cannot be empty'), assert(name == null || name.isNotEmpty, 'GoRoute name cannot be empty'), @@ -437,6 +438,17 @@ class GoRoute extends RouteBase { /// ``` final ExitCallback? onExit; + /// Determines whether the route matching is case sensitive. + /// + /// When `true`, the path must match the specified case. For example, + /// a [GoRoute] with `path: '/family/:fid'` will not match `/FaMiLy/f2`. + /// + /// When `false`, the path matching is case insensitive. The route + /// with `path: '/family/:fid'` will match `/FaMiLy/f2`. + /// + /// Defaults to `true`. + final bool caseSensitive; + // TODO(chunhtai): move all regex related help methods to path_utils.dart. /// Match this route against a location. RegExpMatch? matchPatternAsPrefix(String loc) { diff --git a/packages/go_router/pubspec.yaml b/packages/go_router/pubspec.yaml index 86b0e9643fa..90bdc10f5e9 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: 14.8.1 +version: 15.0.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/go_router_test.dart b/packages/go_router/test/go_router_test.dart index d98f10711c5..80e170b7924 100644 --- a/packages/go_router/test/go_router_test.dart +++ b/packages/go_router/test/go_router_test.dart @@ -755,6 +755,7 @@ void main() { ), GoRoute( path: '/family/:fid', + caseSensitive: false, builder: (BuildContext context, GoRouterState state) => FamilyScreen(state.pathParameters['fid']!), ), @@ -780,6 +781,56 @@ void main() { expect(find.byType(FamilyScreen), findsOneWidget); }); + testWidgets('match path case sensitively', (WidgetTester tester) async { + final FlutterExceptionHandler? oldFlutterError = FlutterError.onError; + addTearDown(() => FlutterError.onError = oldFlutterError); + final List errors = []; + FlutterError.onError = (FlutterErrorDetails details) { + errors.add(details); + }; + final List routes = [ + GoRoute( + path: '/', + builder: (BuildContext context, GoRouterState state) => + const HomeScreen(), + ), + GoRoute( + path: '/family/:fid', + builder: (BuildContext context, GoRouterState state) => + FamilyScreen(state.pathParameters['fid']!), + ), + ]; + + final GoRouter router = await createRouter(routes, tester); + const String wrongLoc = '/FaMiLy/f2'; + + expect(errors, isEmpty); + router.go(wrongLoc); + await tester.pumpAndSettle(); + + expect(errors, hasLength(1)); + expect( + errors.single.exception, + isAssertionError, + reason: 'The path is case sensitive', + ); + + const String loc = '/family/f2'; + router.go(loc); + await tester.pumpAndSettle(); + final List matches = + router.routerDelegate.currentConfiguration.matches; + + expect( + router.routerDelegate.currentConfiguration.uri.toString(), + loc, + ); + + expect(matches, hasLength(1)); + expect(find.byType(FamilyScreen), findsOneWidget); + expect(errors, hasLength(1), reason: 'No new errors should be thrown'); + }); + testWidgets( 'If there is more than one route to match, use the first match.', (WidgetTester tester) async {