Skip to content

Tauri Service: pre-load IPC mock injection to capture startup invoke() calls in browser mode #259

Closed

Description

@goosewobbler

Problem

In browser mode (devServerUrl set), the IPC interceptor is injected after browser.url() resolves — i.e. after readyState === 'complete'. Any invoke() calls the app makes during module initialisation, DOMContentLoaded, or onload handlers run against the real (unpatched) __TAURI_INTERNALS__ and are invisible to mocks.

// service.ts – current flow
await browser.url(this.devServerUrl);   // waits for readyState=complete
await browser.execute(injectionScript); // too late for startup invoke() calls

Affected scope

Mode Affected?
Tauri browser mode (devServerUrl) ✅ Yes – confirmed
Tauri native mode (real binary) ⚠️ Potentially – mock registration happens after before() hook; any invoke() during app launch before the WebDriver session is fully established may also be missed
Electron ❌ No – Electron's IPC is intercepted at the Node.js layer via ipcMain, not injected into the renderer

Proposed solutions

1. Vite plugin (browser mode only)

Ship an optional @wdio/tauri-vite-plugin that imports the injection script as a top-level module in the dev build entry point. This guarantees the mock infrastructure is available before any app code runs.

// vite.config.ts
import { wdioTauriPlugin } from '@wdio/tauri-vite-plugin';
export default { plugins: [wdioTauriPlugin()] };

2. WebDriver BiDi script.addPreloadScript (browser mode)

WebDriver BiDi (supported in ChromeDriver 115+, Firefox 128+) provides script.addPreloadScript which runs before any page script. This would work without a Vite plugin and would not require @wdio/devtools-service.

await browser.scriptAddPreloadScript({ functionDeclaration: injectionScript });

3. Investigate Tauri native mode

Audit whether the native-mode mock lifecycle (registered in the before() hook) can miss startup invoke() calls when the app is already running before the WebDriver session attaches.

Current workaround

Structure tests so that all mocks are created before the first navigation:

before(async () => {
  await browser.url('/');          // navigate first
  const greetMock = await tauri.mock('greet');  // then mock
  // startup invoke() calls already happened — this mock only captures future calls
});

Or defer navigation until after mock setup:

before(async () => {
  const greetMock = await tauri.mock('greet');  // mock first
  await browser.url('/');                        // then navigate
  // still too late — inject is post-load
});

Neither workaround is reliable for true startup calls. The Vite plugin approach is the recommended interim solution.

Related

Activity

  1. changed the title [-]feat(tauri-service): pre-load IPC mock injection to capture startup invoke() calls in browser mode[/-] [+]Tauri Service: pre-load IPC mock injection to capture startup invoke() calls in browser mode[/+] on May 6, 2026
  2. goosewobbler commented on Jun 24, 2026

    @goosewobbler
    ContributorAuthor

    Investigation summary

    Looked into this. Short version: approach #2 (BiDi script.addPreloadScript) is the right fix for browser mode, the Vite plugin (#1) can be dropped, and native mode (#3) isn't affected the way the table suggests.

    The problem is really two separate ordering problems

    1. Infra-timing — the actual bug. In browser mode the IPC interceptor is injected by the service via browser.execute() after browser.url() resolves (service.ts → initBrowserMode). So window.__TAURI_INTERNALS__.invoke doesn't exist while the page's own startup scripts run.
    2. Mock-declaration timing — a fundamental floor. Even with the interceptor present, a command is only intercepted once tauri.mock('greet') has populated window.__wdio_mocks__. The very first page load happens before any test code runs, so its startup invokes can't be retroactively mocked — no approach fixes that without the user declaring intent ahead of time.

    Evidence

    Browser mode is plain Chrome. The launcher rewrites caps to browserName: 'chrome' and strips tauri:options (launcher.ts:173-175). There is no Tauri webview in browser mode — so "will BiDi work with the Tauri service" is a non-issue here; it's just Chrome + chromedriver.

    BiDi addPreloadScript works on our exact stack. Ran a live test (WDIO 9.27.1 + Chrome/chromedriver 149):

    • isBidi: true, and it auto-enables for browserName: 'chrome' with no explicit webSocketUrl cap — WDIO 9 opts non-Safari browsers into BiDi unless wdio:enforceWebDriverClassic is set (webdriver/build/index.js:1310-1321). So browser mode already opens a BiDi socket today; we just aren't using it.
    • A preload script patching window.__TAURI_INTERNALS__.invoke ran in the main world, before page scripts — it captured a startup invoke('greet', {name:'startup'}) fired during initial HTML parse. browser.execute saw the same window.
    • The preload persists across navigations (re-ran on a 2nd browser.url()), so it also subsumes the current patchBrowserUrl re-injection and is more robust — it catches full reloads / location.href changes, not just browser.url() calls.

    This repo already trusts BiDi in browser mode. The Electron service disables BiDi in native mode (electron-service/src/launcher.ts:279-280, enforceWebDriverClassic = true, because it drives embedded Chromium via CDP) but deletes that flag in browser mode (launcher.ts:157) to let vanilla Chrome's BiDi work. Same split we want for Tauri: BiDi in browser mode, classic in native.

    Native mode isn't injection-affected. @wdio/tauri-plugin is compiled into the app and setupInvokeInterception() runs at plugin init() (import / DOMContentLoaded), wrapping core.invoke before the app's startup invokes (tauri-plugin/guest-js/index.ts:434-536). The infra is present early; only the first-load mock-declaration floor remains. BiDi can't help native mode regardless — tauri-driver proxies classic WebDriver, WebKitWebDriver has no BiDi socket, and the embedded driver is custom (and the native e2e configs already set enforceWebDriverClassic).

    Recommendation

    The remaining first-load floor (mocking invokes that fire before any mock is declared) can be closed for browser mode by registering user-declared mocks as additional preload scripts before the first navigation — a follow-up, not part of the core fix.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions