Skip to content

Commit 67bc916

Browse files
committed
fix(e2e): reclaim stale :4445 before each Windows webdriver session
1 parent b82b84e commit 67bc916

1 file changed

Lines changed: 53 additions & 3 deletions

File tree

e2e-webview/support/browser.ts

Lines changed: 53 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,16 @@
11
import { remote } from 'webdriverio';
22
import { execFile } from 'node:child_process';
3+
import net from 'node:net';
34

45
type RemoteOptions = Parameters<typeof remote>[0];
56

67
let current: WebdriverIO.Browser | undefined;
78

9+
// The in-app (tauri-plugin-webdriver) server the app binds once tauri-webdriver
10+
// spawns it. Only one app instance can hold it at a time, so a stale instance
11+
// here is what stalls the next spec's newSession.
12+
const IN_APP_WEBDRIVER_PORT = 4445;
13+
814
/**
915
* Windows-only safety net against native-dialog session poisoning.
1016
*
@@ -16,9 +22,11 @@ let current: WebdriverIO.Browser | undefined;
1622
* subsequent spec cascades. Force-killing the app frees the port and tears down
1723
* the stuck dialog, so each spec gets a clean instance.
1824
*
19-
* Runs only in teardown (closeBrowser), so a spec's app is always reclaimed
20-
* before the next spec launches its own — without adding latency to the tight
21-
* beforeAll/newBrowser budget. No-op on macOS/Linux: they dismiss the dialog
25+
* Runs in teardown (closeBrowser) and, defensively, again right before the next
26+
* spec connects (newBrowser) — teardown alone has two holes that still poison
27+
* the next launch: taskkill returns before the OS releases :4445, and a spec
28+
* whose own beforeAll timed out never set `current`, so its closeBrowser was a
29+
* no-op and nothing was reclaimed. No-op on macOS/Linux: they dismiss the dialog
2230
* reliably, those CI rows don't exhibit the cascade, and a name-based kill
2331
* there could take out a developer's `tauri dev` instance during a local run.
2432
*/
@@ -35,7 +43,49 @@ async function forceKillApp(): Promise<void> {
3543
});
3644
}
3745

46+
/**
47+
* Resolves true when nothing is listening on the in-app webdriver port — i.e. a
48+
* prior app instance has fully exited and released it. A localhost connect
49+
* either completes or refuses promptly; on the unlikely timeout we treat the
50+
* port as free (no firewall drops loopback) so the wait can't wedge.
51+
*/
52+
function isPortFree(port: number, host = '127.0.0.1', timeoutMs = 500): Promise<boolean> {
53+
return new Promise((resolve) => {
54+
const socket = net.connect({ port, host });
55+
const done = (free: boolean) => {
56+
socket.removeAllListeners();
57+
socket.destroy();
58+
resolve(free);
59+
};
60+
socket.setTimeout(timeoutMs);
61+
socket.once('connect', () => done(false)); // something is still listening
62+
socket.once('timeout', () => done(true));
63+
socket.once('error', () => done(true)); // ECONNREFUSED → free
64+
});
65+
}
66+
67+
async function waitForPortFree(port: number, capMs = 6000): Promise<void> {
68+
const deadline = Date.now() + capMs;
69+
while (Date.now() < deadline) {
70+
if (await isPortFree(port)) return;
71+
await new Promise((r) => setTimeout(r, 250));
72+
}
73+
// Best-effort: fall through after the cap. remote() then proceeds as it did
74+
// before this guard existed — no worse than the prior behaviour.
75+
}
76+
3877
export async function newBrowser(opts: RemoteOptions): Promise<WebdriverIO.Browser> {
78+
// Pre-launch reclaim (Windows only). Before connecting, kill any app instance
79+
// a prior spec left behind and wait for :4445 to actually free, so this
80+
// session never races a dying instance for the port — the cascade that
81+
// otherwise stalls newSession past the 30s beforeAll ceiling and reds the
82+
// gate. On the first spec there's nothing to kill and the port is already
83+
// free, so this costs ~one refused connect. No-op on macOS/Linux (see
84+
// forceKillApp).
85+
if (process.platform === 'win32') {
86+
await forceKillApp();
87+
await waitForPortFree(IN_APP_WEBDRIVER_PORT);
88+
}
3989
current = await remote(opts);
4090
return current;
4191
}

0 commit comments

Comments
 (0)