import { authenticate, expect, roomPath, test } from "./room"; import { storedQuestion } from "../apps/server/src/testing/plan"; import type { WebSocketRoute } from "@playwright/test"; const WIDGET = "01K0N4TR8K7JGM4R1J7PW4R8YJ"; const QUESTION = "01K0N4V4E7Y6P4MJ5WD8XZF3B2"; const OPTION = "01K0N4W3B7P27CBAEC7A8C8WEA"; const DEFINITION = { questions: [{ id: QUESTION, header: "Rollout", question: "How should we deploy?", multiple: false, options: [{ id: OPTION, label: "Canary", description: "" }], }], }; const CARD = ` `; test("empty decisions remain reachable in a short viewport", async ({ join }) => { let page = await join("ana", { viewport: { width: 320, height: 260 } }); await page.getByRole("navigation", { name: "Workspace view" }) .getByRole("button", { name: /^Decisions/ }).click(); let view = page.locator('[data-document-view="decisions"]:visible'); await expect(page.locator(".workspace-document-layer.is-open").filter({ has: view })) .toHaveCSS("opacity", "1"); let scroller = view.locator("[data-plan-decisions-scroll]"); let panel = scroller.locator('[data-slot="empty-state"]'); await expect(panel).toBeVisible(); let top = await scroller.evaluate(element => ({ viewport: element.getBoundingClientRect().top, panel: element.querySelector('[data-slot="empty-state"]')!.getBoundingClientRect().top, })); expect(top.panel).toBeGreaterThanOrEqual(top.viewport); await scroller.evaluate(element => element.scrollTop = element.scrollHeight); let bottom = await scroller.evaluate(element => ({ viewport: element.getBoundingClientRect().bottom, panel: element.querySelector('[data-slot="empty-state"]')!.getBoundingClientRect().bottom, })); expect(bottom.panel).toBeLessThanOrEqual(bottom.viewport); }); test("a remembered Decisions view waits for the document before claiming it is empty", async ({ baseURL, page, room }) => { let opened = Promise.withResolvers(); let release: (() => void) | undefined; await page.addInitScript(() => localStorage.setItem("chopin:view:document", "decisions")); await page.routeWebSocket("**/ws?**", route => { let server = route.connectToServer(); route.onMessage(message => server.send(message)); server.onMessage(message => { if (typeof message === "string" && JSON.parse(message).kind === "plan:open" && !release) { release = () => route.send(message); opened.resolve(); return; } route.send(message); }); }); await authenticate(page, "ana", baseURL!); await page.goto(roomPath(room)); await opened.promise; let decisions = page.locator('[data-document-view="decisions"]:visible'); let announcement = decisions.getByRole("status"); await expect(announcement).toHaveText("Loading decisions"); let liveNode = await announcement.elementHandle(); await expect(decisions.getByText("Loading decisions…")).toBeVisible(); await expect(decisions.getByText("No decisions yet")).toHaveCount(0); release?.(); await expect(decisions.getByRole("heading", { name: "No decisions yet" })).toBeVisible(); await expect(announcement).toHaveText("No decisions yet"); expect(await announcement.evaluate((node, prior) => node === prior, liveNode)).toBe(true); }); test("an epoch reset hides stale cards until the replacement document is confirmed", async ({ baseURL, page, room, seed }) => { await seed(CARD, { revision: 1, questions: [{ id: WIDGET, definition: DEFINITION, status: "open", origin: "planner", history: [], optionOrigins: {}, editors: [], }], openQuestions: [{ definition: DEFINITION, id: WIDGET, model: storedQuestion(DEFINITION), revision: 0, widget: WIDGET, }], }); let browserRoute: WebSocketRoute | undefined; let holdNextOpen = false; let opened = Promise.withResolvers(); let release: (() => void) | undefined; await page.routeWebSocket("**/ws?**", route => { browserRoute = route; let server = route.connectToServer(); route.onMessage(message => server.send(message)); server.onMessage(message => { if (typeof message === "string" && JSON.parse(message).kind === "plan:open" && holdNextOpen) { holdNextOpen = false; release = () => route.send(message); opened.resolve(); return; } route.send(message); }); }); await authenticate(page, "ana", baseURL!); await page.goto(roomPath(room)); await page.getByRole("group", { name: "Document view" }) .getByRole("button", { name: /^Decisions/ }).click(); let decisions = page.locator('[data-document-view="decisions"]:visible'); let card = decisions.locator(`[data-plan-sidecar-questionnaire="${WIDGET}"]`); await expect(card).toBeVisible(); holdNextOpen = true; browserRoute?.send(JSON.stringify({ kind: "plan:reset", ts: 0, epoch: "fixture-replacement", reason: "replaced", })); await opened.promise; await expect(card).toHaveCount(0); await expect(decisions.getByRole("status")).toHaveText("Loading decisions"); release?.(); await expect(card).toBeVisible(); }); test("a rejected document open explains the failure and retries", async ({ baseURL, page, room }) => { let failed = false; await page.addInitScript(() => localStorage.setItem("chopin:view:document", "decisions")); await page.routeWebSocket("**/ws?**", route => { let server = route.connectToServer(); route.onMessage(message => server.send(message)); server.onMessage(message => { if (typeof message === "string") { let frame = JSON.parse(message) as { kind: string; rid?: string }; if (frame.kind === "plan:open" && !failed) { failed = true; route.send(JSON.stringify({ kind: "session:error", ts: 0, rid: frame.rid, message: "temporary fixture failure", })); return; } } route.send(message); }); }); await authenticate(page, "ana", baseURL!); await page.goto(roomPath(room)); let decisions = page.locator('[data-document-view="decisions"]:visible'); await expect(decisions.getByRole("heading", { name: "Decisions unavailable" })).toBeVisible(); await expect(decisions.getByRole("status")).toHaveText("Decisions unavailable"); await decisions.getByRole("button", { name: "Try again" }).click(); await expect(decisions.getByRole("heading", { name: "No decisions yet" })).toBeVisible(); }); test("a read-only reader reaches the confirmed empty state", async ({ baseURL, page, room }) => { await authenticate(page, "readonly", baseURL!); await page.goto(roomPath(room)); await page.getByRole("group", { name: "Document view" }) .getByRole("button", { name: /^Decisions/ }).click(); let decisions = page.locator('[data-document-view="decisions"]:visible'); await expect(decisions.getByRole("heading", { name: "No decisions yet" })).toBeVisible(); });