From bf43b9f0320ba1567229e73cc759b1056a5db0c0 Mon Sep 17 00:00:00 2001 From: Francis Lavoie Date: Sun, 12 Jul 2026 04:47:58 -0400 Subject: [PATCH 1/2] fix: connect over TCP on Windows (Unix socket fallback) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Windows can't bind a Unix domain socket, so the Rust plugin falls back to a TCP listener, but the fixture ignored that and always dialed the socket path — so `mode: 'tauri'` could never connect on Windows. - Use the endpoint TauriProcessManager.start() actually detected ({ tcpPort } | { socketPath }) instead of discarding it and hardcoding the socket path. - For an already-running app on Windows, connect to the TCP port (127.0.0.1:6274 by default, overridable via TAURI_PW_TCP_PORT) since there is no socket file to wait for. - Retry the initial connect while the app finishes booting. Fixes #12 Co-Authored-By: Claude Opus 4.8 (1M context) --- packages/test/src/fixture.ts | 44 ++++++++++++++++++++++++++++++++---- 1 file changed, 40 insertions(+), 4 deletions(-) diff --git a/packages/test/src/fixture.ts b/packages/test/src/fixture.ts index 9950e2f..1ee2607 100644 --- a/packages/test/src/fixture.ts +++ b/packages/test/src/fixture.ts @@ -63,6 +63,16 @@ export function createTauriTest(config: TauriTestConfig) { try { const socketPath = config.mcpSocket ?? '/tmp/tauri-playwright.sock'; + // Windows can't bind a Unix domain socket, so the Rust plugin falls + // back to a TCP listener (127.0.0.1:6274 by default). Track which + // endpoint we should actually dial rather than always assuming the + // socket path. + const isWindows = process.platform === 'win32'; + const envPort = Number(process.env.TAURI_PW_TCP_PORT); + const defaultTcpPort = + Number.isFinite(envPort) && envPort > 0 ? envPort : 6274; + let connectSocketPath: string | undefined = socketPath; + let connectTcpPort: number | undefined; // If a Tauri command is configured, spawn the app if (config.tauriCommand) { @@ -75,16 +85,42 @@ export function createTauriTest(config: TauriTestConfig) { socketPath, startTimeout: config.startTimeout ?? 120, }); - await processManager.start(); + // start() already parses the plugin's "listening on ..." line and + // returns { socketPath } or { tcpPort }; honor whichever it found + // instead of discarding it and hardcoding the socket path. + const started = await processManager.start(); + if (started?.tcpPort) { + connectTcpPort = started.tcpPort; + connectSocketPath = undefined; + } else if (started?.socketPath) { + connectSocketPath = started.socketPath; + } + } else if (isWindows) { + // Already-running app on Windows: there is no socket file to wait + // for — the plugin is listening on TCP. + connectTcpPort = defaultTcpPort; + connectSocketPath = undefined; } else { // Assume the app is already running — wait for the socket const pm = new TauriProcessManager({ socketPath }); await pm.waitForSocket(30000); } - // Connect to the plugin - client = new PluginClient(socketPath); - await client.connect(); + // Connect to the plugin, retrying while the app finishes booting. + const connectDeadline = Date.now() + 30000; + for (;;) { + client = connectTcpPort + ? new PluginClient(undefined, connectTcpPort) + : new PluginClient(connectSocketPath); + try { + await client.connect(); + break; + } catch (err) { + client = null; + if (Date.now() > connectDeadline) throw err; + await new Promise((r) => setTimeout(r, 300)); + } + } // Verify connection const ping = await client.send({ type: 'ping' }); From e0b96183233212a76c1087a5a8241d035a09c458 Mon Sep 17 00:00:00 2001 From: Francis Lavoie Date: Sun, 12 Jul 2026 05:39:20 -0400 Subject: [PATCH 2/2] feat: add reloadBeforeEach option to skip the per-test page reload MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The tauri-mode fixture reloads the page (window.location.href = devUrl) before each test to reset state. On apps whose reload reboots a heavy runtime — re-running app bootstrap, reconnecting sockets, etc. — this is unsafe: the post-reload readiness poll races the reboot, and an `eval` issued while the page is mid-navigation never receives its `pw_result`, so the test hangs until the plugin's 30s eval timeout (deterministically on the first test after a cold launch). Add `reloadBeforeEach` (default `true`, so existing behavior is unchanged). Set it to `false` to run tests against the app's current shared state and avoid the reload entirely; the readiness wait is still performed. Co-Authored-By: Claude Opus 4.8 (1M context) --- packages/test/src/fixture.ts | 14 ++++++++++---- packages/test/src/types.ts | 13 +++++++++++++ 2 files changed, 23 insertions(+), 4 deletions(-) diff --git a/packages/test/src/fixture.ts b/packages/test/src/fixture.ts index 1ee2607..d7cb66c 100644 --- a/packages/test/src/fixture.ts +++ b/packages/test/src/fixture.ts @@ -130,11 +130,17 @@ export function createTauriTest(config: TauriTestConfig) { tauriPage = new TauriPage(client); - // Reset app state by reloading the page before each test if (config.devUrl) { - await tauriPage.evaluate(`window.location.href = ${JSON.stringify(config.devUrl)}`); - // Wait for the page to reload and the polling bridge to reconnect - await new Promise((r) => setTimeout(r, 500)); + // Reset app state by reloading the page before each test. Skipped + // when reloadBeforeEach is false: on apps whose reload reboots a + // heavy runtime, the readiness poll below races that reboot and an + // eval issued mid-navigation never gets its pw_result, hanging the + // test until the plugin's 30s eval timeout. + if (config.reloadBeforeEach !== false) { + await tauriPage.evaluate(`window.location.href = ${JSON.stringify(config.devUrl)}`); + // Wait for the page to reload and the polling bridge to reconnect + await new Promise((r) => setTimeout(r, 500)); + } await tauriPage.waitForFunction( 'document.readyState === "complete" && !!window.__PW_ACTIVE__', ); diff --git a/packages/test/src/types.ts b/packages/test/src/types.ts index 84a9427..3c32326 100644 --- a/packages/test/src/types.ts +++ b/packages/test/src/types.ts @@ -56,6 +56,19 @@ export interface TauriTestConfig { /** Startup timeout in seconds. Default: 120 */ startTimeout?: number; + /** + * Tauri mode only. Reload the page (`window.location.href = devUrl`) before + * each test to reset state. Default: `true`. + * + * Set to `false` for apps whose reload reboots a heavy runtime (re-running + * app bootstrap, reconnecting sockets, etc.): the post-reload readiness poll + * races that reboot, and an `eval` issued mid-navigation never receives its + * `pw_result` — so the test hangs until the plugin's 30s eval timeout. With + * the reload off, tests run against the app's current (shared) state; reset + * explicitly where a test needs a clean slate. + */ + reloadBeforeEach?: boolean; + /** * CDP endpoint for connecting to WebView2 on Windows. * When mode is 'cdp', Playwright connects directly via Chrome DevTools Protocol.