Repository navigation
Tauri Service: pre-load IPC mock injection to capture startup invoke() calls in browser mode #259
Description
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() callsAffected scope
| Mode | Affected? |
|---|---|
Tauri browser mode (devServerUrl) |
✅ Yes – confirmed |
| Tauri native mode (real binary) | 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
- PR feat(tauri): implement browser mode in TauriLaunchService #255 – browser mode implementation (where this limitation was first documented)
Activity
[-]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[/+]goosewobbler commentedon Jun 24, 2026
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
browser.execute()afterbrowser.url()resolves (service.ts→initBrowserMode). Sowindow.__TAURI_INTERNALS__.invokedoesn't exist while the page's own startup scripts run.tauri.mock('greet')has populatedwindow.__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 stripstauri: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
addPreloadScriptworks on our exact stack. Ran a live test (WDIO 9.27.1 + Chrome/chromedriver 149):isBidi: true, and it auto-enables forbrowserName: 'chrome'with no explicitwebSocketUrlcap — WDIO 9 opts non-Safari browsers into BiDi unlesswdio:enforceWebDriverClassicis set (webdriver/build/index.js:1310-1321). So browser mode already opens a BiDi socket today; we just aren't using it.window.__TAURI_INTERNALS__.invokeran in the main world, before page scripts — it captured a startupinvoke('greet', {name:'startup'})fired during initial HTML parse.browser.executesaw the samewindow.browser.url()), so it also subsumes the currentpatchBrowserUrlre-injection and is more robust — it catches full reloads /location.hrefchanges, not justbrowser.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-pluginis compiled into the app andsetupInvokeInterception()runs at plugininit()(import / DOMContentLoaded), wrappingcore.invokebefore 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 setenforceWebDriverClassic).Recommendation
script.addPreloadScript(Bump octokit/request-action from 2.1.9 to 2.4.0 in the minor-deps-updates group #2) for browser mode, as a layered enhancement, not a hard dependency:browser.url()whenbrowser.isBidiis true.browser.execute(injectionScript)as an idempotent belt-and-suspenders fallback, so a classic session (or a failed BiDi call) degrades to exactly today's behavior — nothing regresses.wdio:enforceWebDriverClassic: true→isBidifalse → preload skipped automatically. No new service option.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.
feat(tauri-service): capture startup invoke() via BiDi preload in bro…
feat(tauri-service): capture startup invoke() via BiDi preload in bro…
feat(tauri-service): capture startup invoke() via BiDi preload in bro…