import { expect, test } from "@playwright/test" import { api, apiPage, deleteAll } from "./utils/api" /** * One socket serves the whole shell, and it has to survive the routes under it * coming and going. * * It used to belong to whichever component's effect ran first, which on Home is * the brain graph rather than the shell around it. Leaving Home closed the * socket while the shell held the reference count above zero, and the page was * deaf for the rest of its life: no live values, no pulses, and nothing that * would ever reconnect. */ const flowName = `test_live_${Date.now().toString(36)}` test.use({ storageState: "playwright/.auth/user.json" }) test.beforeAll(async ({ browser }) => { const page = await apiPage(browser) await api(page, `/flows/${flowName}`, { method: "PUT", data: { name: flowName, title: "Live", nodes: [ { id: "source", type: "python", provides: [{ name: "reading", dtype: "float" }], }, { id: "sink", type: "python", requires: [{ name: "reading", dtype: "float" }], }, ], }, }) const detail = await (await api(page, `/flows/${flowName}`)).json() await api(page, `/flows/${flowName}/publish`, { method: "POST", data: { version: detail.definition.version }, }) await page.close() }) test.afterAll(async ({ browser }) => { await deleteAll(browser, [`/flows/${flowName}`]) }) test("the live socket survives leaving Home and coming back", async ({ page, }) => { await page.addInitScript(() => { const Native = window.WebSocket const opened: WebSocket[] = [] ;(window as unknown as { __sockets: WebSocket[] }).__sockets = opened window.WebSocket = class extends Native { constructor(url: string | URL, protocols?: string | string[]) { super(url, protocols) opened.push(this) } } }) await page.goto("/") const neuron = page.locator(".brain-cell").first() await neuron.waitFor() // The full-bleed canvas is its own shell, so a trip through it takes the // sidebar shell down to nothing: coming back mounts the shell and the brain // graph in the same commit, which is the order that used to pick an owner. await page.goto(`/flows/${flowName}`) const bar = page.locator('[data-sidebar="sidebar"]') await bar.getByRole("link", { name: "Home", exact: true }).click() await neuron.waitFor() // And this is the navigation that used to end it: a sibling route in the // same shell, which unmounts the graph but not the shell. await bar.getByRole("link", { name: "Flows", exact: true }).click() await page.waitForURL(/\/flows$/) await bar.getByRole("link", { name: "Home", exact: true }).click() await neuron.waitFor() await expect .poll(() => page.evaluate(() => (window as unknown as { __sockets: WebSocket[] }).__sockets.some( (socket) => socket.readyState === 1, ), ), ) .toBe(true) })