import { authenticate, content, expect, roomPath, test } from "./room"; import { createChannel, seedChannel } from "./database"; import { chatInput, expectChatValue, fillChat } from "./chat-input"; function channel(id: string, title: string, description?: string) { return { createdAt: "2026-08-19T12:00:00.000Z", createdBy: "U_ana", id, repositoryId: "R_score", repositoryName: "score", repositoryOwner: "octo-org", revision: 0, descriptionRevision: description ? 1 : 0, ...(description ? { description } : {}), slug: title.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, ""), title, updatedAt: "2026-08-19T12:00:00.000Z", }; } const repository = { defaultBranch: "main", fullName: "octo-org/score", id: "R_score", name: "score", owner: "octo-org", ownerAvatarUrl: "https://example.invalid/octo-org.png", permissions: { admin: false, pull: true, push: true }, private: true, url: "https://github.com/octo-org/score", }; function sidebar(page: import("@playwright/test").Page) { return page.getByRole("complementary", { name: "Projects" }); } function headerDocument(page: import("@playwright/test").Page) { return page.getByRole("banner").locator('[aria-label^="Document:"]'); } function headerActions(page: import("@playwright/test").Page) { return page.getByRole("banner").getByRole("button", { name: /^Actions for / }); } function documentRouteLayers(page: import("@playwright/test").Page, selector: string) { return page.locator(selector).filter({ has: page.getByRole("banner", { includeHidden: true }), }); } // Records visible route layers every frame; exit and entry are too brief to poll. async function recordRouteLayers(page: import("@playwright/test").Page) { await page.evaluate(() => { let samples: string[] = []; (window as Window & { __routeSamples?: string[] }).__routeSamples = samples; let sample = () => { let layers = document.querySelectorAll( ".document-route-swap > [data-content-swap-state]:not([hidden])", ); samples.push( [...layers].map(layer => `${layer.dataset.contentSwapState}${layer.hasAttribute("inert") ? ":inert" : ""}` ).join(" "), ); if (samples.length < 1200) requestAnimationFrame(sample); }; requestAnimationFrame(sample); }); } async function routeLayerSamples(page: import("@playwright/test").Page) { let samples = await page.evaluate(() => (window as Window & { __routeSamples?: string[] }).__routeSamples ?? [] ); // Two routes are never interactive at once. for (let sample of samples) { expect(sample.split(" ").filter(layer => layer && !layer.endsWith(":inert")).length) .toBeLessThanOrEqual(1); } return samples; } async function headerAction(page: import("@playwright/test").Page, action: string) { await headerActions(page).click(); await page.getByRole("menuitem", { name: action, exact: true }).click(); } test("a newly navigated parent document opens and submits its own comment composer", async ({ baseURL, join, seed }) => { await seed("Original parent document.\n"); let destination = crypto.randomUUID(); let destinationTitle = `Test ${destination.slice(0, 8)}`; let destinationText = "The destination parent document accepts a comment."; let databasePort = Number(new URL(baseURL!).port); await createChannel(databasePort, destination); await seedChannel(databasePort, destination, `${destinationText}\n`); let page = await join("ana"); await sidebar(page).getByRole("link", { name: destinationTitle, exact: true }).click(); await expect(headerDocument(page)).toHaveAccessibleName(`Document: ${destinationTitle}`); let routes = page.locator(".document-route-swap > [data-content-swap-state]:not([hidden])"); await expect(routes).toHaveCount(1); let active = page.locator( ".document-route-swap > [data-content-swap-state]:not([hidden]):not([inert])", ); let editor = active.getByRole("textbox", { name: "editable markdown" }); await editor.getByText(destinationText, { exact: true }).selectText(); await active.getByRole("button", { name: "Comment on this passage", exact: true }).click(); let draft = active.getByRole("dialog", { name: "New comment" }); await draft.getByPlaceholder("Comment on this passage…").fill("Keep this parent passage."); await draft.getByRole("button", { name: "Post comment", exact: true }).click(); await expect(active.getByRole("button", { name: /Comment on “The destination parent/ })) .toBeVisible(); }); test("document action menu motion follows its pointer trigger and survives interruption", async ({ join }) => { let page = await join("ana"); let trigger = headerActions(page); await trigger.click(); let menu = page.getByRole("menu", { name: /^Actions for / }); let retainedMenu = page.getByRole("menu", { includeHidden: true }); await expect(menu).toBeVisible(); let [triggerBox, menuLayout] = await Promise.all([ trigger.boundingBox(), menu.evaluate(element => { let menu = element as HTMLElement; return { height: menu.offsetHeight, left: menu.offsetLeft, origin: getComputedStyle(element).transformOrigin, top: menu.offsetTop, width: menu.offsetWidth, }; }), ]); expect(triggerBox).not.toBeNull(); let [originX, originY] = menuLayout.origin.split(" ").map(Number.parseFloat); let triggerX = triggerBox!.x + triggerBox!.width / 2; let triggerY = triggerBox!.y + triggerBox!.height / 2; expect(menuLayout.left + originX!).toBeCloseTo( Math.min(menuLayout.left + menuLayout.width, Math.max(menuLayout.left, triggerX)), 0, ); expect(menuLayout.top + originY!).toBeCloseTo( Math.min(menuLayout.top + menuLayout.height, Math.max(menuLayout.top, triggerY)), 0, ); await trigger.click(); await expect(retainedMenu).toHaveAttribute("aria-hidden", "true"); await expect(retainedMenu).toHaveAttribute("inert", ""); await expect(trigger).toBeFocused(); await trigger.click(); await expect(retainedMenu).not.toHaveAttribute("aria-hidden", "true"); await expect(retainedMenu).not.toHaveAttribute("inert", ""); await expect(retainedMenu).toHaveCount(1); await page.keyboard.press("Escape"); await expect(menu).toHaveCount(0); }); test("header actions menu is start-aligned to its trigger", async ({ join }) => { let page = await join("ana"); let trigger = headerActions(page); await trigger.click(); let menu = page.getByRole("menu", { name: /^Actions for / }); await expect(menu).toBeVisible(); let [triggerBox, layout] = await Promise.all([ trigger.boundingBox(), menu.evaluate(element => ({ left: (element as HTMLElement).offsetLeft, top: (element as HTMLElement).offsetTop, })), ]); expect(Math.abs(layout.left - triggerBox!.x)).toBeLessThanOrEqual(1); expect(layout.top).toBeGreaterThanOrEqual(triggerBox!.y + triggerBox!.height); }); test("account menu closes on Escape and outside click, and returns focus", async ({ join }) => { let page = await join("ana"); let account = sidebar(page).locator(".project-sidebar-account"); let menu = page.getByRole("menu").filter({ has: page.getByRole("menuitem", { name: "Sign out" }), }); await account.click(); await expect(menu).toBeVisible(); await page.keyboard.press("ArrowDown"); await expect(menu.getByRole("menuitem", { name: "Keyboard shortcuts" })).toBeFocused(); await page.keyboard.press("ArrowDown"); await expect(menu.getByRole("menuitem", { name: "Sign out" })).toBeFocused(); await page.keyboard.press("Escape"); await expect(menu).toHaveCount(0); await expect(account).toBeFocused(); await account.click(); await expect(menu).toBeVisible(); await page.getByRole("banner").click({ position: { x: 600, y: 10 } }); await expect(menu).toHaveCount(0); await expect(account).toHaveAttribute("aria-expanded", "false"); }); test("document action menu motion settles keyboard opening immediately", async ({ join }) => { let page = await join("ana"); let trigger = headerActions(page); await trigger.focus(); await trigger.press("ArrowDown"); let menu = page.getByRole("menu", { name: /^Actions for / }); await expect(page.getByRole("menuitem", { name: "Copy link", exact: true })).toBeFocused(); await expect(menu).toHaveCSS("transition-duration", "0s"); await page.keyboard.press("ArrowDown"); await expect(page.getByRole("menuitem", { name: "Rename", exact: true })).toBeFocused(); }); test("a pointer-collapsed Project stays inert through exit and restores in place", async ({ join }) => { let page = await join("ana"); let projects = sidebar(page); let trigger = projects.getByRole("button", { name: /^score(?:, \d+ unanswered decisions?)?$/ }); let body = projects.locator('[data-motion-disclosure="projects"]'); let bodyId = await body.getAttribute("id"); expect(bodyId).not.toBeNull(); await expect(trigger).toHaveAttribute("aria-controls", bodyId!); await trigger.click(); await expect(trigger).not.toHaveAttribute("aria-controls"); await expect(body).toHaveAttribute("aria-hidden", "true"); await expect(body).toHaveAttribute("inert", ""); await trigger.click(); await expect(trigger).toHaveAttribute("aria-controls", bodyId!); await expect(body).not.toHaveAttribute("aria-hidden", "true"); await expect(body).not.toHaveAttribute("inert", ""); await expect(body).toHaveCount(1); }); test("document action menu closes when placement cannot be measured", async ({ join }) => { let page = await join("ana"); let trigger = headerActions(page); await page.addStyleTag({ content: ".document-actions-menu { display: none !important; }" }); await trigger.click(); await expect(trigger).toHaveAttribute("aria-expanded", "false"); await expect(trigger).toBeFocused(); await expect(page.locator(".document-actions-menu")).toHaveCount(0); }); test("the room header renames the current document and the sidebar creates one immediately", async ({ join }) => { let page = await join("ana"); let header = page.getByRole("banner"); let trigger = headerActions(page); let projects = sidebar(page); await expect(headerDocument(page)).toBeVisible(); await expect(projects.locator('[aria-current="page"]')).toHaveCount(1); await expect(header.getByRole("button", { name: /planner session/i })).toHaveCount(0); await expect(trigger).toBeVisible(); await headerAction(page, "Rename"); let title = page.getByRole("textbox", { name: "Document title" }); await expect(title).toBeFocused(); await title.press("Escape"); await expect(header.getByRole("button", { name: /^Rename / })).toBeFocused(); await projects.getByRole("button", { name: "New document", exact: true }).click(); await expect(page).toHaveURL(/\/documents\/octo-org\/score\/[a-z]+-[a-z]+$/); await expect(headerDocument(page)).toHaveAccessibleName(/^Document: [a-z]+-[a-z]+$/); // A new document opens with its generated name selected, ready to be replaced. await expect(title).toBeFocused(); expect( await title.evaluate(field => { let input = field as HTMLInputElement; return input.selectionStart === 0 && input.selectionEnd === input.value.length; }), ).toBe(true); let name = `Named ${crypto.randomUUID().slice(0, 8)}`; await page.keyboard.type(name); await page.keyboard.press("Enter"); await expect(headerDocument(page)).toHaveAccessibleName(`Document: ${name}`); await expect(content(page)).toBeFocused(); await expect(projects.getByRole("link", { name, exact: true })).toBeVisible(); }); test("the header names the project without adding a sidebar control", async ({ join }) => { let page = await join("ana", { viewport: { width: 1440, height: 900 } }); let header = page.getByRole("banner"); let prefix = header.getByText("score", { exact: true }); await expect(prefix).toBeVisible(); await expect(header.getByRole("button", { name: "Show score in the sidebar" })).toHaveCount(0); await expect(header.getByRole("button", { name: /^Rename / })).toBeVisible(); await expect(header.getByRole("button", { name: /^Actions for / })).toBeVisible(); await sidebar(page).getByRole("button", { name: "Hide sidebar" }).click(); await expect(sidebar(page)).toHaveCount(0); await expect(prefix).toBeVisible(); }); test("the project prefix gives way on phones", async ({ join }) => { let page = await join("ana", { viewport: { width: 390, height: 844 } }); await expect(headerDocument(page)).toBeVisible(); await expect(page.getByRole("banner").getByText("score", { exact: true })).toBeHidden(); }); test("the header title renames in place with click, F2, Escape, and blur", async ({ join, room }) => { let page = await join("ana"); let header = page.getByRole("banner"); let field = page.getByRole("textbox", { name: "Document title" }); let title = `Inline ${room.slice(0, 8)}`; await header.getByRole("button", { name: /^Rename / }).click(); await expect(field).toBeFocused(); await field.fill("Discarded title"); await field.press("Escape"); await expect(field).toHaveCount(0); await expect(headerDocument(page)).not.toHaveAccessibleName("Document: Discarded title"); let button = header.getByRole("button", { name: /^Rename / }); await expect(button).toBeFocused(); await button.press("F2"); await expect(field).toBeFocused(); await field.fill(title); await page.getByRole("banner").click({ position: { x: 600, y: 10 } }); await expect(field).toHaveCount(0); await expect(headerDocument(page)).toHaveAccessibleName(`Document: ${title}`); await expect(header.getByRole("button", { name: `Rename ${title}`, exact: true })).toBeVisible(); }); test("typing straight after New document names it without losing a character", async ({ join }) => { let page = await join("ana"); let cdp = await page.context().newCDPSession(page); await cdp.send("Emulation.setCPUThrottlingRate", { rate: 4 }); try { for (let delay of [0, 10]) { // Sixteen characters typed while the document is still being created and opened. let name = `Quick${crypto.randomUUID().replaceAll("-", "").slice(0, 11)}`; await sidebar(page).getByRole("button", { name: "New document", exact: true }).click(); await page.keyboard.type(name, { delay }); await page.keyboard.press("Enter"); await expect(headerDocument(page)).toHaveAccessibleName(`Document: ${name}`); await expect(content(page)).not.toContainText(name.slice(0, 5)); } } finally { await cdp.send("Emulation.setCPUThrottlingRate", { rate: 1 }); } }); test("sidebar Rename opens that document with its title ready to edit", async ({ baseURL, join }) => { let other = crypto.randomUUID(); let otherTitle = `Test ${other.slice(0, 8)}`; await createChannel(Number(new URL(baseURL!).port), other); let page = await join("ana"); let projects = sidebar(page); await projects.getByRole("link", { name: otherTitle, exact: true }).hover(); await projects.getByRole("button", { name: `Actions for ${otherTitle}` }).click(); await page.getByRole("menuitem", { name: "Rename", exact: true }).click(); await expect(headerDocument(page)).toHaveAccessibleName(`Document: ${otherTitle}`); let field = page.getByRole("textbox", { name: "Document title" }); await expect(field).toBeFocused(); await expect(page.getByRole("dialog")).toHaveCount(0); let renamed = `Renamed ${other.slice(0, 8)}`; await field.fill(renamed); await field.press("Enter"); await expect(projects.getByRole("link", { name: renamed, exact: true })).toBeVisible(); }); test("leaving a rejected title reverts it without retrying", async ({ join, room }) => { let page = await join("ana"); let patches = 0; await page.route("**/api/channels/*", async route => { if (route.request().method() !== "PATCH") return route.continue(); patches += 1; await route.fulfill({ status: 409, json: { error: "a document with this title already exists" }, }); }); let before = await headerDocument(page).getAttribute("aria-label"); await page.getByRole("banner").getByRole("button", { name: /^Rename / }).click(); let field = page.getByRole("textbox", { name: "Document title" }); await field.fill(`Taken ${room.slice(0, 8)}`); await field.press("Enter"); await expect(page.getByRole("alert")).toHaveText( "A document with this title already exists. Try a different title.", ); await page.getByRole("banner").click({ position: { x: 600, y: 10 } }); await expect(field).toHaveCount(0); await expect(headerDocument(page)).toHaveAttribute("aria-label", before!); expect(patches).toBe(1); }); test("a pointer-dismissed navigation dialog releases focus while it exits", async ({ join }) => { let page = await join("ana"); let trigger = sidebar(page).getByRole("button", { name: "Search", exact: true }); await trigger.click(); await expect(page.getByRole("textbox", { name: "Search documents" })).toBeFocused(); let modal = page.getByRole("dialog", { includeHidden: true, name: "Search documents" }) .locator("../.."); await page.getByRole("button", { name: "Close Search documents" }).click({ position: { x: 8, y: 8 }, }); await expect(modal).toHaveAttribute("aria-hidden", "true"); await expect(modal).toHaveAttribute("inert", ""); await expect(trigger).toBeFocused(); await expect(modal).toHaveCount(0); }); test("sidebar titles and actions share row space when controls appear", async ({ join, page }) => { let title = "Complete the implementation"; let listed = channel("cccccccc-0000-4000-8000-000000000000", title); await page.route( "**/api/repositories/octo-org/score/channels*", route => route.fulfill({ json: { canEdit: true, channels: [listed], repository } }), ); page = await join("ana"); let projects = sidebar(page); let link = projects.getByRole("link", { name: title, exact: true }); let titleText = link.locator("span").last(); let row = link.locator(".."); let actions = projects.getByRole("button", { name: `Actions for ${title}` }); let clipped = () => titleText.evaluate(element => element.scrollWidth > element.clientWidth); await expect(actions).toBeHidden(); let restingWidth = await link.evaluate(element => element.clientWidth); expect(await clipped()).toBe(false); await row.hover(); await expect(actions).toBeVisible(); await expect.poll(() => link.evaluate(element => element.clientWidth)).toBeLessThan(restingWidth); let [rowBox, actionsBox] = await Promise.all([row.boundingBox(), actions.boundingBox()]); expect(rowBox!.x + rowBox!.width - actionsBox!.x - actionsBox!.width).toBeLessThanOrEqual(8); }); test("sidebar rows stay single-line and reveal descriptions beside the rail", async ({ join, page }) => { let title = "Release plan"; let description = "Coordinates the release readiness work across every team."; let listed = channel("cccccccc-0000-4000-8000-000000000000", title, description); await page.route( "**/api/repositories/octo-org/score/channels*", route => route.fulfill({ json: { canEdit: true, channels: [listed], repository } }), ); page = await join("ana"); let projects = sidebar(page); let link = projects.getByRole("link", { name: title, exact: true }); await expect(link).toHaveAccessibleDescription(description); await expect(projects.getByText(description)).toHaveCount(0); let row = link.locator(".."); expect((await row.boundingBox())!.height).toBe(30); let pencil = projects.getByRole("button", { name: "New document in score", exact: true }); await row.hover(); await expect(pencil).toHaveCSS("opacity", "0"); let card = page.locator("[data-icon-tooltip]"); await expect(card).toHaveText(description); await expect(card).toBeVisible(); let [cardBox, railBox] = await Promise.all([card.boundingBox(), projects.boundingBox()]); expect(cardBox!.x).toBeGreaterThanOrEqual(railBox!.x + railBox!.width); await pencil.locator("..").hover(); await expect(pencil).toHaveCSS("opacity", "1"); }); test("a tapped sidebar row does not open its description card", async ({ join }) => { let title = "Release plan"; let listed = channel("cccccccc-0000-4000-8000-000000000000", title, "Touch never shows this."); let page = await join("ana", { hasTouch: true, viewport: { width: 1280, height: 820 } }); await page.context().route( "**/api/repositories/octo-org/score/channels*", route => route.fulfill({ json: { canEdit: true, channels: [listed], repository } }), ); await page.reload(); let link = sidebar(page).getByRole("link", { name: title, exact: true }); await link.evaluate(element => element.addEventListener("click", event => { event.preventDefault(); event.stopPropagation(); }) ); await link.tap(); await expect(link).toBeFocused(); await page.waitForTimeout(800); await expect(page.locator("[data-icon-tooltip]")).toBeHidden(); }); test("a long description card stays inside the window beside a bottom row", async ({ join }) => { let description = Array.from({ length: 12 }, () => "Coordinates release readiness work.") .join(" "); let listed = Array.from( { length: 12 }, (_, index) => channel( `${String(index + 1).padStart(8, "0")}-0000-4000-8000-000000000000`, `Note ${index + 1}`, index === 11 ? description : undefined, ), ); let height = 420; let page = await join("ana", { viewport: { width: 1440, height } }); await page.context().route( "**/api/repositories/octo-org/score/channels*", route => route.fulfill({ json: { canEdit: true, channels: listed, repository } }), ); await page.reload(); let link = sidebar(page).getByRole("link", { name: "Note 12", exact: true }); // Scrolling hides tooltips, so settle the rail before hovering. await link.scrollIntoViewIfNeeded(); await page.evaluate(() => new Promise(resolve => requestAnimationFrame(() => requestAnimationFrame(resolve))) ); await link.hover(); let card = page.locator("[data-icon-tooltip]"); await expect(card).toBeVisible(); let box = (await card.boundingBox())!; expect(box.height).toBeGreaterThan(100); expect(box.y).toBeGreaterThanOrEqual(8); expect(box.y + box.height).toBeLessThanOrEqual(height - 8); }); test("a stale catalogue response cannot remove a newly created document", async ({ join, page }) => { let captured = Promise.withResolvers(); let release = Promise.withResolvers(); let intercepted = false; await page.route("**/api/repositories/octo-org/score/channels*", async route => { if (intercepted || route.request().method() !== "GET") { await route.continue(); return; } intercepted = true; let response = await route.fetch(); captured.resolve(); await release.promise; await route.fulfill({ response }); }); page = await join("ana"); await captured.promise; let projects = sidebar(page); await projects.getByRole("button", { name: "New document", exact: true }).click(); await expect(page).toHaveURL(/\/documents\/octo-org\/score\/[a-z]+-[a-z]+$/); let created = projects.locator('a[aria-current="page"]'); await expect(created).toBeVisible(); let createdTitle = (await created.textContent())!.trim(); await expect(projects.getByText("Loading more…", { exact: true })).toBeVisible(); release.resolve(); await expect(projects.getByText("Loading more…", { exact: true })).toHaveCount(0); await expect(projects.getByRole("link", { name: createdTitle, exact: true })) .toHaveAttribute("aria-current", "page"); await expect(projects.locator('a[aria-current="page"]')).toHaveCount(1); }); test("document switches preserve navigation state and avoid catalogue reloads", async ({ join, page }) => { let requested: Array<{ documentId?: string; method: string; path: string }> = []; page.on("request", request => { let method = request.method(); let path = new URL(request.url()).pathname; let body = method === "PATCH" && path === "/api/navigation" ? request.postDataJSON() as { documentId?: unknown } : undefined; requested.push({ ...(typeof body?.documentId === "string" ? { documentId: body.documentId } : {}), method, path, }); }); let latestVisit = () => requested.findLast(request => request.method === "PATCH" && request.path === "/api/navigation"); page = await join("ana"); let initialNavigationRequests = requested.filter(request => request.method === "GET" && request.path === "/api/navigation") .length; let projects = sidebar(page); let original = projects.locator('a[aria-current="page"]'); let originalTitle = (await original.textContent())!.trim(); let originalPath = await original.getAttribute("href"); expect(originalPath).toBeTruthy(); await projects.getByRole("button", { name: "New document", exact: true }).click(); await expect(page).toHaveURL(/\/documents\/octo-org\/score\/[a-z]+-[a-z]+$/); let created = projects.locator('a[aria-current="page"]'); let createdTitle = (await created.textContent())!.trim(); let createdPath = await created.getAttribute("href"); expect(createdPath).toBeTruthy(); await expect(headerDocument(page)).toHaveAccessibleName(`Document: ${createdTitle}`); await expect(documentRouteLayers(page, "[data-content-swap-state]:not([hidden])")) .toHaveCount(1); await expect(projects.getByRole("link", { name: originalTitle, exact: true })).toBeVisible(); expect( requested.filter(request => request.method === "GET" && request.path === "/api/navigation"), ).toHaveLength(initialNavigationRequests); await expect.poll(() => latestVisit()?.documentId).not.toBeUndefined(); let createdDocumentId = latestVisit()!.documentId!; let documentRoutePattern = "**/api/repositories/octo-org/score/documents/*"; let release = Promise.withResolvers(); await page.route(documentRoutePattern, async route => { await release.promise; await route.continue(); }); requested.length = 0; await page.evaluate(() => { (window as Window & { __chopinNavigationSentinel?: string }).__chopinNavigationSentinel = "preserved"; }); await projects.getByRole("link", { name: originalTitle, exact: true }).click(); let interactiveRoutes = documentRouteLayers( page, "[data-content-swap-state]:not([hidden]):not([inert])", ); await expect(page.getByText("Opening document…", { exact: true })).toBeHidden(); await expect(interactiveRoutes).toHaveCount(1); await expect(interactiveRoutes).toBeVisible(); await expect(headerDocument(page)).toHaveAccessibleName(`Document: ${createdTitle}`); await expect(documentRouteLayers(page, "[data-content-swap-state]:not([hidden])")) .toHaveCount(1); await expect(projects.getByRole("link", { name: createdTitle, exact: true })).toBeVisible(); expect( await page.evaluate(() => (window as Window & { __chopinNavigationSentinel?: string }).__chopinNavigationSentinel ), ).toBe("preserved"); await page.keyboard.press("Shift"); await recordRouteLayers(page); release.resolve(); let visibleRoutes = documentRouteLayers(page, "[data-content-swap-state]:not([hidden])"); let outgoingRoutes = documentRouteLayers( page, '[data-content-swap-state="outgoing"]:not([hidden])', ); await expect(headerDocument(page)).toHaveAccessibleName(`Document: ${originalTitle}`); await expect(visibleRoutes).toHaveCount(1); // The incoming route waits unseen and inert while the outgoing one leaves. expect(await routeLayerSamples(page)).toContain("outgoing:inert staged:inert"); await expect(headerDocument(page)).toHaveAccessibleName(`Document: ${originalTitle}`); await expect(page).toHaveURL(originalPath!); let createdLink = projects.getByRole("link", { name: createdTitle, exact: true }); await recordRouteLayers(page); await createdLink.click(); await expect(headerDocument(page)).toHaveAccessibleName(`Document: ${createdTitle}`); await expect(visibleRoutes).toHaveCount(1); expect(await routeLayerSamples(page)).toContain("outgoing:inert staged:inert"); await expect(page).toHaveURL(createdPath!); await page.unroute(documentRoutePattern); expect(requested.filter(request => request.path === "/api/session")).toHaveLength(0); expect( requested.filter(request => request.method === "GET" && request.path === "/api/navigation"), ).toHaveLength(0); await expect.poll(() => requested.filter(request => request.method === "PATCH" && request.path === "/api/navigation") .length ).toBe(2); expect(latestVisit()?.documentId).toBe(createdDocumentId); expect(requested.filter(request => request.path === "/api/repositories/octo-org/score/channels")) .toHaveLength(0); expect( requested.filter(request => request.path === "/api/repositories/octo-org/score/research-workspaces" ), ).toHaveLength(0); await page.evaluate(() => history.back()); await expect(headerDocument(page)).toHaveAccessibleName(`Document: ${originalTitle}`); await page.evaluate(() => history.forward()); await expect(headerDocument(page)).toHaveAccessibleName(`Document: ${createdTitle}`); await expect(visibleRoutes).toHaveCount(1); let keyboardRelease = Promise.withResolvers(); await page.route(documentRoutePattern, async route => { await keyboardRelease.promise; await route.continue(); }); let originalLink = projects.getByRole("link", { name: originalTitle, exact: true }); await originalLink.focus(); await page.keyboard.press("Enter"); await page.locator("body").dispatchEvent("pointerover", { pointerType: "mouse" }); await expect(headerDocument(page)).toHaveAccessibleName(`Document: ${createdTitle}`); keyboardRelease.resolve(); await expect(headerDocument(page)).toHaveAccessibleName(`Document: ${originalTitle}`); await expect(visibleRoutes).toHaveCount(1, { timeout: 100 }); await expect(outgoingRoutes).toHaveCount(0); }); test("rapid document switches and history jumps settle on one visible document", async ({ baseURL, join, seed }) => { await seed("Burst origin document.\n"); let databasePort = Number(new URL(baseURL!).port); let documents = await Promise.all([0, 1, 2, 3].map(async index => { let id = crypto.randomUUID(); await createChannel(databasePort, id); await seedChannel(databasePort, id, `Burst document ${index}.\n`); return { text: `Burst document ${index}.`, title: `Test ${id.slice(0, 8)}` }; })); let page = await join("ana"); let projects = sidebar(page); for (let document of documents) { await expect(projects.getByRole("link", { name: document.title, exact: true })).toBeVisible(); } let routes = page.locator(".document-route-swap > [data-content-swap-state]:not([hidden])"); let interactive = page.locator( ".document-route-swap > [data-content-swap-state]:not([hidden]):not([inert])", ); let settled = async (document: { text: string; title: string }) => { await expect(headerDocument(page)).toHaveAccessibleName(`Document: ${document.title}`); await expect(routes).toHaveCount(1); await expect(interactive).toHaveCount(1); await expect(interactive.getByText(document.text, { exact: true })).toBeVisible(); }; // Clicks land faster than a route can finish leaving. await page.evaluate(async titles => { for (let title of titles) { [...document.querySelectorAll("a")] .find(link => link.textContent?.trim() === title)!.click(); await new Promise(resolve => setTimeout(resolve, 80)); } }, documents.map(document => document.title)); await settled(documents[3]!); await page.evaluate(async () => { for (let step of [-1, 1, -1, -1]) { history.go(step); await new Promise(resolve => setTimeout(resolve, 60)); } }); await expect(page).toHaveURL(/\/documents\/octo-org\/score\//); let current = (await projects.locator('a[aria-current="page"]').textContent())!.trim(); await settled(documents.find(document => document.title === current)!); }); test("overlapping document workspaces keep IDs, ARIA targets, and focus instance-scoped", async ({ baseURL, join }) => { let page = await join("ana", { viewport: { width: 390, height: 844 } }); let created = crypto.randomUUID(); await createChannel(Number(new URL(baseURL!).port), created); let destination = roomPath(created); await page.locator("body").dispatchEvent("pointerover", { pointerType: "mouse" }); await page.evaluate(path => { history.pushState(null, "", path); dispatchEvent(new PopStateEvent("popstate")); }, destination); let routes = page.locator(".document-route-swap > [data-content-swap-state]:not([hidden])"); await expect(routes).toHaveCount(2); let audit = await routes.evaluateAll(async layers => { let current = layers.find(layer => !layer.hasAttribute("inert")); let decision = [...current!.querySelectorAll( 'nav[aria-label="Workspace view"] button', )].find(button => button.textContent?.includes("Decisions")); decision!.click(); await new Promise(resolve => requestAnimationFrame(() => resolve())); let roots = layers.flatMap( layer => [...layer.querySelectorAll(".workspace-root")], ); let ids = roots.flatMap(root => [...root.querySelectorAll("[id]")]) .map(element => element.id); let counts = new Map(); for (let id of ids) counts.set(id, (counts.get(id) ?? 0) + 1); let duplicateIds = [...counts].filter(([, count]) => count > 1).map(([id]) => id).sort(); let invalidReferences = roots.flatMap(root => [...root.querySelectorAll("[aria-controls], [aria-labelledby]")] .flatMap(element => ["aria-controls", "aria-labelledby"].flatMap(attribute => (element.getAttribute(attribute)?.split(/\s+/) ?? []).flatMap(id => document.querySelectorAll(`#${CSS.escape(id)}`).length === 1 ? [] : [`${attribute}:${id}`] ) ) ) ).sort(); let heading = current?.querySelector('[data-document-view="decisions"] h2'); return { activeDecisionFocused: document.activeElement === heading, duplicateIds, invalidReferences, workspaceCount: roots.length, }; }); expect(audit).toEqual({ activeDecisionFocused: true, duplicateIds: [], invalidReferences: [], workspaceCount: 2, }); }); test("the archive view refreshes catalogues without reopening the document", async ({ join, page }) => { let sockets = 0; await page.routeWebSocket("**/ws?**", route => { sockets++; route.connectToServer(); }); page = await join("ana"); let initialSockets = sockets; let path = page.url(); let catalogue = (endpoint: string, includeArchived: boolean) => page.waitForResponse(response => { let url = new URL(response.url()); return response.request().method() === "GET" && url.pathname === `/api/repositories/octo-org/score/${endpoint}` && (url.searchParams.get("includeArchived") === "true") === includeArchived; }); let projects = sidebar(page); let archived = catalogue("channels", true); await projects.getByRole("button", { name: "Archived", exact: true }).click(); await archived; expect(page.url()).toBe(path); expect(sockets).toBe(initialSockets); let active = catalogue("channels", false); await projects.getByRole("button", { name: "All documents", exact: true }).click(); await active; expect(page.url()).toBe(path); expect(sockets).toBe(initialSockets); }); test("the sidebar paginates documents and global search queries beyond the loaded page", async ({ join, page }) => { let requests: URL[] = []; let first = Array.from( { length: 2 }, (_, index) => channel( `${String(index + 1).padStart(8, "0")}-0000-4000-8000-000000000000`, `Note ${index + 1}`, index === 0 ? "Plan for note taking" : undefined, ), ); let searched = channel( "aaaaaaaa-0000-4000-8000-000000000000", "Search needle", "RFC about catalogue search", ); let continued = channel("bbbbbbbb-0000-4000-8000-000000000000", "Continued document"); await page.route("**/api/repositories/octo-org/score/channels*", async route => { let url = new URL(route.request().url()); requests.push(url); let query = url.searchParams.get("query"); let body = query === "needle" ? { canEdit: true, channels: [searched], repository } : url.searchParams.get("cursor") === "next" ? { canEdit: true, channels: [continued], repository } : { canEdit: true, channels: first, nextCursor: "next", repository }; await route.fulfill({ json: body }); }); page = await join("ana"); let projects = sidebar(page); await expect(projects.getByRole("link", { name: "Note 2", exact: true })).toBeVisible(); await expect(projects.getByRole("link", { name: "Note 1", exact: true })) .toHaveAccessibleDescription("Plan for note taking"); await projects.getByRole("button", { name: "Load more documents in score" }).click(); await expect(projects.getByRole("link", { name: "Continued document", exact: true })) .toBeVisible(); await projects.getByRole("button", { name: "Search", exact: true }).click(); let dialog = page.getByRole("dialog", { name: "Search documents" }); let search = dialog.getByRole("textbox", { name: "Search documents" }); await expect(search).toBeFocused(); await search.fill("needle"); await expect(dialog.getByRole("option", { name: /Search needle/ })).toBeVisible(); await expect(dialog.getByText("RFC about catalogue search", { exact: true })).toBeVisible(); expect(requests.some(url => url.searchParams.get("query") === "needle")).toBe(true); }); test("renaming the current document updates collaborators and survives reload", async ({ join, room }) => { let ana = await join("ana"); let bo = await join("bo"); let title = `Launch plan ${room.slice(0, 8)}`; let previousPath = roomPath(room); let renamedPath = `/documents/octo-org/score/${title.toLowerCase().replaceAll(" ", "-")}`; await ana.setViewportSize({ width: 320, height: 568 }); await headerAction(ana, "Rename"); let input = ana.getByRole("textbox", { name: "Document title" }); await expect(input).toBeFocused(); await input.fill(title); await input.press("Enter"); await expect(headerDocument(ana)).toHaveAccessibleName(`Document: ${title}`); await expect(headerDocument(bo)).toHaveAccessibleName(`Document: ${title}`); await expect(ana).toHaveURL(renamedPath); await expect(bo).toHaveURL(renamedPath); await ana.reload(); await expect(ana.getByRole("banner").locator(`[aria-label="Document: ${title}"]`)) .toBeVisible(); await ana.goto(previousPath); await expect(ana).toHaveURL(renamedPath); }); test("a delayed rename response cannot overwrite a newer collaborator rename", async ({ join, room }) => { let ana = await join("ana"); let bo = await join("bo"); let release = Promise.withResolvers(); let delay = true; await ana.route(`**/api/channels/${room}`, async route => { if (route.request().method() !== "PATCH" || !delay) return route.continue(); delay = false; let response = await route.fetch(); await release.promise; await route.fulfill({ response }); }); let first = `First rename ${room.slice(0, 8)}`; let latest = `Latest rename ${room.slice(0, 8)}`; await headerAction(ana, "Rename"); await ana.getByRole("textbox", { name: "Document title" }).fill(first); await ana.getByRole("textbox", { name: "Document title" }).press("Enter"); await expect(headerDocument(bo)).toHaveAccessibleName(`Document: ${first}`); await headerAction(bo, "Rename"); await bo.getByRole("textbox", { name: "Document title" }).fill(latest); await bo.getByRole("textbox", { name: "Document title" }).press("Enter"); await expect(headerDocument(ana)).toHaveAccessibleName(`Document: ${latest}`); release.resolve(); await expect(headerDocument(ana)).toHaveAccessibleName(`Document: ${latest}`); }); test("read-only visitors can browse documents and get creation guidance", async ({ baseURL, page, room }) => { await authenticate(page, "readonly", baseURL!); await page.goto(roomPath(room)); await expect(page.getByRole("banner")).toBeVisible(); await expect(page.getByRole("textbox", { name: "editable markdown" })).toHaveAttribute( "contenteditable", "false", ); await expect(headerActions(page)).toHaveCount(0); await expect(page.getByText("You have read-only access to this document.")).toBeVisible(); await expect(page.getByPlaceholder("Use @chopin to ask Chopin")).toHaveCount(0); await sidebar(page).getByRole("button", { name: "New document", exact: true }).click(); let creation = page.getByRole("dialog", { name: "New document", exact: true }); await expect( creation.getByText("You need write access to an available project to create a document."), ) .toBeVisible(); await expect(creation.getByRole("link", { name: "Manage repository access" })).toBeVisible(); await page.keyboard.press("Escape"); await sidebar(page).getByRole("button", { name: "Search", exact: true }).click(); await expect(page.getByRole("textbox", { name: "Search documents" })).toBeFocused(); }); test("document rename failures preserve the draft and can be retried", async ({ join, room }) => { let page = await join("ana"); let failed = true; await page.route("**/api/channels/*", async route => { if (route.request().method() === "PATCH" && failed) { failed = false; await route.fulfill({ status: 503, json: { error: "rename is unavailable" } }); return; } await route.continue(); }); let title = `Retry rename ${room.slice(0, 8)}`; await headerAction(page, "Rename"); let input = page.getByRole("textbox", { name: "Document title" }); await input.fill(title); await input.press("Enter"); await expect(page.getByRole("alert")).toBeVisible(); await expect(input).toHaveValue(title); await expect(input).toBeFocused(); await input.press("Enter"); await expect(headerDocument(page)).toHaveAccessibleName(`Document: ${title}`); }); test("archiving offers Undo and archived mode names itself", async ({ join, room }) => { let ana = await join("ana"); let title = `Test ${room.slice(0, 8)}`; let projects = sidebar(ana); await headerAction(ana, "Archive"); let notice = ana.getByRole("status").filter({ hasText: `Archived ${title}` }); await expect(notice).toBeVisible(); await expect(projects.getByRole("link", { name: title, exact: true })).toHaveCount(0); await notice.getByRole("button", { name: "Undo", exact: true }).click(); await expect(ana.getByRole("banner").getByText("Archived", { exact: true })).toHaveCount(0); await expect(projects.getByRole("link", { name: title, exact: true })).toBeFocused(); await headerAction(ana, "Archive"); await expect(notice).toBeVisible(); await expect(ana.getByRole("banner").getByRole("button", { name: /^Actions for / })) .toBeFocused(); await ana.keyboard.press("Shift+Tab"); let undo = notice.getByRole("button", { name: "Undo", exact: true }); await expect(undo).toBeFocused(); await ana.waitForTimeout(6000); await expect(notice).toBeVisible(); await ana.keyboard.press("Enter"); await expect(projects.getByRole("link", { name: title, exact: true })).toBeFocused(); await projects.getByRole("button", { name: "Archived", exact: true }).click(); await expect(projects.getByRole("navigation", { name: "Archived documents" })).toBeVisible(); await expect(projects.getByText("Projects", { exact: true })).toHaveCount(0); }); test("writers can archive, restore, and permanently delete a document", async ({ join, room }) => { let ana = await join("ana"); let bo = await join("bo"); let title = `Test ${room.slice(0, 8)}`; let path = roomPath(room); let projects = sidebar(ana); await headerAction(ana, "Archive"); await expect(ana.getByRole("banner").getByText("Archived", { exact: true })).toBeVisible(); await expect(bo.getByRole("banner").getByText("Archived", { exact: true })).toBeVisible(); await expect(ana.getByText("Archived. Restore it to keep chatting.")) .toBeVisible(); await expect(ana.getByPlaceholder("Use @chopin to ask Chopin")).toHaveCount(0); await expect(content(ana)).toHaveAttribute("contenteditable", "false"); await expect(content(bo)).toHaveAttribute("contenteditable", "false"); await expect(projects.getByRole("link", { name: title, exact: true })).toHaveCount(0); await bo.reload(); await expect(bo).toHaveURL(path); await expect(content(bo)).toHaveAttribute("contenteditable", "false"); await projects.getByRole("button", { name: "Archived", exact: true }).click(); let back = projects.getByRole("button", { name: "All documents", exact: true }); await expect(back).toBeFocused(); await expect(projects.getByRole("link", { name: title, exact: true })).toBeVisible(); await back.click(); let archivedButton = projects.getByRole("button", { name: "Archived", exact: true }); await expect(archivedButton).toBeFocused(); await archivedButton.click(); await ana.getByRole("banner").getByRole("button", { name: "Restore", exact: true }).click(); await expect(content(ana)).toHaveAttribute("contenteditable", "true"); await expect(content(bo)).toHaveAttribute("contenteditable", "true"); await expect(ana.getByRole("banner").getByText("Archived", { exact: true })).toHaveCount(0); await expect(chatInput(ana)).toBeVisible(); await expect(projects.getByRole("button", { name: "Archived", exact: true })).toBeVisible(); await expect(projects.getByRole("link", { name: title, exact: true })).toBeVisible(); await headerAction(ana, "Archive"); await expect(content(ana)).toHaveAttribute("contenteditable", "false"); await headerAction(ana, "Delete permanently"); let confirmation = ana.getByRole("dialog", { name: "Delete document permanently?" }); await confirmation.getByRole("button", { name: "Delete permanently", exact: true }).click(); await expect(ana).not.toHaveURL(path); await expect(bo).not.toHaveURL(path); let unavailable = await ana.request.get(`/api/channels/${room}`); expect(unavailable.status()).toBe(404); }); test( "navigation fetch failures reserve space and retry without losing the chat draft", async ({ join, page, seed }, testInfo) => { await seed( "# A calmer document\n\nRetrying navigation keeps the document and draft available.\n", ); await page.setViewportSize({ width: 1452, height: 895 }); let failed = false; await page.route("**/api/navigation", async route => { if (route.request().method() === "GET" && !failed) { failed = true; await route.abort("failed"); return; } await route.continue(); }); page = await join("ana"); let notice = page.getByRole("alert").filter({ hasText: "Could not reach Chopin." }); await expect(notice).toBeVisible(); let [noticeBox, headerBox] = await Promise.all([ notice.boundingBox(), page.getByRole("banner").boundingBox(), ]); expect(headerBox!.y).toBeGreaterThanOrEqual(noticeBox!.y + noticeBox!.height); let draft = chatInput(page); await fillChat(draft, "Keep this draft while retrying navigation."); await page.screenshot({ path: testInfo.outputPath("navigation-warning.png") }); await notice.getByRole("button", { name: "Try again", exact: true }).click(); await expect(notice).toHaveCount(0); await expectChatValue(draft, "Keep this draft while retrying navigation."); await expect(sidebar(page).getByRole("link").and(page.locator('[aria-current="page"]'))) .toBeVisible(); }, ); test("sidebar creation failures remain retryable", async ({ join, page }) => { let creationFailed = true; await page.route("**/api/repositories/octo-org/score/channels*", async route => { if (route.request().method() === "POST" && creationFailed) { creationFailed = false; await route.fulfill({ status: 503, json: { error: "creation is unavailable" } }); return; } await route.continue(); }); page = await join("ana"); let create = sidebar(page).getByRole("button", { name: "New document", exact: true }); await create.click(); await expect(page.getByRole("alert")).toBeVisible(); await expect(create).toBeEnabled(); await create.click(); await expect(page).toHaveURL(/\/documents\/octo-org\/score\/[a-z]+-[a-z]+$/); }); test("archiving a sidebar row by keyboard moves focus to a neighbouring row", async ({ baseURL, join }) => { let page = await join("ana"); let port = Number(new URL(baseURL!).port); await createChannel(port, crypto.randomUUID()); await createChannel(port, crypto.randomUUID()); await page.reload(); let links = sidebar(page).locator(".project-sidebar-document-link"); await expect.poll(() => links.count()).toBeGreaterThanOrEqual(3); let names = (await links.allInnerTexts()).map(name => name.trim()); let index = await links.evaluateAll(elements => elements.findIndex(element => element.getAttribute("aria-current") !== "page") ); let neighbour = names[index + 1] ?? names[index - 1]; let row = links.nth(index).locator( "xpath=ancestor::div[contains(@class,'project-sidebar-document')][1]", ); let trigger = row.getByRole("button", { name: /^Actions for / }); await row.hover(); await trigger.focus(); await trigger.press("ArrowDown"); await expect(page.getByRole("menuitem", { name: "Copy link", exact: true })).toBeFocused(); await page.keyboard.press("ArrowDown"); await page.keyboard.press("ArrowDown"); await expect(page.getByRole("menuitem", { name: "Archive", exact: true })).toBeFocused(); await page.keyboard.press("Enter"); await expect(links).toHaveCount(names.length - 1); await expect(sidebar(page).locator(".project-sidebar-document-link", { hasText: neighbour! })) .toBeFocused(); });