Skip to content
Closed
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
2 changes: 1 addition & 1 deletion packages/dioxus-bridge/guest-js/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -149,7 +149,7 @@ if (typeof window.__WDIO_EMBEDDED_PORT === 'number' && !window.__WDIO_EMBEDDED_R
error = e instanceof Error ? `${e.name}: ${e.message}` : String(e);
}
try {
await invoke('__embedded_result', { id: cmd.id, result: result ?? null, error });
await invoke('__embedded_result', { id: cmd.id, result: error ? null : { __wdio_value__: result }, error });
} catch {
// Result delivery failed (e.g. IPC teardown during navigation).
// Attempt once more with an error marker so the Axum oneshot
Expand Down
27 changes: 24 additions & 3 deletions packages/dioxus-service/src/commands/execute.ts
Original file line number Diff line number Diff line change
Expand Up @@ -75,10 +75,12 @@ export async function execute<ReturnValue, InnerArguments extends unknown[] = un
}
});
`;
return (await browser.execute(wrapped)) as ReturnValue;
const raw = await browser.execute(wrapped);
return unwrapEmbeddedResult<ReturnValue>(raw);
}

return (await browser.execute(script, ...args)) as ReturnValue;
const raw = await browser.execute(script, ...args);
return unwrapEmbeddedResult<ReturnValue>(raw);
}

/**
Expand All @@ -90,5 +92,24 @@ export async function execute<ReturnValue, InnerArguments extends unknown[] = un
* Not exported from the package's public surface — only consumed by mock.ts.
*/
export async function runInterceptorScript<T>(browser: WebdriverIO.Browser, script: string): Promise<T> {
return browser.execute(`return (${script})()`) as Promise<T>;
const raw = await browser.execute(`return (${script})()`);
return unwrapEmbeddedResult<T>(raw);
}

// The embedded polling loop wraps every result in { __wdio_value__: result }
// before JSON-serialising it through the IPC channel. JSON.stringify omits
// undefined properties, so { __wdio_value__: undefined } → {} (key absent),
// while { __wdio_value__: null } → {"__wdio_value__":null} (key present).
// This lets the service distinguish undefined from null — something plain
// WebDriver cannot do because both map to JSON null.
function unwrapEmbeddedResult<T>(raw: unknown): T {
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;
Comment on lines +106 to +114

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

}
19 changes: 17 additions & 2 deletions packages/dioxus-service/test/commands/execute.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,10 +50,20 @@ describe('execute command', () => {
});

it('should return the value produced by browser.execute', async () => {
const browser = browserStub({ hello: 'world' });
const browser = browserStub({ __wdio_value__: { hello: 'world' } });
await expect(execute(browser, 'return {}')).resolves.toEqual({ hello: 'world' });
});

it('should return undefined when the embedded driver returns the empty envelope (undefined result)', async () => {
const browser = browserStub({});
await expect(execute(browser, 'return undefined')).resolves.toBeUndefined();
});

it('should return null when the embedded driver returns a null-valued envelope', async () => {
const browser = browserStub({ __wdio_value__: null });
await expect(execute(browser, 'return null')).resolves.toBeNull();
});

it('should inline multiple positional args as JSON into the wrapper', async () => {
const browser = browserStub();
await execute(browser, (_dx, _a: number, _b: string, _c: boolean) => null, 1, 'two', true);
Expand Down Expand Up @@ -96,8 +106,13 @@ describe('runInterceptorScript', () => {
});

it('should return the value produced by browser.execute', async () => {
const browser = browserStub({ calls: [['a']], results: [], invocationCallOrder: [1] });
const browser = browserStub({ __wdio_value__: { calls: [['a']], results: [], invocationCallOrder: [1] } });
const result = await runInterceptorScript(browser, '(_dx) => ({ calls: [["a"]] })');
expect(result).toEqual({ calls: [['a']], results: [], invocationCallOrder: [1] });
});

it('should return undefined when the embedded driver returns the empty envelope', async () => {
const browser = browserStub({});
await expect(runInterceptorScript(browser, '() => undefined')).resolves.toBeUndefined();
});
});
30 changes: 18 additions & 12 deletions packages/dioxus-service/test/mock.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -118,9 +118,11 @@ describe('createMock', () => {
vi.mocked(browser.execute)
.mockResolvedValueOnce(undefined)
.mockResolvedValueOnce({
calls: [['arg1']],
results: [{ type: 'return', value: 42 }],
invocationCallOrder: [1],
__wdio_value__: {
calls: [['arg1']],
results: [{ type: 'return', value: 42 }],
invocationCallOrder: [1],
},
});

const m = await createMock('greet', browser);
Expand All @@ -136,18 +138,22 @@ describe('createMock', () => {
.mockResolvedValueOnce(undefined)
// First update: two calls.
.mockResolvedValueOnce({
calls: [['a'], ['b']],
results: [
{ type: 'return', value: 1 },
{ type: 'return', value: 2 },
],
invocationCallOrder: [1, 2],
__wdio_value__: {
calls: [['a'], ['b']],
results: [
{ type: 'return', value: 1 },
{ type: 'return', value: 2 },
],
invocationCallOrder: [1, 2],
},
})
// Second update: inner cleared, only one call now.
.mockResolvedValueOnce({
calls: [['c']],
results: [{ type: 'return', value: 3 }],
invocationCallOrder: [3],
__wdio_value__: {
calls: [['c']],
results: [{ type: 'return', value: 3 }],
invocationCallOrder: [3],
},
});

const m = await createMock('greet', browser);
Expand Down
Loading