Skip to content

fix(dioxus): preserve undefined in execute() via __wdio_value__ envelope#411

Merged
goosewobbler merged 6 commits into
mainfrom
fix/dioxus-execute-undefined-complete
Jun 18, 2026
Merged

fix(dioxus): preserve undefined in execute() via __wdio_value__ envelope#411
goosewobbler merged 6 commits into
mainfrom
fix/dioxus-execute-undefined-complete

Conversation

@goosewobbler

Copy link
Copy Markdown
Contributor

Summary

  • browser.dioxus.execute(...) returning undefined previously delivered null to the test; null and undefined were indistinguishable
  • Root cause: the embedded polling loop coerced result ?? null, collapsing both to null
  • Fix: mirror @wdio/tauri-service's __wdio_value__ envelope — wrap the result before posting through __embedded_result; unwrap in execute.ts and runInterceptorScript
  • Because JSON.stringify omits undefined object properties, { __wdio_value__: undefined } serialises to {} (key absent) while { __wdio_value__: null } serialises to {"__wdio_value__":null} (key present), making the two distinguishable on receipt
  • Rebuild and commit dist-js/index.js (now tracked per fix(dioxus-bridge): commit guest-js bundle for git/crates.io consumers #407)

Closes #409

Test plan

  • pnpm --filter @wdio/dioxus-service exec vitest run test/commands/execute.spec.ts — 12 tests pass
  • pnpm --filter @wdio/dioxus-bridge test — 10 tests pass
  • CI green
  • Manual: after resetAllMocks(), execute(() => invoke('cmd')) returns undefined, not null

🤖 Generated with Claude Code

goosewobbler and others added 2 commits June 17, 2026 17:42
The embedded polling loop coerced `result ?? null`, collapsing undefined
and null to null. Mirror @wdio/tauri-service's envelope pattern:

- guest-js: emit `{ __wdio_value__: result }` instead of `result ?? null`;
  JSON.stringify omits undefined properties so undefined→{} (key absent)
  while null→{"__wdio_value__":null} (key present), making them
  distinguishable on receipt.
- dioxus-service execute.ts: unwrap the envelope in both `execute` and
  `runInterceptorScript` via a shared `unwrapEmbeddedResult` helper.
- Rebuild and commit dist-js/index.js.
- Update tests to use envelope-wrapped mock return values.

Closes #409

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…spec.ts

runInterceptorScript now unwraps the __wdio_value__ envelope, so unit
test stubs that mock browser.execute returning call-data objects need
to wrap those values in the envelope too.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@greptile-apps

greptile-apps Bot commented Jun 17, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR fixes browser.dioxus.execute() losing undefined return values on the embedded driver path by mirroring the __wdio_value__ envelope pattern from @wdio/tauri-service. It also corrects the previous iteration's regression where unwrapEmbeddedResult was applied unconditionally, now guarding it behind a per-session WeakSet<WebdriverIO.Browser> populated only when driverProvider === 'embedded'.

  • execute.ts: introduces markAsEmbedded / embeddedBrowsers WeakSet; on the embedded path injects function(__r){resolve({__wdio_value__:__r});} as the Promise resolver so the envelope (not undefined) travels through the result ?? null coercion in the polling loop unchanged; unwrapEmbeddedResult then distinguishes key-absent ({}undefined) from key-present ({__wdio_value__:null}null).
  • service.ts: derives isEmbedded in the constructor and calls markAsEmbedded per browser instance (root + each multiremote instance) inside addDioxusApi.
  • Tests: new embeddedBrowserStub helper, targeted specs for undefined/null/object results on both paths, and updated mock.spec.ts mocks to use __wdio_value__ envelopes.

Confidence Score: 5/5

Safe to merge — the external WebDriver path is fully guarded by the WeakSet and returns raw values unchanged, the Rust handler was verified to add no conflicting envelope, and all key branches (undefined, null, plain object, array) are correctly handled.

The envelope is injected at the TypeScript script level rather than in the driver, so the result ?? null coercion in the guest-js polling loop becomes a no-op for both undefined and non-null results. unwrapEmbeddedResult is gated behind embeddedBrowsers.has(browser), so external WebDriver results are never misinterpreted as envelopes. The Rust execute_sync handler was checked and does not add its own envelope, confirming there is no double-wrapping. Tests cover the critical undefined/null distinction plus the plain-object passthrough on both driver paths.

No files require special attention.

Important Files Changed

Filename Overview
packages/dioxus-service/src/commands/execute.ts Adds embeddedBrowsers WeakSet gate, conditional envelope injection in function-form execute() and runInterceptorScript, and unwrapEmbeddedResult — all logic is correct and well-guarded
packages/dioxus-service/src/service.ts Computes isEmbedded from capabilities in the constructor and calls markAsEmbedded per browser instance (including each multiremote instance) inside addDioxusApi
packages/dioxus-service/test/commands/execute.spec.ts Adds embeddedBrowserStub helper and new tests covering the envelope unwrap for undefined, null, and plain-object results on both embedded and external driver paths
packages/dioxus-service/test/mock.spec.ts Updates makeBrowser to mark the stub as embedded and wraps all interceptor-script mock return values in wdio_value envelopes to match the new embedded-driver contract

Sequence Diagram

%%{init: {'theme': 'neutral'}}%%
sequenceDiagram
    participant Test as Test
    participant Service as DioxusWorkerService
    participant Execute as execute.ts
    participant BrowserExecute as browser.execute
    participant GuestJS as guest-js polling

    Test->>Service: before()
    Service->>Execute: markAsEmbedded(browser)

    Test->>Execute: execute(browser, fn, args)
    Note over Execute: isEmbedded=true, inject envelope wrapper
    Execute->>BrowserExecute: wrapped script with resolve envelope
    BrowserExecute->>GuestJS: run via embedded channel
    Note over GuestJS: result = envelope object, not null
    GuestJS-->>BrowserExecute: JSON response with envelope
    BrowserExecute-->>Execute: raw object
    Execute->>Execute: unwrapEmbeddedResult(raw)
    Note over Execute: absent key = undefined, present key = value
    Execute-->>Test: ReturnValue (undefined or actual value)
Loading
%%{init: {'theme': 'base', 'themeVariables': {"darkMode": true, "background": "#0d1117", "primaryColor": "#21262d", "primaryTextColor": "#e6edf3", "primaryBorderColor": "#8b949e", "lineColor": "#8b949e", "textColor": "#e6edf3", "edgeLabelBackground": "#161b22", "actorBkg": "#21262d", "actorBorder": "#8b949e", "actorTextColor": "#e6edf3", "actorLineColor": "#8b949e", "signalColor": "#8b949e", "signalTextColor": "#e6edf3", "noteBkgColor": "#373320", "noteBorderColor": "#d4a72c", "noteTextColor": "#f0e6c0", "labelBoxBkgColor": "#21262d", "labelBoxBorderColor": "#8b949e", "labelTextColor": "#e6edf3", "loopTextColor": "#e6edf3", "activationBkgColor": "#30363d", "activationBorderColor": "#8b949e"}}}%%
sequenceDiagram
    participant Test as Test
    participant Service as DioxusWorkerService
    participant Execute as execute.ts
    participant BrowserExecute as browser.execute
    participant GuestJS as guest-js polling

    Test->>Service: before()
    Service->>Execute: markAsEmbedded(browser)

    Test->>Execute: execute(browser, fn, args)
    Note over Execute: isEmbedded=true, inject envelope wrapper
    Execute->>BrowserExecute: wrapped script with resolve envelope
    BrowserExecute->>GuestJS: run via embedded channel
    Note over GuestJS: result = envelope object, not null
    GuestJS-->>BrowserExecute: JSON response with envelope
    BrowserExecute-->>Execute: raw object
    Execute->>Execute: unwrapEmbeddedResult(raw)
    Note over Execute: absent key = undefined, present key = value
    Execute-->>Test: ReturnValue (undefined or actual value)
Loading

Reviews (5): Last reviewed commit: "fix(dioxus): move __wdio_value__ envelop..." | Re-trigger Greptile

Comment thread packages/dioxus-service/src/commands/execute.ts
Only call unwrapEmbeddedResult when the browser is using the embedded
driver. External WebDriver returns raw JS values — wrapping them
unconditionally would corrupt any plain object result.

Uses a module-level WeakSet<WebdriverIO.Browser> so the browser type
is not changed. markAsEmbedded() is called from addDioxusApi() when
driverProvider === 'embedded'.

Tests: add embeddedBrowserStub() helper and an external-driver
passthrough test; mark mock.spec.ts browsers as embedded so the
interceptor call-data scripts unwrap correctly.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@github-actions

github-actions Bot commented Jun 17, 2026

Copy link
Copy Markdown
Contributor

Standing release PR: #400 · 11 packages queued · open 23h 30m · ✅ ready to merge

Release Preview — 12 packages

Note: Labels on this PR are advisory in standing-pr mode. Bumps come from conventional commits in the standing PR; override by editing labels on the standing PR itself. Add release:immediate to bypass the standing PR and release this PR directly.

These changes will be added to the release PR (#400) when merged:

Changelog

Project-wide changes

Fixed

@wdio/dioxus-service N/A → 1.0.1

Fixed

@wdio/electron-cdp-bridge wdio-electron-cdp-bridge@v10.0.0 → 10.0.1

Changed

  • Update version to 10.0.1
@wdio/electron-service wdio-electron-service@v10.0.0 → 10.0.1

Changed

  • Update version to 10.0.1
@wdio/flutter-service v1.0.0-next.0 → 1.0.1

Changed

  • Update version to 1.0.1
@wdio/native-cdp-bridge v0.1.0-next.0 → 0.1.1

Changed

  • Update version to 0.1.1
@wdio/native-mobile-core v1.0.0 → 1.0.1

Changed

  • Update version to 1.0.1
@wdio/native-spy wdio-native-spy@v1.1.0 → 1.1.1

Changed

  • Update version to 1.1.1
@wdio/native-types wdio-native-types@v2.3.1 → 2.3.2

Changed

  • Update version to 2.3.2
@wdio/native-utils wdio-native-utils@v2.4.0 → 2.4.1

Changed

  • Update version to 2.4.1
@wdio/react-native-service v1.0.0-next.0 → 1.0.1

Changed

  • Update version to 1.0.1
dioxus-package-test-app v0.1.0 → 0.1.1

Changed

  • Update version to 0.1.1
wdio-dioxus-embedded-driver N/A → 1.0.1

Fixed

After merge — predicted release

No version escalation — this PR's changes will be included in the queued release without affecting the projected versions.

Package Standing PR This PR After merge
@wdio/electron-cdp-bridge 10.1.0 10.0.1 10.1.0
@wdio/electron-service 10.1.0 10.0.1 10.1.0
@wdio/flutter-service 1.1.0 1.0.1 1.1.0
@wdio/native-cdp-bridge 0.2.0 0.1.1 0.2.0
@wdio/native-mobile-core 1.1.0 1.0.1 1.1.0
@wdio/native-spy 1.2.0 1.1.1 1.2.0
@wdio/native-types 2.4.0 2.3.2 2.4.0
@wdio/native-utils 2.5.0 2.4.1 2.5.0
@wdio/react-native-service 1.1.0 1.0.1 1.1.0
dioxus-package-test-app 0.2.0 0.1.1 0.2.0
wdio-dioxus-embedded-driver 1.0.1 1.0.1 1.0.1

Updated automatically by ReleaseKit

goosewobbler and others added 2 commits June 17, 2026 19:19
…handlers

The guest-js polling loop is shared by all embedded-driver eval paths:
execute/sync, execute/async, and internal calls like getTitle/getUrl.
Wrapping every result in the polling loop broke getTitle (returned an
object instead of a string) and getUrl.

Move the envelope injection into the execute_sync and execute_async
Rust handlers instead, where it is applied only to user scripts.
Internal eval calls continue to return raw values.

The guest-js is reverted to its original `result ?? null` form.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…nvelope

The previous attempt wrapped execute_sync results as:
  return { __wdio_value__: (function() { <script> })() }

The TS execute.ts script always returns a Promise. That left an
unresolved Promise inside the envelope; JSON.stringify serialised it
as {} before it settled, corrupting the result.

Use Promise.resolve().then() instead so the inner Promise is fully
awaited before the settled value is wrapped in {__wdio_value__: r}.
This handles both synchronous returns (raw values) and async returns
(Promises from the TS function-script wrapper) uniformly.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Comment thread packages/dioxus-embedded-driver/src/server/handlers/script.rs
Comment thread packages/dioxus-embedded-driver/src/server/handlers/script.rs
The Rust execute_sync/execute_async handlers were wrapping every
/execute/sync and /execute/async response in the envelope. This broke
WDIO internal atoms (isDisplayed, scrollIntoView, etc.) that call
browser.execute() directly and have no TS-side unwrapper.

Correct approach: the Rust handlers stay as plain IIFEs. The envelope
is injected inside the TS-built function-form script's Promise .then()
resolver, only when the browser is marked as embedded. External driver
and string-form paths are unaffected.

runInterceptorScript: embedded path wraps synchronously as
`return { __wdio_value__: (${script})() }` — safe because interceptor
scripts are synchronous and need no Promise chain.

Tests updated: string-form embedded tests moved to function-form;
new test verifies the embedded wrapper in runInterceptorScript.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@goosewobbler goosewobbler merged commit ef60d51 into main Jun 18, 2026
89 checks passed
@goosewobbler goosewobbler deleted the fix/dioxus-execute-undefined-complete branch June 18, 2026 09:29
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[dioxus] execute() does not preserve undefined (returns null); mirror Tauri's __wdio_value__ envelope

1 participant