feat(dioxus): multi-window + deeplink (Phase 4-5)#277
Conversation
Greptile SummaryThis PR ships Phase 4 (multi-window label registry +
Confidence Score: 5/5Safe to merge — the three issues from the prior review are all addressed, the new Rust registry is correctly bounded, and the TS window/deeplink helpers are well-covered by unit tests. No functional defects found across the new Rust registry, the TS multi-window helpers, or the deeplink command. The label-to-handle resolution approach (OS focus after switchToWindow) is inherently correct for the msedgedriver external-provider path. The monotonic counter correctly decouples label allocation from the pruned entry count. Tests cover the failure and restore paths in sufficient depth. No files require special attention.
|
| Filename | Overview |
|---|---|
| packages/dioxus-bridge/src/window_state.rs | New process-global window registry with monotonic label counter, Weak liveness tracking, and GC pruning on each new registration; well-tested with 3 unit tests. |
| packages/dioxus-bridge/src/deeplink.rs | New optional convenience helper that wraps Config::with_custom_protocol for in-app deeplink handling; cleanly documented, pure library code not required for testing. |
| packages/dioxus-bridge/src/lib.rs | Adds window registry commands (__list_windows, __active_window, __window_states) and with_on_window hook; doc updated to reflect all shipped modules; stale forward-reference removed. |
| packages/dioxus-service/src/window.ts | New multi-window helpers: per-session label cache, listWindowLabels with item-type validation, and switchWindowByLabel with handle resolution and original-handle restore on failure. |
| packages/dioxus-service/src/commands/triggerDeeplink.ts | New Phase 5 command that validates the URL via @wdio/native-core, then delegates OS-native protocol dispatch to the same package; thin and well-tested. |
| packages/dioxus-service/src/service.ts | Wires switchWindow, listWindows, and triggerDeeplink onto browser.dioxus in before(); clears window label cache alongside mockStore in after(). |
| packages/native-types/src/dioxus.ts | DioxusServiceAPI interface updated with switchWindow, listWindows, and triggerDeeplink; stale comment replaced. |
| packages/dioxus-service/test/window.spec.ts | 14 new tests covering label cache, item-type validation, handle resolution, restore-on-failure, and session isolation; comprehensive coverage. |
| packages/dioxus-service/test/commands/triggerDeeplink.spec.ts | 8 new tests covering all three platform commands, URL protocol rejection, malformed URLs, and spawn failure propagation. |
| packages/dioxus-service/test/service.spec.ts | Extends existing service smoke test to assert switchWindow, listWindows, and triggerDeeplink are installed on browser.dioxus. |
Sequence Diagram
sequenceDiagram
participant Test as Test Script
participant Svc as DioxusWorkerService
participant Win as window.ts
participant WD as WebDriver
participant Bridge as wdio-dioxus-bridge
Note over Test,Bridge: switchWindowByLabel('window-1')
Test->>Svc: browser.dioxus.switchWindow('window-1')
Svc->>Win: switchWindowByLabel(browser, 'window-1')
Win->>WD: execute(__list_windows)
WD->>Bridge: invoke('__list_windows')
Bridge-->>WD: ['main','window-1']
WD-->>Win: ['main','window-1']
Win->>WD: getWindowHandle() → h-main (original)
loop for each handle
Win->>WD: switchToWindow(handle)
Win->>WD: execute(__active_window)
WD->>Bridge: invoke('__active_window')
Bridge-->>WD: label
WD-->>Win: label
alt "label === targetLabel"
Win-->>Win: return handle (driver already focused)
end
end
Win->>Win: setCurrentWindowLabel(browser, 'window-1')
Win-->>Test: void
Note over Test,Bridge: triggerDeeplink('myapp://open')
Test->>Svc: browser.dioxus.triggerDeeplink('myapp://open')
Svc->>Svc: validateDeeplinkUrl('myapp://open')
Svc->>Svc: getPlatformCommand(url, process.platform)
Svc->>Svc: executeDeeplinkCommand(cmd, args)
Note right of Svc: OS routes URL to registered protocol handler
Svc-->>Test: void
Reviews (2): Last reviewed commit: "fix(dioxus): address greptile P2s on PR ..." | Re-trigger Greptile
Bridge:
- New window_state.rs: process-global registry that auto-labels windows
in creation order (first is 'main', rest are 'window-N'). Stores
Weak<Window> so closed windows are filtered out at query time.
- install() now chains with_on_window to feed each created window into
the registry, and registers three invoke commands: __list_windows,
__active_window, __window_states.
- Docs note: install() must be the last builder before launch because
Config.on_window is an Option<Box<_>> with no getter — a later user
with_on_window would overwrite the bridge hook.
Service:
- New window.ts: per-sessionId label cache + getDefaultWindowLabel /
getCurrentWindowLabel / setCurrentWindowLabel / clearWindowState /
getActiveWindowLabel / listWindowLabels / switchWindowByLabel.
- service.before() installs switchWindow + listWindows on browser.dioxus.
- service.after() clears the window cache alongside mockStore.
- Surface restored on DioxusServiceAPI in native-types.
Unit tests cover label cache, list/active happy + fallback paths, switch
validation, handle resolution, and original-handle restore on failure.
E2E fixture work + window.spec.ts follow.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
… (Phase 5)
Service:
- New commands/triggerDeeplink.ts: validate URL via native-core then
spawn the platform-native protocol handler (rundll32 / open / gio).
Custom-protocol-only — http/https/file rejected.
- service.before() installs triggerDeeplink on browser.dioxus.
- DioxusServiceAPI gains triggerDeeplink in native-types.
Bridge:
- New deeplink.rs reference helper: register_handler(scheme, callback)
wraps Config::with_custom_protocol to give app authors a copy-paste
pattern. Optional — apps with existing protocol handlers can ignore.
Tests: 8 new unit tests covering the three platform branches, protocol
rejection (http/https/file), malformed URLs, and spawn failure propagation.
93/93 dioxus-service unit tests pass; 18/18 bridge Rust tests pass.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
a26612e to
e73cf22
Compare
Four review comments, all legitimate:
1. lib.rs crate doc — said "subsequent milestones add the deeplink
reference handler" but deeplink.rs ships in this PR. Doc rewritten
to list every module the bridge now exposes.
2. window_state.rs — dead Weak<Window> entries accumulated in the Vec
indefinitely. Split labelling state into:
- entries: Mutex<Vec<Entry>> (live + freshly-pruned)
- total_registered: Mutex<usize> (monotonic, drives labels)
register_window() now retains-on-upgrade before push, so the Vec
stays proportional to live windows. Labels remain stable across GC
because they depend on the counter, not Vec length.
3. window.ts switchWindowByLabel — was double-switching: resolveLabel
already calls switchToWindow on the matching handle, then we did
it again. Dropped the redundant call. Also dropped the outer
error-wrap: the inner resolver error already carried the
@wdio/dioxus-service prefix, producing a double-prefixed message
on the failure path. Test expectations updated.
4. window.ts listWindowLabels — only validated the response was an
Array, not that items were strings. A bridge bug returning [null]
or [42] would silently pass through. Added an every-string check
at the boundary with a discoverable error message.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Tauri's logParser.ts has the same whole-line regex matching bug as Dioxus's logParser — the level detector can match level words in the message body rather than the level token position. Electron is not affected (uses CDP events). PR #277 comments were all Dioxus-specific and addressed; no new cross-service entries needed from that PR. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix(dioxus-bridge): serialise embedded tests + remove unused reset_for_tests embedded.rs: the three async tests share a process-global OnceLock<Channel>. Without serialisation, should_return_none_when_queue_is_empty's drain-all loop races against should_round_trip_an_eval_request's push/poll_next pair, causing the round-trip test to see an empty queue and panic on unwrap(). Fix: introduce a static tokio::sync::Mutex::const_new(()) test lock that each test acquires before touching the channel. Each test also drains leftovers at entry so earlier failures don't contaminate subsequent ones. window_state.rs: reset_for_tests was pub(crate) but never called — the window_state tests only exercise allocate_label (a pure function) and cannot call register_window without a real Arc<Window>. Remove the dead helper rather than silencing the warning. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * feat(dioxus): macOS support + full E2E infrastructure (Phase 7) - Add darwin guard in launcher (macOS never supports 'external' provider) - Add `install_with_commands` API to embedded driver lib for fixture apps - Add `install_with_embedded_port_and_registry` to dioxus-bridge lib.rs - Add dioxus fixture app (fixtures/e2e-apps/dioxus/) with bridge commands for get_platform_info, get_command_line_args, generate_test_logs - Add WDIO E2E configs (wdio.dioxus.conf.ts, wdio.dioxus-embedded.conf.ts) - Add 15 E2E spec files covering api, application, execute, mocking, logging, window, deeplink, visual, multiremote, and standalone test scenarios - Extend envSchema.ts to accept FRAMEWORK=dioxus - Extend utils.ts getE2EAppDirName for dioxus - Add e2e package.json scripts for all dioxus test modes Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * fix(dioxus-e2e): resolve provider-aware log dir + multiremote port collision - logging.spec.ts: read DRIVER_PROVIDER env var to derive the log dir dynamically (mirrors tauri/logging.spec.ts pattern) so the spec works under both wdio.dioxus.conf.ts (external/official) and wdio.dioxus-embedded.conf.ts (embedded) without targeting the wrong dir - wdio.dioxus.conf.ts multiremote: remove port: 0 from both instances and add distinct dioxusDriverPort hints (9515 / 9517) so the launcher can allocate non-overlapping port pairs when external provider wiring lands; port: 0 would have been silently promoted to 4444 by WDIO detectBackend, causing both instances to share the same connection target Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * fix(dioxus-e2e): add dioxusDriverPort to local DioxusCapability type Excess-property check rejected dioxusDriverPort on wdio:dioxusServiceOptions because the locally-defined type omitted it. Adds the optional field so the multiremote per-instance port hints type-check correctly. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * fix(dioxus-e2e): set DRIVER_PROVIDER=embedded in embedded test scripts Without this, process.env.DRIVER_PROVIDER is undefined inside logging.spec.ts, so getLogDirName returns standard-dioxus while wdio.dioxus-embedded.conf.ts writes output to embedded-standard-dioxus — readWdioLogs targets a non-existent directory and both log assertions always fail. Mirrors the pattern used by all tauri-basic-embedded scripts. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * fix(dioxus-e2e): set DRIVER_PROVIDER from conf file, not npm scripts Each WDIO config already knows which provider it uses, so it sets process.env.DRIVER_PROVIDER directly. Specs can then read it without the caller needing to pass it externally. Reverts the cross-env DRIVER_PROVIDER additions from the previous commit. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * fix(dioxus): three E2E + test hygiene fixes embedded.rs: rename should_start_inactive_before_init → should_be_idempotent_on_repeated_init — the OnceLock<Channel> is permanent so pre-init state can never be observed; the test only verifies that calling init() twice does not panic. wdio.dioxus.conf.ts: set process.env.DRIVER_PROVIDER = 'official' at config load time, mirroring wdio.dioxus-embedded.conf.ts, so logging.spec.ts derives the correct log directory regardless of whether DRIVER_PROVIDER was set externally. wdio.dioxus.conf.ts multiremote: add concrete port: 9515 / port: 9517 on the outer InstanceConfig objects. WDIO uses these top-level fields to open WebDriver connections; without them WDIO defaults both to 4444 and the second instance fails. Values match the dioxusDriverPort hints already set in each capability. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * fix(e2e): add @wdio/dioxus-service workspace link to e2e dependencies Without this, pnpm strict linking means Node.js cannot resolve @wdio/dioxus-service when wdio.dioxus*.conf.ts loads, causing every dioxus E2E run to fail with "Cannot find module". Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * fix(dioxus-e2e): decouple DRIVER_PROVIDER service value from log-dir sentinel wdio.dioxus.conf.ts set DRIVER_PROVIDER='official' so that logging.spec.ts would resolve the correct prefix-less log directory, but that caused the service to receive 'official' instead of 'external', silently bypassing the external driver path in the launcher. Fix: set DRIVER_PROVIDER='external' (correct service value). In logging.spec.ts, stop forwarding the raw env value to getLogDirName — instead branch only on === 'embedded', since only the embedded provider uses a prefixed log directory. Any other value (external, unset) maps to the same prefix-less directory as 'official'. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * fix(dioxus): add session.ts for standalone mode; fix standalone spec remote() only invokes worker-level hooks, so the embedded WebDriver server was never started in standalone mode. Mirrors the Tauri pattern: - native-utils log.ts: add 'session' to LogArea union so session.ts can use a distinct logger namespace rather than borrowing 'service'. - packages/dioxus-service/src/session.ts: new module with init(), cleanup(), and createDioxusCapabilities(). init() manually calls launcher.onPrepare() to start the embedded driver and read back the assigned port before calling remote(), then calls service.before() to install browser.dioxus.*. - packages/dioxus-service/src/index.ts: export startWdioSession, cleanupWdioSession, createDioxusCapabilities from session.ts. - e2e/test/dioxus/standalone/api.spec.ts: replace the broken remote() + services[] call with startWdioSession/cleanupWdioSession/createDioxusCapabilities imported from @wdio/dioxus-service, matching the Tauri standalone spec pattern. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * chore: ignore local MCP config * fix(dioxus): address Greptile PR review findings - session.ts: call launcher.onComplete() on remote() failure to prevent subprocess leak when the embedded driver fails to connect - embedded.rs: convert idempotent-init test to #[tokio::test] and acquire TEST_LOCK so it serializes correctly with other async tests - wdio.dioxus-embedded.conf.ts: explicitly set maxInstances=1 in the standalone switch arm (was silently inheriting the default) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * fix(dioxus-e2e): use dedicated port 4447 in standalone spec Prevents an "address already in use" error if the spec is ever run inside the WDIO-managed test runner (where the launcher already occupies the default embedded port). Standalone specs are normally invoked via tsx directly, but the explicit port eliminates any ambiguity. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * fix(dioxus-e2e): standalone consistency nits in external conf and api spec - wdio.dioxus.conf.ts: explicitly set maxInstances=1 in the standalone switch arm (was silently inheriting the 5-instance default) - standalone/api.spec.ts: restructure from a raw top-level script to a proper Mocha describe/it spec with before/after session lifecycle hooks, removing the non-standard process.exit() call Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * fix(e2e): run Dioxus standalone spec via tsx, not wdio runner Standalone mode tests the session API called directly from user code — running it through wdio run + Mocha defeats that purpose because the runner's own service lifecycle wraps the test. Mirror the Tauri/Electron pattern: invoke the spec with tsx directly. - Rewrite api.spec.ts as a top-level-await script (no describe/it) - Change test:e2e:dioxus:standalone npm script to use tsx - Drop the standalone switch-case from both dioxus WDIO configs Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * fix(e2e): fix bare expression string and remove orphaned standalone logging spec - Add explicit 'return' to bare expression string in execute-advanced.spec.ts: WDIO wraps string scripts as a function body, so '1 + 2 + 3' returns undefined rather than 6 without an explicit return statement. - Delete e2e/test/dioxus/standalone/logging.spec.ts: uses @wdio/globals so it cannot run via tsx (standalone mode), and is not included in any WDIO config spec glob. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * fix(dioxus): use startTimeout for connectionRetryTimeout in standalone session statusPollTimeout is the per-request alive-check interval (default 2 s), not the overall startup budget. Using it produced connectionRetryTimeout = 8 s on slow CI, causing spurious connection failures. Add startTimeout to DioxusServiceOptions (mirrors TauriServiceOptions) and use it in session.ts with a 60 s default, matching the Tauri service behaviour. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * fix(e2e/dioxus): wrap standalone test body in try/finally to prevent process leak If an assertion throws after startWdioSession() succeeds the embedded binary stays running on port 4447, blocking subsequent invocations. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * fix(dioxus): store worker service and call service.after() on cleanup init() was discarding the DioxusWorkerService after service.before(), so cleanup() never called service.after(), leaving mockStore entries and window state alive across sessions. Add activeServices WeakMap parallel to activeLaunchers and drain it in cleanup(). Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * docs: add DEFERRED_ISSUES.md for post-dioxus follow-ups Tracks bugs found during Dioxus work that also affect Tauri and Electron but belong in separate PRs. First entry: service.after() never called in standalone sessions (tauri-service, electron-service). Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * fix(dioxus): clean up browser session and launcher if service.before() throws If remote() succeeds but service.before() subsequently throws, init() propagated the error without closing the open WebDriver session or stopping the embedded driver. Wrap service.before() in a try/catch that calls browser.deleteSession() and launcher.onComplete() before rethrowing. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * docs: add two more deferred issues for tauri-service and electron-service - remote() failure not cleaning up launcher (Tauri catch block is incomplete, Electron has no catch at all) - service.before() throwing after remote() succeeds leaks browser session and launcher in both services Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * docs: add log-level detector deferred issue from PR #276 audit Tauri's logParser.ts has the same whole-line regex matching bug as Dioxus's logParser — the level detector can match level words in the message body rather than the level token position. Electron is not affected (uses CDP events). PR #277 comments were all Dioxus-specific and addressed; no new cross-service entries needed from that PR. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * fix(e2e/dioxus): guard deleteSession() so cleanupWdioSession always runs If the app crashed or the WebDriver connection timed out, deleteSession() would reject and cleanupWdioSession() would never be called, leaving the embedded binary running. Swallow the deleteSession error so cleanup always proceeds. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * fix(e2e/dioxus): add Linux platform guard to external-provider config On Linux the launcher throws SevereServiceError rather than exiting cleanly, causing CI to see a hard failure instead of a skip. Exit 78 (same as the darwin guard) until the upstream Dioxus Config::with_allow_automation PR lands. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * fix(e2e): add 'external' to DRIVER_PROVIDER Zod enum in envSchema.ts wdio.dioxus.conf.ts sets DRIVER_PROVIDER='external' but the schema only accepted 'official'|'crabnebula'|'embedded', causing Zod to reject the value in any code path that calls validateEnvironment() in the same process. Also update the driverProvider getter return type to match. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * fix(dioxus-service): guard launcher.onComplete() if service.after() throws in cleanup If service.after() rejects (IPC teardown error, mock-store flush failure, etc.) the await propagates and launcher.onComplete() is never reached, leaving the embedded binary running indefinitely. Wrap in try/finally so the launcher is always stopped. Also updated DEFERRED_ISSUES.md to include the same guard pattern for when tauri-service / electron-service get their service.after() fix. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * fix(dioxus-service): clean up launcher when port check fails in init() If onPrepare() starts the embedded driver but capabilities.port is absent, the previous code threw without calling launcher.onComplete(), leaving the subprocess running. Call onComplete() (with .catch so it can't mask the original error) before rethrowing, consistent with the remote() and service.before() error paths. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> --------- Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Summary
PR3 first slice — ships Phase 4 (multi-window) and Phase 5 (deeplink) of the Dioxus service rollout. Stacks on
feat/dioxus-service.Phase 4 — Multi-window
Bridge (
packages/dioxus-bridge/)window_state.rs— process-global registry that auto-labels Dioxus windows in creation order: first ismain, rest arewindow-1,window-2, .... StoresWeak<Window>so closed windows are filtered at query time. Labels are never reused.install()chainsConfig::with_on_windowto feed each new window into the registry and registers three invoke commands:__list_windows,__active_window,__window_states.install()must be the last builder before launch (Dioxus'sConfig.on_windowis anOption<Box<_>>with no getter — a later userwith_on_windowwould silently shadow the bridge hook).Service (
packages/dioxus-service/)window.ts— per-sessionIdlabel cache +getDefaultWindowLabel/getCurrentWindowLabel/setCurrentWindowLabel/clearWindowState/getActiveWindowLabel/listWindowLabels/switchWindowByLabel. The switch path validates against the bridge's live list, resolves label → WebDriver handle by walkinggetWindowHandles(), and restores the original handle on failure.service.before()installsswitchWindow+listWindowsonbrowser.dioxus.service.after()clears the window cache alongsidemockStore.Phase 5 — Deeplink
Service
commands/triggerDeeplink.ts— validate URL via@wdio/native-core, spawn the platform-native protocol handler:rundll32.exe url.dll,FileProtocolHandler <url>open <url>gio open <url>http:///https:///file://.Bridge
deeplink.rs— optional reference helperregister_handler(scheme, callback)that wrapsConfig::with_custom_protocol(scheme, ...). Pure convenience for app authors; not required for testing.Types
DioxusServiceAPIrestored:switchWindow,listWindows,triggerDeeplink.Test plan
cargo test— 18/18 (3 new inwindow_state)pnpm test:unit— 93/93 (8 new fortriggerDeeplink, 14 new forwindow.ts)pnpm typecheckgreenfeat/dioxus-feature-completeDeferred
fixtures/e2e-apps/dioxus/doesn't exist yet; PR2 shipped without it. Window + deeplink E2E specs land in a dedicated PR that bootstraps the fixture app.wdio-dioxus-embedded-drivercrate + platform executors +providers/embedded.ts+ Vite dev-server branch.🤖 Generated with Claude Code