From 42940b9ecffd6037bf254d5523a0f389aea716bb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?An=C4=B1lcan=20=C3=87ak=C4=B1r?= Date: Thu, 25 Jun 2026 02:38:37 +0300 Subject: [PATCH] fix: register a dusk navigate adapter so ext.dusk.navigate drives GoRouter MagicDuskIntegration.install() registered enrichers but no navigate adapter, so dusk:navigate --route was a no-op on GoRouter apps (it fell back to an ignored SystemNavigator broadcast). Registers an adapter via DuskPlugin.registerNavigateAdapter that calls MagicRouter.instance.to(path), returning false on an uninitialised router (#8, #13). --- CHANGELOG.md | 11 +++++ lib/src/dusk_integration.dart | 33 ++++++++++++-- test/dusk_integration_test.dart | 77 +++++++++++++++++++++++++++++++++ 3 files changed, 117 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index df3662e..e4f86b0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,17 @@ All notable changes to this package are documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [Unreleased] + +### Added + +- `MagicDuskIntegration.install()` now registers a navigate adapter via + `DuskPlugin.registerNavigateAdapter` so `ext.dusk.navigate --route ` + drives GoRouter through `MagicRouter.instance.to(path)` instead of falling + back to the `SystemNavigator` platform broadcast. Returns `true` on success + and `false` when the router is not yet initialised (catches `StateError`). + `resetForTesting()` clears the adapter with `DuskPlugin.registerNavigateAdapter(null)`. + ## [0.0.1] - 2026-06-17 ### Added diff --git a/lib/src/dusk_integration.dart b/lib/src/dusk_integration.dart index 62420db..6dd8b09 100644 --- a/lib/src/dusk_integration.dart +++ b/lib/src/dusk_integration.dart @@ -18,7 +18,10 @@ import 'package:magic/magic.dart'; /// /// Adds fourteen enrichers to [DuskPlugin.enrichers] (in insertion order; /// later enrichers see the same Element, first-write-wins on overlapping -/// keys per oracle finding #3 contract): +/// keys per oracle finding #3 contract) and registers one navigate adapter +/// via [DuskPlugin.registerNavigateAdapter] so `ext.dusk.navigate --route` +/// drives GoRouter through [MagicRouter] instead of falling back to the +/// [SystemNavigator] broadcast: /// /// 1. [magicFormEnricher] — `magicFormField: ` for elements backed /// by a [MagicFormData] text controller. @@ -77,7 +80,11 @@ class MagicDuskIntegration { /// (echoConnection, gateResultsAll) land at slots 9 and 10, and the /// three Step-1.3 telescope-bridge enrichers (recentHttp, recentLogs, /// recentExceptions) land at slots 11, 12, 13 so any first-write-wins - /// overlap stays deterministic across versions. + /// overlap stays deterministic across versions. After enricher + /// registration, one navigate adapter is wired via + /// [DuskPlugin.registerNavigateAdapter] so `ext.dusk.navigate --route` + /// drives [MagicRouter.instance.to] (path-based) instead of falling + /// back to the [SystemNavigator] broadcast. static void install() { if (_installed) return; _installed = true; @@ -116,11 +123,28 @@ class MagicDuskIntegration { (state) => _lastEchoState = _connectionStateName(state), ); } + + // 7. Register the navigate adapter so `ext.dusk.navigate --route ` + // drives GoRouter through MagicRouter instead of falling back to the + // SystemNavigator broadcast (which GoRouter does not listen to). + // + // The adapter uses MagicRouter.instance.to(route) (path-based navigation, + // NOT .toNamed) because the dusk `--route` argument is a URL path. A + // null router instance (not yet initialised) throws StateError; we catch + // it and return false so dusk can fall back to the platform broadcast. + DuskPlugin.registerNavigateAdapter((String route) async { + try { + MagicRouter.instance.to(route); + return true; + } on StateError { + return false; + } + }); } /// Test-only reset. Drops all fourteen enrichers from - /// [DuskPlugin.enrichers], cancels the Echo connection-state - /// subscription, and clears the idempotency guard. + /// [DuskPlugin.enrichers], clears the navigate adapter, cancels the Echo + /// connection-state subscription, and clears the idempotency guard. @visibleForTesting static void resetForTesting() { DuskPlugin.enrichers.remove(magicFormEnricher); @@ -137,6 +161,7 @@ class MagicDuskIntegration { DuskPlugin.enrichers.remove(magicRecentHttpEnricher); DuskPlugin.enrichers.remove(magicRecentLogsEnricher); DuskPlugin.enrichers.remove(magicRecentExceptionsEnricher); + DuskPlugin.registerNavigateAdapter(null); _echoSubscription?.cancel(); _echoSubscription = null; _lastEchoState = null; diff --git a/test/dusk_integration_test.dart b/test/dusk_integration_test.dart index 10b330e..ff32a9a 100644 --- a/test/dusk_integration_test.dart +++ b/test/dusk_integration_test.dart @@ -1593,6 +1593,83 @@ void main() { }); }); + // --------------------------------------------------------------------------- + // navigate adapter (Step 5) + // --------------------------------------------------------------------------- + + group('MagicDuskIntegration navigate adapter', () { + test('install() registers a non-null navigate adapter', () { + expect(DuskPlugin.navigateAdapter, isNull); + + MagicDuskIntegration.install(); + + expect(DuskPlugin.navigateAdapter, isNotNull); + }); + + test('resetForTesting() clears the navigate adapter', () { + MagicDuskIntegration.install(); + expect(DuskPlugin.navigateAdapter, isNotNull); + + MagicDuskIntegration.resetForTesting(); + + expect(DuskPlugin.navigateAdapter, isNull); + }); + + testWidgets( + 'adapter returns true and navigates via MagicRouter.instance.to on a valid path', + (tester) async { + // 1. Wire up a real GoRouter-backed MagicRouter with two known routes. + MagicRoute.page('/', () => const SizedBox()); + MagicRoute.page('/dashboard', () => const SizedBox()); + + await tester.pumpWidget( + MaterialApp.router(routerConfig: MagicRouter.instance.routerConfig), + ); + await tester.pumpAndSettle(); + + // 2. Install the integration — this registers the adapter. + MagicDuskIntegration.install(); + + final adapter = DuskPlugin.navigateAdapter; + expect(adapter, isNotNull); + + // 3. Invoke the adapter with a valid path. + final result = await adapter!('/dashboard'); + + // 4. Pump so GoRouter processes the navigation. + await tester.pumpAndSettle(); + + // 5. Adapter must return true and the router must reflect the new route. + expect(result, isTrue); + expect(MagicRouter.instance.currentLocation, equals('/dashboard')); + }, + ); + + testWidgets( + 'adapter returns false (or throws) when MagicRouter is not initialised', + (tester) async { + // No routes registered, no MaterialApp.router pumped: _router is null. + MagicDuskIntegration.install(); + + final adapter = DuskPlugin.navigateAdapter; + expect(adapter, isNotNull); + + // The adapter must not crash the caller (dusk swallows throws), but it + // must NOT return true when the router is not ready. + bool? result; + try { + result = await adapter!('/anywhere'); + } on Object { + // Throw is acceptable per the DuskNavigateAdapter contract. + result = null; + } + + // Either null (threw) or false; never true. + expect(result, isNot(isTrue)); + }, + ); + }); + // --------------------------------------------------------------------------- // magicRecentExceptionsEnricher (Step 1.3 (c)) // ---------------------------------------------------------------------------