Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 <path>`
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
Expand Down
33 changes: 29 additions & 4 deletions lib/src/dusk_integration.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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: <name>` for elements backed
/// by a [MagicFormData] text controller.
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -116,11 +123,28 @@ class MagicDuskIntegration {
(state) => _lastEchoState = _connectionStateName(state),
);
}

// 7. Register the navigate adapter so `ext.dusk.navigate --route <path>`
// 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);
Expand All @@ -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;
Expand Down
77 changes: 77 additions & 0 deletions test/dusk_integration_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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))
// ---------------------------------------------------------------------------
Expand Down