// 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");
});