Browser Mode
Browser mode lets you test your Dioxus frontend UI in plain Chrome against a running dev server — no Dioxus binary, no driver. The Dioxus invoke API is intercepted at the JavaScript boundary in the renderer, so you can mock individual commands and assert on call arguments just like in native mode.
Overview
What Is It?
Browser mode is a frontend-only test mode. Your frontend code runs for real in Chrome; the Dioxus Rust backend is replaced by mocks you define per command. Same WDIO API, same frontend code path — the only thing that changes is what's on the other end of invoke(...).
In normal (native) mode the service launches your compiled Dioxus app, drives it via the configured driver provider, and communicates with the backend through the bridge. Browser mode replaces all of that with a standard Chrome session: it sets browserName to 'chrome', navigates to your dev server URL, and injects a lightweight script that patches the invoke API so your Dioxus commands can be intercepted in tests.
Why Use It?
- No build step needed — point the service at a dev server and start testing immediately.
- Fast feedback — no Dioxus startup, no Rust compilation, no driver negotiation.
- Standard browser devtools — Chrome DevTools and HMR work as normal during development.
When to Use It
Browser mode is the right choice when your tests are renderer-focused: asserting UI state, verifying that components call the correct Dioxus commands with the right arguments, or checking that the renderer handles mock responses correctly.
It is not suitable when your tests need to:
- Call
browser.dioxus.execute()to run code with access to the bridge - Test window management with
browser.dioxus.switchWindow()orbrowser.dioxus.listWindows() - Use
browser.dioxus.triggerDeeplink() - Assert on real command round-trips to a running Rust backend
For those scenarios use native mode (the default).
Setup
1. Start Your Dev Server
Browser mode requires a running dev server that serves your frontend code.
Or let the service start and stop it for you with the optional devServer option (see Auto-managing the dev server below) — then you don't need to start it yourself.
2. Configure the Service
Set mode: 'browser' and provide devServerUrl in your WDIO configuration. No appBinaryPath, dioxus:options, or driver config is needed.
wdio.conf.ts
export const config = {
services: ['@wdio/dioxus-service'],
capabilities: [
{
browserName: 'dioxus',
'wdio:dioxusServiceOptions': {
mode: 'browser',
devServerUrl: 'http://localhost:8080',
},
},
],
};
You can also set mode and devServerUrl at the global service level:
export const config = {
services: [
[
'@wdio/dioxus-service',
{
mode: 'browser',
devServerUrl: 'http://localhost:8080',
},
],
],
capabilities: [
{ browserName: 'dioxus' },
],
};
Capability-level options take precedence over service-level ones. All capabilities in a session must use the same mode; mixing 'native' and 'browser' across capabilities throws a SevereServiceError at startup.
Auto-managing the dev server
By default you start the dev server yourself. The optional devServer service option makes the service spawn it in onPrepare, wait until devServerUrl is reachable, and tear it down on completion (and on a startup failure). It takes three forms:
// 1. A shell command (the common case)
devServer: 'pnpm dev'
// 2. An object — command plus cwd/env/timeout, and reuse control
devServer: {
command: 'pnpm dev',
cwd: './app',
env: { NODE_ENV: 'test' },
timeoutMs: 60_000, // readiness budget (default 60s, 120s in CI)
reuseExistingServer: true, // default: reuse a running server locally, always spawn in CI
}
// 3. A function — start it programmatically and return how to close it.
// No subprocess/quoting/child-kill involved; the returned `url` supplies/overrides devServerUrl.
devServer: async () => {
const server = await createServer();
await server.listen();
return { url: server.resolvedUrls.local[0], close: () => server.close() };
}
Notes:
devServeris a service-level option (one dev server per run) and only applies in browser mode.- With
reuseExistingServer(default off in CI, on locally), ifdevServerUrlis already reachable the spawn is skipped — handy when you already havepnpm devrunning during local iteration. - The command form spawns in a shell and tears down the whole process tree (so
pnpm dev → vite → esbuildgrandchildren don't orphan); the function form avoids subprocesses entirely. - The same option is available on the Tauri, Electron, and Electrobun services.
IPC Mocking
How It Works
When the session starts, the service injects a script into the page that:
- Creates
window.__wdio_mocks__— a registry of per-command mock functions. - Patches the Dioxus invoke API to look up
window.__wdio_mocks__[command]and call it; throws if the command has no registered mock.
The injection script runs again after every browser.url() navigation because a page load wipes window state.
Mocking a Command
const mockReadFile = await browser.dioxus.mock('read_file');
await mockReadFile.mockResolvedValue('mocked file content');
Asserting on Calls
After triggering the relevant UI action, call update() to sync call data from the browser-side spy to the outer mock object, then assert:
await $('button#load-file').click();
await mockReadFile.update();
expect(mockReadFile).toHaveBeenCalledTimes(1);
expect(mockReadFile.mock.calls[0]).toEqual([{ path: '/some/file' }]);
Element commands (click, doubleClick, setValue, clearValue) trigger update() automatically on all active mocks.
Setting Implementations
// Return a fixed value
await mockReadFile.mockReturnValue('file content');
// Resolve a promise (for async commands)
await mockReadFile.mockResolvedValue('file content');
// Use a function for dynamic responses
await mockReadFile.mockImplementation((args) => {
return `content of ${args.path}`;
});
// Respond differently on first call, then fall back
await mockReadFile.mockResolvedValueOnce('first call content');
await mockReadFile.mockResolvedValue('default content');