// An unmodified ws WebSocketServer (demo/apps/ws, ws pinned by its lockfile) serving // a page whose WebSocket goes through the shim. tests/goldens/ws/ws.json is what the pinned // Node saw running the same client script (demo/apps/ws/public/client.js), recorded by // `node scripts/golden.mjs`. import { test, expect, type Frame, type Page } from "@playwright/test"; import { readGolden } from "../scripts/golden-files.ts"; const GOLDEN = readGolden("ws", "ws"); async function appFrame(page: Page, query = ""): Promise { await page.goto(`/demo/ws.html${query}`); await expect(page.frameLocator("#app").locator("#hi")).toBeVisible({ timeout: 20_000 }); return (await (await page.waitForSelector("#app")).contentFrame())!; } test("the page's exchange with ws matches what Node recorded, through to the crash", async ({ page }) => { await appFrame(page, "?crash"); const app = page.frameLocator("#app"); await expect(app.locator("body")).toHaveAttribute("data-done", "true", { timeout: 20_000 }); expect((await app.locator("#log").textContent())!.split("\n")).toEqual(GOLDEN.log); await expect(page.locator("#exit")).toHaveText("1"); await expect(page.locator("#err")).toContainText("Error: crash requested"); }); type Sockets = Record; /** Open a socket from the app page, keep it on window, and return its first message. */ const openSocket = (frame: Frame, name: string) => frame.evaluate((name) => new Promise((resolve, reject) => { const ws = new WebSocket(`ws://${location.host}/live`); (window as unknown as Sockets)[name] = ws; ws.onmessage = (e) => resolve(String(e.data)); ws.onclose = (e) => reject(new Error(`closed ${e.code}`)); }), name); const roundTrip = (frame: Frame, name: string, text: string) => frame.evaluate(({ name, text }) => new Promise((resolve) => { const ws = (window as unknown as Sockets)[name]!; ws.onmessage = (e) => resolve(String(e.data)); ws.send(text); }), { name, text }); test("stopping the SW leaves an open socket open, and new sockets connect after revival", async ({ page, context }) => { const frame = await appFrame(page, "?manual"); expect(await openSocket(frame, "first")).toBe("welcome 1"); const cdp = await context.newCDPSession(page); await cdp.send("ServiceWorker.enable"); const stopped = new Promise((resolve) => { cdp.on("ServiceWorker.workerVersionUpdated", ({ versions }) => { if (versions.some((v: { runningStatus: string }) => v.runningStatus === "stopped")) resolve(); }); }); await cdp.send("ServiceWorker.stopAllWorkers"); await stopped; expect(await roundTrip(frame, "first", "still here")).toBe("echo:still here"); expect(await openSocket(frame, "second")).toBe("welcome 2"); expect(await roundTrip(frame, "second", "new")).toBe("echo:new"); }); test("an open socket keeps the process alive after server.close(), until it closes", async ({ page }) => { const frame = await appFrame(page, "?manual"); expect(await openSocket(frame, "only")).toBe("welcome 1"); await frame.evaluate(() => (window as unknown as Sockets).only!.send("shutdown")); await page.waitForTimeout(500); await expect(page.locator("#exit")).toHaveText("…"); const closed = await frame.evaluate(() => new Promise((resolve) => { const ws = (window as unknown as Sockets).only!; ws.onclose = (e) => resolve(`${e.code} ${e.wasClean}`); ws.close(1000); })); expect(closed).toBe("1000 true"); await expect(page.locator("#exit")).toHaveText("0"); });