Skip to content

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

Closed
goosewobbler wants to merge 1 commit into
mainfrom
fix/dioxus-execute-undefined
Closed

fix(dioxus): preserve undefined in execute() via __wdio_value__ envelope#412
goosewobbler wants to merge 1 commit into
mainfrom
fix/dioxus-execute-undefined

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 it 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
  • Updated mock.spec.ts call-data stubs to match the new envelope shape

Closes #409

Test plan

  • pnpm --filter @wdio/dioxus-service test passes (all 12 execute + 11 mock tests green)
  • pnpm --filter @wdio/dioxus-bridge test passes
  • CI green
  • Manual: after resetAllMocks(), execute(() => invoke('cmd')) returns undefined, not null (downstream example app mocking spec)

🤖 Generated with Claude Code

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@goosewobbler goosewobbler deleted the fix/dioxus-execute-undefined branch June 17, 2026 16:46
@github-actions

Copy link
Copy Markdown
Contributor

Standing release PR: #400 · 10 packages queued · open 15h 25m · ✅ ready to merge

Release Preview — 13 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

  • preserve undefined in execute() via wdio_value envelope (dioxus)
@wdio/dioxus-bridge N/A → 1.0.1

Fixed

  • preserve undefined in execute() via wdio_value envelope (dioxus)
@wdio/dioxus-service N/A → 1.0.1

Fixed

  • preserve undefined in execute() via wdio_value envelope (dioxus)
@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

Also bumped (sync versioning)

  • wdio-dioxus-bridge → 1.0.1

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

Updated automatically by ReleaseKit

@greptile-apps

greptile-apps Bot commented Jun 17, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR fixes browser.dioxus.execute() silently coercing undefined results to null by mirroring the __wdio_value__ envelope pattern from @wdio/tauri-service. The embedded polling loop now wraps every successful result before IPC delivery, and a new unwrapEmbeddedResult helper in execute.ts reverses that wrapping — exploiting JSON.stringify's omission of undefined object properties to distinguish the two values.

  • guest-js/index.ts: Changes result ?? null to error ? null : { __wdio_value__: result } so the bridge sends a distinguishable envelope on the success path.
  • execute.ts: Adds unwrapEmbeddedResult<T> and threads it through both execute and runInterceptorScript; the key-absent branch ({}) maps back to undefined, and { __wdio_value__: null } maps to null.
  • Tests: Stubs updated throughout execute.spec.ts and mock.spec.ts to return the new envelope shape; dedicated new cases cover the undefined and null return values explicitly.

Confidence Score: 4/5

Safe to merge for the embedded Dioxus path, which is the only production usage; the one concern is a defensive gap in unwrapEmbeddedResult.

The envelope mechanism and the unwrap logic are correct for the embedded bridge path, and the test suite now exercises the previously-broken undefined vs null distinction. The minor concern is that unwrapEmbeddedResult treats any plain object without a __wdio_value__ key as an undefined result — an assumption that holds as long as all browser.execute calls are routed through the embedded polling loop, but would silently swallow real object results on a non-embedded path. This is unlikely to bite today but the assumption is undocumented at the call sites.

packages/dioxus-service/src/commands/execute.ts — the unwrapEmbeddedResult fallthrough for key-absent plain objects

Important Files Changed

Filename Overview
packages/dioxus-bridge/guest-js/index.ts Correctly replaces result ?? null with the { __wdio_value__: result } envelope on the success path; error path still sends null as before. Logic is sound — JSON.stringify drops the key for undefined, preserves it for null.
packages/dioxus-service/src/commands/execute.ts Adds unwrapEmbeddedResult and applies it to both execute and runInterceptorScript. Correct for the embedded path; the key-absent fallthrough silently returns undefined for any plain object that isn't the envelope, which could mislead callers in non-embedded contexts.
packages/dioxus-service/test/commands/execute.spec.ts Tests correctly updated to use the new envelope stubs; new cases added for undefined and null return values covering both execute and runInterceptorScript.
packages/dioxus-service/test/mock.spec.ts Call-data stubs correctly wrapped in the __wdio_value__ envelope to match the new runInterceptorScript return contract.

Sequence Diagram

%%{init: {'theme': 'neutral'}}%%
sequenceDiagram
    participant Test as Test (dioxus-service)
    participant Exec as execute.ts
    participant WD as browser.execute (WebDriver)
    participant Poll as guest-js polling loop
    participant Script as User Script

    Test->>Exec: execute(browser, fn, ...args)
    Exec->>WD: browser.execute(wrappedScript)
    WD->>Poll: queue command (id, script, args)
    Poll->>Script: AsyncFunction(...args)
    Script-->>Poll: result (any value, incl. undefined)
    Note over Poll: error ? null : { __wdio_value__: result }
    Poll->>WD: invoke('__embedded_result', envelope)
    WD-->>Exec: "raw = { __wdio_value__: X } or {}"
    Note over Exec: unwrapEmbeddedResult(raw)
    Exec-->>Test: ReturnValue
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 (dioxus-service)
    participant Exec as execute.ts
    participant WD as browser.execute (WebDriver)
    participant Poll as guest-js polling loop
    participant Script as User Script

    Test->>Exec: execute(browser, fn, ...args)
    Exec->>WD: browser.execute(wrappedScript)
    WD->>Poll: queue command (id, script, args)
    Poll->>Script: AsyncFunction(...args)
    Script-->>Poll: result (any value, incl. undefined)
    Note over Poll: error ? null : { __wdio_value__: result }
    Poll->>WD: invoke('__embedded_result', envelope)
    WD-->>Exec: "raw = { __wdio_value__: X } or {}"
    Note over Exec: unwrapEmbeddedResult(raw)
    Exec-->>Test: ReturnValue
Loading

Fix All in Claude Code Fix All in Cursor

Reviews (1): Last reviewed commit: "fix(dioxus): preserve undefined in execu..." | Re-trigger Greptile

Comment on lines +106 to +114
if (raw !== null && typeof raw === 'object' && !Array.isArray(raw)) {
const envelope = raw as Record<string, unknown>;
if ('__wdio_value__' in envelope) {
return envelope['__wdio_value__'] as T;
}
// Key absent: the result was undefined (omitted by JSON.stringify)
return undefined as unknown as T;
}
return raw as T;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Silent undefined for any non-envelope object in non-embedded contexts

unwrapEmbeddedResult returns undefined for ANY plain object that lacks __wdio_value__, not just the serialized-undefined empty envelope {}. This is correct when browser.execute is always routed through the embedded Dioxus polling loop (which guarantees the envelope). However, the string-script overload of execute carries no such guarantee — the file header documents it as providing "standard WDIO behaviour where the body is wrapped as function() { ${body} }", implying it can be called against a non-embedded WebDriver. If that path is ever exercised and the script returns a plain object (e.g., { x: 1 }), unwrapEmbeddedResult would fall into the key-absent branch and silently return undefined instead of the actual value. Adding an explicit guard (e.g., checking Object.keys(envelope).length === 0 for the empty-envelope case, or only applying unwrapEmbeddedResult when the embedded bridge is known to be active) would make the assumption explicit and prevent silent corruption.

Fix in Claude Code Fix in Cursor

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