diff --git a/packages/go_router/CHANGELOG.md b/packages/go_router/CHANGELOG.md index 6518a538ed0..ebc65a4c0cd 100644 --- a/packages/go_router/CHANGELOG.md +++ b/packages/go_router/CHANGELOG.md @@ -1,3 +1,7 @@ +## 15.2.4 + +- Fixes routing to treat URLs with different cases (e.g., `/Home` vs `/home`) as distinct routes. + ## 15.2.3 - Updates Type-safe routes topic documentation to use the mixin from `go_router_builder` 3.0.0. diff --git a/packages/go_router/lib/src/configuration.dart b/packages/go_router/lib/src/configuration.dart index 95fd1002913..a730500b6b0 100644 --- a/packages/go_router/lib/src/configuration.dart +++ b/packages/go_router/lib/src/configuration.dart @@ -20,6 +20,8 @@ import 'state.dart'; typedef GoRouterRedirect = FutureOr Function( BuildContext context, GoRouterState state); +typedef _NamedPath = ({String path, bool caseSensitive}); + /// The route configuration for GoRouter configured by the app. class RouteConfiguration { /// Constructs a [RouteConfiguration]. @@ -246,7 +248,7 @@ class RouteConfiguration { /// example. final Codec? extraCodec; - final Map _nameToPath = {}; + final Map _nameToPath = {}; /// Looks up the url location by a [GoRoute]'s name. String namedLocation( @@ -264,11 +266,11 @@ class RouteConfiguration { return true; }()); assert(_nameToPath.containsKey(name), 'unknown route name: $name'); - final String path = _nameToPath[name]!; + final _NamedPath path = _nameToPath[name]!; assert(() { // Check that all required params are present final List paramNames = []; - patternToRegExp(path, paramNames); + patternToRegExp(path.path, paramNames, caseSensitive: path.caseSensitive); for (final String paramName in paramNames) { assert(pathParameters.containsKey(paramName), 'missing param "$paramName" for $path'); @@ -284,7 +286,10 @@ class RouteConfiguration { for (final MapEntry param in pathParameters.entries) param.key: Uri.encodeComponent(param.value) }; - final String location = patternToPath(path, encodedParams); + final String location = patternToPath( + path.path, + encodedParams, + ); return Uri( path: location, queryParameters: queryParameters.isEmpty ? null : queryParameters, @@ -528,8 +533,9 @@ class RouteConfiguration { if (_nameToPath.isNotEmpty) { sb.writeln('known full paths for route names:'); - for (final MapEntry e in _nameToPath.entries) { - sb.writeln(' ${e.key} => ${e.value}'); + for (final MapEntry e in _nameToPath.entries) { + sb.writeln( + ' ${e.key} => ${e.value.path}${e.value.caseSensitive ? '' : ' (case-insensitive)'}'); } } @@ -594,8 +600,9 @@ class RouteConfiguration { assert( !_nameToPath.containsKey(name), 'duplication fullpaths for name ' - '"$name":${_nameToPath[name]}, $fullPath'); - _nameToPath[name] = fullPath; + '"$name":${_nameToPath[name]!.path}, $fullPath'); + _nameToPath[name] = + (path: fullPath, caseSensitive: route.caseSensitive); } if (route.routes.isNotEmpty) { diff --git a/packages/go_router/lib/src/match.dart b/packages/go_router/lib/src/match.dart index bb4499dfaf3..53020786fe5 100644 --- a/packages/go_router/lib/src/match.dart +++ b/packages/go_router/lib/src/match.dart @@ -660,9 +660,20 @@ class RouteMatchList with Diagnosticable { matches: newMatches, ); } + + if (newMatches.isEmpty) { + return RouteMatchList.empty; + } + + RouteBase newRoute = newMatches.last.route; + while (newRoute is ShellRouteBase) { + newRoute = newRoute.routes.last; + } + newRoute as GoRoute; // Need to remove path parameters that are no longer in the fullPath. final List newParameters = []; - patternToRegExp(fullPath, newParameters); + patternToRegExp(fullPath, newParameters, + caseSensitive: newRoute.caseSensitive); final Set validParameters = newParameters.toSet(); final Map newPathParameters = Map.fromEntries( diff --git a/packages/go_router/lib/src/path_utils.dart b/packages/go_router/lib/src/path_utils.dart index 8bcb62a3c38..53c56b22fa9 100644 --- a/packages/go_router/lib/src/path_utils.dart +++ b/packages/go_router/lib/src/path_utils.dart @@ -23,7 +23,8 @@ final RegExp _parameterRegExp = RegExp(r':(\w+)(\((?:\\.|[^\\()])+\))?'); /// To extract the path parameter values from a [RegExpMatch], pass the /// [RegExpMatch] into [extractPathParameters] with the `parameters` that are /// used for generating the [RegExp]. -RegExp patternToRegExp(String pattern, List parameters) { +RegExp patternToRegExp(String pattern, List parameters, + {required bool caseSensitive}) { final StringBuffer buffer = StringBuffer('^'); int start = 0; for (final RegExpMatch match in _parameterRegExp.allMatches(pattern)) { @@ -47,7 +48,7 @@ RegExp patternToRegExp(String pattern, List parameters) { if (!pattern.endsWith('/')) { buffer.write(r'(?=/|$)'); } - return RegExp(buffer.toString(), caseSensitive: false); + return RegExp(buffer.toString(), caseSensitive: caseSensitive); } String _escapeGroup(String group, [String? name]) { diff --git a/packages/go_router/lib/src/route.dart b/packages/go_router/lib/src/route.dart index 542fbdd77b6..032ba621567 100644 --- a/packages/go_router/lib/src/route.dart +++ b/packages/go_router/lib/src/route.dart @@ -285,7 +285,8 @@ class GoRoute extends RouteBase { 'if onExit is provided, one of pageBuilder or builder must be provided'), super._() { // cache the path regexp and parameters - _pathRE = patternToRegExp(path, pathParameters); + _pathRE = + patternToRegExp(path, pathParameters, caseSensitive: caseSensitive); } /// Whether this [GoRoute] only redirects to another route. @@ -1193,7 +1194,8 @@ class StatefulNavigationShell extends StatefulWidget { /// find the first GoRoute, from which a full path will be derived. final GoRoute route = branch.defaultRoute!; final List parameters = []; - patternToRegExp(route.path, parameters); + patternToRegExp(route.path, parameters, + caseSensitive: route.caseSensitive); assert(parameters.isEmpty); final String fullPath = _router.configuration.locationForRoute(route)!; return patternToPath( diff --git a/packages/go_router/pubspec.yaml b/packages/go_router/pubspec.yaml index c5d35eec684..6be302ef6ba 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: 15.2.3 +version: 15.2.4 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 80e170b7924..5d6b142c313 100644 --- a/packages/go_router/test/go_router_test.dart +++ b/packages/go_router/test/go_router_test.dart @@ -13,6 +13,7 @@ import 'package:flutter/services.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:go_router/go_router.dart'; import 'package:go_router/src/match.dart'; +import 'package:go_router/src/pages/material.dart'; import 'package:logging/logging.dart'; import 'test_helpers.dart'; @@ -782,12 +783,6 @@ void main() { }); 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: '/', @@ -804,16 +799,11 @@ void main() { 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', - ); + expect(find.byType(MaterialErrorScreen), findsOne); + expect(find.text('Page Not Found'), findsOne); const String loc = '/family/f2'; router.go(loc); @@ -827,8 +817,42 @@ void main() { ); expect(matches, hasLength(1)); - expect(find.byType(FamilyScreen), findsOneWidget); - expect(errors, hasLength(1), reason: 'No new errors should be thrown'); + expect(find.byType(FamilyScreen), findsOne); + }); + + testWidgets('supports routes with a different case', + (WidgetTester tester) async { + final List routes = [ + GoRoute( + path: '/', + builder: (BuildContext context, GoRouterState state) => + const HomeScreen(), + ), + GoRoute( + path: '/abc', + builder: (BuildContext context, GoRouterState state) => + const SizedBox(key: Key('abc')), + ), + GoRoute( + path: '/ABC', + builder: (BuildContext context, GoRouterState state) => + const SizedBox(key: Key('ABC')), + ), + ]; + + final GoRouter router = await createRouter(routes, tester); + const String loc1 = '/abc'; + + router.go(loc1); + await tester.pumpAndSettle(); + + expect(find.byKey(const Key('abc')), findsOne); + + const String loc = '/ABC'; + router.go(loc); + await tester.pumpAndSettle(); + + expect(find.byKey(const Key('ABC')), findsOne); }); testWidgets( diff --git a/packages/go_router/test/path_utils_test.dart b/packages/go_router/test/path_utils_test.dart index b13599c74a5..e2d79a73571 100644 --- a/packages/go_router/test/path_utils_test.dart +++ b/packages/go_router/test/path_utils_test.dart @@ -9,7 +9,8 @@ void main() { test('patternToRegExp without path parameter', () async { const String pattern = '/settings/detail'; final List pathParameter = []; - final RegExp regex = patternToRegExp(pattern, pathParameter); + final RegExp regex = + patternToRegExp(pattern, pathParameter, caseSensitive: true); expect(pathParameter.isEmpty, isTrue); expect(regex.hasMatch('/settings/detail'), isTrue); expect(regex.hasMatch('/settings/'), isFalse); @@ -22,7 +23,8 @@ void main() { test('patternToRegExp with path parameter', () async { const String pattern = '/user/:id/book/:bookId'; final List pathParameter = []; - final RegExp regex = patternToRegExp(pattern, pathParameter); + final RegExp regex = + patternToRegExp(pattern, pathParameter, caseSensitive: true); expect(pathParameter.length, 2); expect(pathParameter[0], 'id'); expect(pathParameter[1], 'bookId'); @@ -44,7 +46,8 @@ void main() { test('patternToPath without path parameter', () async { const String pattern = '/settings/detail'; final List pathParameter = []; - final RegExp regex = patternToRegExp(pattern, pathParameter); + final RegExp regex = + patternToRegExp(pattern, pathParameter, caseSensitive: true); const String url = '/settings/detail'; final RegExpMatch? match = regex.firstMatch(url); @@ -60,7 +63,8 @@ void main() { test('patternToPath with path parameter', () async { const String pattern = '/user/:id/book/:bookId'; final List pathParameter = []; - final RegExp regex = patternToRegExp(pattern, pathParameter); + final RegExp regex = + patternToRegExp(pattern, pathParameter, caseSensitive: true); const String url = '/user/123/book/456'; final RegExpMatch? match = regex.firstMatch(url); diff --git a/packages/go_router/test/route_data_test.dart b/packages/go_router/test/route_data_test.dart index 83889adfbf4..218caef690d 100644 --- a/packages/go_router/test/route_data_test.dart +++ b/packages/go_router/test/route_data_test.dart @@ -50,7 +50,7 @@ class _ShellRouteDataWithKey extends ShellRouteData { GoRouterState state, Widget navigator, ) => - SizedBox( + KeyedSubtree( key: key, child: navigator, );