Skip to content

Commit a7ae2d9

Browse files
committed
fix(e2e): fresh browser fixture eliminates Firefox WS flake
Firefox's WS upgrade silently degrades after ~54 tests in one process: server receives the upgrade request but the handshake never completes. Override Playwright's worker-scoped browser fixture to launch a fresh instance for websocket.spec.ts, giving all WS tests a clean networking stack. Zero test code changes — context/page inherit automatically. Result: 146/146 passed, 0 flaky, 1.5 min total.
1 parent f69df68 commit a7ae2d9

1 file changed

Lines changed: 68 additions & 16 deletions

File tree

pkgs/id/e2e/tests/websocket.spec.ts

Lines changed: 68 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,71 @@
1-
import { expect, type Page, test } from "@playwright/test";
1+
import {
2+
test as base,
3+
expect,
4+
type Browser,
5+
type BrowserType,
6+
type Page,
7+
chromium,
8+
firefox,
9+
webkit,
10+
} from "@playwright/test";
11+
12+
const browserTypes: Record<string, BrowserType> = { chromium, firefox, webkit };
13+
14+
// ---------------------------------------------------------------------------
15+
// Fresh browser fixture — WHY this file overrides Playwright's default browser
16+
// ---------------------------------------------------------------------------
17+
//
18+
// PROBLEM:
19+
// This test file runs LAST in the Playwright suite (files run alphabetically:
20+
// basic → file-operations → navigation → websocket). By the time Playwright
21+
// reaches this file, Firefox's shared browser process has already executed
22+
// ~54 tests across 3 prior test files — all using the same browser process.
23+
//
24+
// After that many sequential page navigations in a single Firefox process,
25+
// the WebSocket upgrade path SILENTLY DEGRADES: the server receives the HTTP
26+
// upgrade request, but the WS handshake never completes at the protocol level.
27+
// Server logs confirm this — a 40+ second gap between the upgrade request
28+
// arriving and any subsequent activity, with no "Client connected" or
29+
// "Sending Init" log entries. The connection just hangs at the HTTP layer.
30+
//
31+
// Regular HTTP continues to work fine (which is why basic/file-ops/navigation
32+
// tests pass without issue), but the HTTP→WebSocket protocol switch machinery
33+
// in Firefox's networking stack breaks down after extended use.
34+
//
35+
// EVIDENCE:
36+
// - Server logs show the upgrade request arrives but WS handshake never
37+
// completes (no "Client connected" after the initial connection log)
38+
// - The client's 2s connect timeout fires, triggering reconnect
39+
// - The RETRY always succeeds (same browser process, new socket)
40+
// - But the initial 2s timeout + 30s test timeout = test fails before
41+
// the retry can complete
42+
// - Chromium is completely unaffected by this issue
43+
// - The problem ONLY occurs after ~40+ prior tests in the same process
44+
// - A fresh browser process connects instantly every time
45+
//
46+
// FIX:
47+
// Override Playwright's `browser` fixture to launch a FRESH browser instance
48+
// for this file. Since `browser` is worker-scoped (one per file when
49+
// workers=1), this gives all WebSocket tests a clean networking stack.
50+
// The built-in `context` and `page` fixtures automatically inherit from our
51+
// fresh browser, so NO test code changes are needed — tests keep using
52+
// `{ page }`, `{ browser, baseURL }`, etc. exactly as before.
53+
//
54+
// COST:
55+
// ~1-2s of browser launch overhead per worker, which eliminates the 30s+
56+
// timeout-and-retry cycle that made these tests flaky in Firefox.
57+
// Chromium is unaffected by the bug but also benefits (no downside).
58+
// ---------------------------------------------------------------------------
59+
const test = base.extend<{}, { browser: Browser }>({
60+
browser: [
61+
async ({ browserName }, use) => {
62+
const browser = await browserTypes[browserName].launch();
63+
await use(browser);
64+
await browser.close();
65+
},
66+
{ scope: "worker" },
67+
],
68+
});
269

370
// WebSocket tests need more headroom than basic tests because Firefox's WS
471
// handshake can occasionally hang (~20s browser timeout + reconnect delay).
@@ -118,23 +185,8 @@ async function createFileWithUniqueContent(page: Page, name: string, baseURL: st
118185

119186
test.describe("WebSocket Connection + Editor Ready", () => {
120187
test("editor status shows connected after WS handshake", async ({ page, baseURL }) => {
121-
// This is the first WS test in the full suite (~test #129). In Firefox,
122-
// the browser process accumulates state from ~128 prior tests that can
123-
// cause the first WS upgrade request to hang in CONNECTING state. The 2s
124-
// connect timeout + fast reconnect (250ms base backoff) in collab.ts
125-
// recovers in ~2.5s per attempt, so default 20s waitForEditorReady
126-
// easily handles 5+ reconnect cycles without needing test.slow().
127-
128188
const fileName = `ws-connect-${Date.now()}.txt`;
129189

130-
// Warmup: force Firefox to make a real HTTP request before attempting WS.
131-
// The API calls in createFileWithUniqueContent use Playwright's internal
132-
// HTTP client (not Firefox), so the browser hasn't made any requests to
133-
// this origin yet. Loading "/" first establishes TCP connections and may
134-
// help prime Firefox's connection pool for the WS handshake.
135-
await page.goto("/");
136-
await page.waitForLoadState("domcontentloaded");
137-
138190
// Use unique content to avoid shared collab document under Firefox load
139191
await createFileWithUniqueContent(page, fileName, baseURL!);
140192

0 commit comments

Comments
 (0)