diff --git a/packages/zenrouter/CHANGELOG.md b/packages/zenrouter/CHANGELOG.md index ca508bd..b0912eb 100644 --- a/packages/zenrouter/CHANGELOG.md +++ b/packages/zenrouter/CHANGELOG.md @@ -1,3 +1,7 @@ +## 2.0.1 +- **Fix**: Revert `hasEmptyPath` back to `pathSegments.isEmpty` in `resolveInitialUri` for correct path empty checks. +- **Refactor**: Remove redundant `initialRouteInformation` parameter from `CoordinatorRouteInformationProvider` since fallback defaults and resolution logic is robust now. + ## 2.0.0 🎉 **Major Release - Core Architecture & Layouts** diff --git a/packages/zenrouter/lib/src/coordinator/base.dart b/packages/zenrouter/lib/src/coordinator/base.dart index a943ae7..5148b85 100644 --- a/packages/zenrouter/lib/src/coordinator/base.dart +++ b/packages/zenrouter/lib/src/coordinator/base.dart @@ -231,14 +231,12 @@ abstract class Coordinator extends CoordinatorCore /// The [RouteInformationProvider] that is used to configure the [Router]. /// /// ## Relationship - /// Supplies the initial URI from [initialRoutePath] or defaults to `/`. + /// Supplies the initial URI, preferring any platform-provided route + /// (e.g. from [PlatformDispatcher.defaultRouteName]), then falling back to + /// [initialRoutePath] if set, and finally defaulting to `/`. @override late final RouteInformationProvider routeInformationProvider = - PlatformRouteInformationProvider( - initialRouteInformation: RouteInformation( - uri: initialRoutePath ?? Uri.parse('/'), - ), - ); + CoordinatorRouteInformationProvider(coordinator: this); /// Access to the navigator state. /// diff --git a/packages/zenrouter/lib/src/coordinator/route_information_provider.dart b/packages/zenrouter/lib/src/coordinator/route_information_provider.dart new file mode 100644 index 0000000..d94bc1f --- /dev/null +++ b/packages/zenrouter/lib/src/coordinator/route_information_provider.dart @@ -0,0 +1,50 @@ +import 'package:flutter/widgets.dart'; +import 'package:zenrouter/zenrouter.dart'; + +/// A [RouteInformationProvider] that derives its initial route from both the +/// platform and a [Coordinator]. +/// +/// The initial [Uri] is resolved by first parsing the platform dispatcher’s +/// [defaultRouteName]. If that route has no path segments (is effectively +/// empty) and [Coordinator.initialRoutePath] is non-null, the coordinator’s +/// [initialRoutePath] is used instead. When an initial URI is chosen but has +/// an empty path, it is normalized to `/`, and if no usable platform route +/// can be parsed the URI defaults to `/`. +class CoordinatorRouteInformationProvider + extends PlatformRouteInformationProvider { + CoordinatorRouteInformationProvider({required Coordinator coordinator}) + : _coordinator = coordinator, + super( + initialRouteInformation: RouteInformation( + uri: resolveInitialUri( + WidgetsBinding.instance.platformDispatcher.defaultRouteName, + coordinator.initialRoutePath, + ), + ), + ); + + final Coordinator _coordinator; + + Coordinator get coordinator => _coordinator; + + @visibleForTesting + static Uri resolveInitialUri(String? platformRouteName, Uri? initialUri) { + final defaultUri = Uri.tryParse(platformRouteName ?? ''); + + // If the platform route name can't be parsed, fall back to the provided + // initialUri when available; otherwise, use the root route. + if (defaultUri == null) { + return initialUri ?? Uri.parse('/'); + } + + if (defaultUri.pathSegments.isEmpty && initialUri != null) { + return initialUri; + } + + if (defaultUri.hasEmptyPath) { + return defaultUri.replace(path: '/'); + } + + return defaultUri; + } +} diff --git a/packages/zenrouter/lib/zenrouter.dart b/packages/zenrouter/lib/zenrouter.dart index 2b2db3b..0374291 100644 --- a/packages/zenrouter/lib/zenrouter.dart +++ b/packages/zenrouter/lib/zenrouter.dart @@ -8,6 +8,7 @@ export 'src/coordinator/observer.dart'; export 'src/coordinator/router.dart'; export 'src/coordinator/transition.dart'; export 'src/coordinator/restoration/_public.dart'; +export 'src/coordinator/route_information_provider.dart'; /// Path base export 'src/path/layout.dart'; diff --git a/packages/zenrouter/pubspec.yaml b/packages/zenrouter/pubspec.yaml index adb083a..e3cd1b9 100644 --- a/packages/zenrouter/pubspec.yaml +++ b/packages/zenrouter/pubspec.yaml @@ -2,7 +2,7 @@ name: zenrouter description: >- A powerful Flutter router with deep linking, web support, type-safe routing, guards, redirects, and zero boilerplate. -version: 2.0.0 +version: 2.0.1 repository: https://github.com/definev/zenrouter homepage: https://github.com/definev/zenrouter/tree/main/packages/zenrouter diff --git a/packages/zenrouter/test/coordinator/restoration_test.dart b/packages/zenrouter/test/coordinator/restoration_test.dart index a14c0c3..052d042 100644 --- a/packages/zenrouter/test/coordinator/restoration_test.dart +++ b/packages/zenrouter/test/coordinator/restoration_test.dart @@ -855,9 +855,8 @@ void main() { final coordinator = TestCoordinator( initialRoutePath: Uri.parse('/settings'), ); - final config = coordinator; - await tester.pumpWidget(MaterialApp.router(routerConfig: config)); + await tester.pumpWidget(MaterialApp.router(routerConfig: coordinator)); await tester.pumpAndSettle(); diff --git a/packages/zenrouter/test/coordinator/route_information_provider_test.dart b/packages/zenrouter/test/coordinator/route_information_provider_test.dart new file mode 100644 index 0000000..9fe2fe6 --- /dev/null +++ b/packages/zenrouter/test/coordinator/route_information_provider_test.dart @@ -0,0 +1,171 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:zenrouter/zenrouter.dart'; + +abstract class AppRoute extends RouteTarget with RouteUnique { + @override + Uri toUri(); +} + +class HomeRoute extends AppRoute { + @override + Uri toUri() => Uri.parse('/'); + + @override + Widget build(covariant TestCoordinator coordinator, BuildContext context) { + return const Scaffold(body: Text('Home')); + } + + @override + List get props => []; +} + +class TestCoordinator extends Coordinator { + TestCoordinator({super.initialRoutePath}); + + @override + AppRoute parseRouteFromUri(Uri uri) { + return HomeRoute(); + } +} + +void main() { + TestWidgetsFlutterBinding.ensureInitialized(); + + group('CoordinatorRouteInformationProvider', () { + test('creates with coordinator and default initial route', () { + final coordinator = TestCoordinator(); + final provider = CoordinatorRouteInformationProvider( + coordinator: coordinator, + ); + + expect(provider.coordinator, equals(coordinator)); + expect(provider.value.uri.toString(), equals('/')); + }); + + test('reports new route information', () { + final coordinator = TestCoordinator(); + final provider = CoordinatorRouteInformationProvider( + coordinator: coordinator, + ); + + provider.routerReportsNewRouteInformation( + RouteInformation(uri: Uri.parse('/new-route')), + ); + + expect(provider.value.uri.toString(), equals('/new-route')); + }); + + test('inherits from PlatformRouteInformationProvider', () { + final coordinator = TestCoordinator(); + final provider = CoordinatorRouteInformationProvider( + coordinator: coordinator, + ); + + expect(provider, isA()); + expect(provider, isA()); + }); + }); + + group('resolveInitialUri', () { + test('returns "/" when platformRouteName and initialUri are null', () { + final result = CoordinatorRouteInformationProvider.resolveInitialUri( + null, + null, + ); + expect(result.toString(), equals('/')); + }); + + test( + 'returns "/" when platformRouteName is empty and initialUri is null', + () { + final result = CoordinatorRouteInformationProvider.resolveInitialUri( + '', + null, + ); + expect(result.toString(), equals('/')); + }, + ); + + test( + 'returns initialUri when platformRouteName is empty and initialUri is provided', + () { + final initialUri = Uri.parse('/initial'); + final result = CoordinatorRouteInformationProvider.resolveInitialUri( + '', + initialUri, + ); + expect(result, equals(initialUri)); + }, + ); + + test( + 'returns defaultUri with "/" path when platformRouteName has empty path and initialUri is null', + () { + final result = CoordinatorRouteInformationProvider.resolveInitialUri( + 'https://example.com', + null, + ); + expect(result.toString(), equals('https://example.com/')); + }, + ); + + test( + 'returns initialUri when platformRouteName has empty path and initialUri is provided', + () { + final initialUri = Uri.parse('/custom'); + final result = CoordinatorRouteInformationProvider.resolveInitialUri( + 'https://example.com', + initialUri, + ); + expect(result, equals(initialUri)); + }, + ); + + test( + 'returns platformRouteName when it has non-empty path and initialUri is null', + () { + final result = CoordinatorRouteInformationProvider.resolveInitialUri( + '/platform', + null, + ); + expect(result.toString(), equals('/platform')); + }, + ); + + test( + 'returns platformRouteName when it has non-empty path and initialUri is provided', + () { + final initialUri = Uri.parse('/initial'); + final result = CoordinatorRouteInformationProvider.resolveInitialUri( + '/platform', + initialUri, + ); + expect(result.toString(), equals('/platform')); + }, + ); + + test( + 'returns "/" when platformRouteName is invalid and initialUri is null', + () { + final result = CoordinatorRouteInformationProvider.resolveInitialUri( + '::invalid::', + null, + ); + expect(result.toString(), equals('/')); + }, + ); + + test( + 'returns "/" when platformRouteName is invalid and initialUri is provided', + () { + final initialUri = Uri.parse('/initial'); + final result = CoordinatorRouteInformationProvider.resolveInitialUri( + '::invalid::', + initialUri, + ); + expect(result.toString(), equals('/initial')); + }, + ); + }); +}