import { chatInput } from "./chat-input"; import { seedChildChannel } from "./database"; import { installPointerMedia } from "./pointer-media"; import { recordRoomMounts } from "./room-mounts"; import { authenticate, expect, test } from "./room"; import type { Chat } from "../packages/protocol/index"; import type { Page } from "@playwright/test"; const PARENT_SOURCE = `# Parent document ${Array.from({ length: 36 }, (_, index) => `Parent passage ${index + 1}.`).join("\n\n")} `; const CHILD_SOURCE = `# Source review This ordinary child has its own editable document, Decisions, and inline comments. `; const LINKED_PARENT_SOURCE = `# Parent document [Parent source](https://example.com/parent-source) `; const LINKED_CHILD_SOURCE = `# Source review [Child source](https://example.com/child-source) `; function port(baseURL: string): number { return Number(new URL(baseURL).port); } test("authored links open from parent and child documents", async ({ baseURL, join, page, room, seed }) => { await page.context().route( "https://example.com/**", route => route.fulfill({ contentType: "text/html", body: "Authored source" }), ); await seed(LINKED_PARENT_SOURCE); let child = await seedChildChannel( port(baseURL!), room, crypto.randomUUID(), `Linked child ${room.slice(0, 8)}`, LINKED_CHILD_SOURCE, ); await join("ana"); let parent = page.locator(`[data-workspace-room="${room}"]`); let parentLink = parent.getByRole("link", { name: "Parent source", exact: true }); // A click shows where the link goes; opening it is one more deliberate step. await parentLink.click(); let parentPopupPromise = page.waitForEvent("popup"); await page.getByRole("dialog", { name: "Link" }).getByRole("button", { name: "Open" }).click(); let parentPopup = await parentPopupPromise; await expect(parentPopup).toHaveURL("https://example.com/parent-source"); expect(await parentPopup.evaluate(() => opener === null)).toBe(true); await parentPopup.close(); let sidebar = page.getByRole("complementary", { name: "Projects" }); await sidebar.getByRole("link", { name: `Linked child ${room.slice(0, 8)}`, exact: true }) .click(); let surface = page.getByRole("region", { name: `Child document: Linked child ${room.slice(0, 8)}`, }); await expect(surface.locator(`[data-workspace-room="${child.id}"]`)).toBeVisible(); let childLink = surface.getByRole("link", { name: "Child source", exact: true }); await childLink.click(); let childPopupPromise = page.waitForEvent("popup"); await page.getByRole("dialog", { name: "Link" }).getByRole("button", { name: "Open" }).click(); let childPopup = await childPopupPromise; await expect(childPopup).toHaveURL("https://example.com/child-source"); await childPopup.close(); }); async function captureChatSends(page: Page) { let sends: { channelId: string; text: string; to: Chat.Destination }[] = []; await page.route("**/api/session", async route => { let response = await route.fetch(); let session = await response.json() as Record; await route.fulfill({ response, json: { ...session, agent: true } }); }); await page.routeWebSocket("**/ws?**", route => { let server = route.connectToServer(); let channelId = new URL(route.url()).searchParams.get("channel"); route.onMessage(message => { if (typeof message === "string" && channelId) { try { let frame = JSON.parse(message) as Partial; if ( frame.kind === "chat:send" && typeof frame.text === "string" && (frame.to === "room" || frame.to === "planner") ) sends.push({ channelId, text: frame.text, to: frame.to }); } catch { // The real server remains authoritative for malformed and non-chat frames. } } server.send(message); }); server.onMessage(message => route.send(message)); }); return () => sends.map(send => ({ ...send })); } test("a parent-owned child keeps the parent chrome and nested geometry", async ({ baseURL, join, page, room, seed }) => { await seed(PARENT_SOURCE); let childTitle = `Chrome source ${room.slice(0, 8)}`; let child = await seedChildChannel( port(baseURL!), room, crypto.randomUUID(), childTitle, CHILD_SOURCE, ); // A child inherits the parent's Chat state; closed here so the child toolbar shows its toggle. await page.addInitScript(() => localStorage.setItem("chopin:pane:chat:choice", "false")); await join("ana"); await page.setViewportSize({ width: 1920, height: 1080 }); let parent = page.locator(`[data-workspace-room="${room}"]`); let sidebar = page.getByRole("complementary", { name: "Projects" }); let childLink = sidebar.getByRole("link", { name: childTitle, exact: true }); await childLink.focus(); await childLink.click(); let surface = page.getByRole("region", { name: `Child document: ${childTitle}` }); await expect(page).toHaveURL(url => url.pathname === child.path); await expect(surface.locator(`[data-workspace-room="${child.id}"]`)).toBeVisible(); let parentHeader = parent.locator(".room-header"); let parentPaper = parent.locator(".workspace-frame"); await expect(page.locator(".room-header:visible")).toHaveCount(1); await expect(parentHeader.getByRole("button", { name: `Back to Test ${room.slice(0, 8)}` })) .toHaveCount(0); await expect(parentHeader.getByRole("button", { name: `Return to Test ${room.slice(0, 8)}` })) .toBeVisible(); await expect(parentHeader.getByText(childTitle, { exact: true })).toBeVisible(); await expect(parentHeader.getByRole("group", { name: /People here:/ })).toBeVisible(); await expect(parentPaper).toHaveAttribute("inert", ""); await expect(parentPaper).toHaveAttribute("aria-hidden", "true"); await expect(parentPaper).toHaveCSS("filter", "none"); await expect(parentPaper).toHaveCSS("opacity", "1"); await expect.poll(() => page.locator(".anchored-child-parent").evaluate( element => getComputedStyle(element, "::after").opacity, ) ).toBe("1"); await expect(surface.locator(".room-header")).toBeHidden(); await surface.evaluate(async element => { await Promise.all(element.getAnimations().map(animation => animation.finished)); }); let parentBox = await parentPaper.boundingBox(); let childBox = await surface.boundingBox(); let headerBox = await parentHeader.boundingBox(); expect(parentBox).not.toBeNull(); expect(childBox).not.toBeNull(); expect(headerBox).not.toBeNull(); expect(childBox!.x).toBeGreaterThan(parentBox!.x); expect(childBox!.y).toBeGreaterThan(parentBox!.y); expect(childBox!.x + childBox!.width).toBeLessThan(parentBox!.x + parentBox!.width); expect(childBox!.y + childBox!.height).toBeLessThan(parentBox!.y + parentBox!.height); expect(childBox!.y).toBeGreaterThanOrEqual(headerBox!.y + headerBox!.height + 12); expect(await surface.evaluate(element => getComputedStyle(element).boxShadow)).not.toBe("none"); let childClose = surface.getByRole("button", { name: `Close ${childTitle}`, exact: true }); let childChatToggle = surface.getByRole("button", { name: "Show chat", exact: true, }); await expect(childChatToggle).toBeVisible(); let closeHandle = await childClose.elementHandle(); expect(closeHandle).not.toBeNull(); expect( await childChatToggle.evaluate( (toggle, close) => { let toolbar = toggle.closest("[data-document-toolbar]"); return toolbar !== null && toolbar === close?.closest("[data-document-toolbar]"); }, closeHandle, ), ).toBe(true); let chatToggleBox = await childChatToggle.boundingBox(); let childCloseBox = await childClose.boundingBox(); expect(chatToggleBox).not.toBeNull(); expect(childCloseBox).not.toBeNull(); expect(chatToggleBox!.x + chatToggleBox!.width) .toBeLessThanOrEqual(childCloseBox!.x); let resize = page.getByRole("separator", { name: "Resize Projects sidebar" }); let resizeBox = await resize.boundingBox(); let sidebarWidth = Number(await resize.getAttribute("aria-valuenow")); expect(resizeBox).not.toBeNull(); await page.mouse.move(resizeBox!.x + resizeBox!.width - 0.5, resizeBox!.y + 80); await page.mouse.down(); await page.mouse.move(resizeBox!.x + resizeBox!.width + 31.5, resizeBox!.y + 80); await page.mouse.up(); await expect(resize).toHaveAttribute("aria-valuenow", String(sidebarWidth + 32)); await expect(surface).toBeVisible(); await childClose.click(); await expect(surface).toHaveCount(0); await expect(childLink).toBeFocused(); }); test("a child isolates chat and decisions across every parent-owned close path", async ({ baseURL, join, page, room, seed }) => { test.slow(); await seed(PARENT_SOURCE); let childTitle = `Isolated source ${room.slice(0, 8)}`; let child = await seedChildChannel( port(baseURL!), room, crypto.randomUUID(), childTitle, CHILD_SOURCE, ); let chatSends = await captureChatSends(page); await page.setViewportSize({ width: 1920, height: 1080 }); await join("ana"); let parent = page.locator(`[data-workspace-room="${room}"]`); let parentChat = parent.getByRole("complementary", { name: "Chat", includeHidden: true, }); let parentRoomMessage = `Parent room message ${room.slice(0, 8)}`; let parentDraft = chatInput(parentChat); await parentDraft.fill(parentRoomMessage); await parentChat.getByRole("button", { name: "Send message" }).click(); await expect(parentChat.getByText(parentRoomMessage, { exact: true })).toBeVisible(); let parentScroll = parent.locator("[data-plan-scroll]"); await parentScroll.evaluate(element => element.scrollTop = 180); let parentScrollTop = await parentScroll.evaluate(element => element.scrollTop); let sidebar = page.getByRole("complementary", { name: "Projects" }); let childLink = sidebar.getByRole("link", { name: childTitle, exact: true }); await childLink.focus(); await childLink.click(); let surface = page.getByRole("region", { name: `Child document: ${childTitle}` }); let parentHeader = parent.locator(".room-header"); let childClose = surface.getByRole("button", { name: `Close ${childTitle}`, exact: true }); let childChat = surface.getByRole("complementary", { name: "Chat" }); // The parent has Chat open, so the child opens with its own Chat open too. await expect(childChat).toBeVisible(); await expect(childChat.getByText("Discuss this report here.", { exact: false })).toBeVisible(); await expect(childChat).not.toContainText(parentRoomMessage); await expect(parentChat).toContainText(parentRoomMessage); let childRoomMessage = `Child room message ${room.slice(0, 8)}`; let childPlannerMessage = `@chopin Child Planner message ${room.slice(0, 8)}`; let childPlannerTranscript = childPlannerMessage.replace("@chopin ", ""); let childDraft = chatInput(childChat); await childDraft.fill(childRoomMessage); await childChat.getByRole("button", { name: "Send message" }).click(); await expect(childChat.getByText(childRoomMessage, { exact: true })).toBeVisible(); await expect.poll(chatSends).toContainEqual({ channelId: child.id, text: childRoomMessage, to: "room", }); await childDraft.fill(childPlannerMessage); await childChat.getByRole("button", { name: "Send message" }).click(); await expect(childChat.getByText(childPlannerTranscript, { exact: true })).toBeVisible(); await expect.poll(chatSends).toContainEqual({ channelId: child.id, text: childPlannerMessage, to: "planner", }); await expect(parentChat).not.toContainText(childRoomMessage); await expect(parentChat).not.toContainText(childPlannerTranscript); await childChat.getByRole("button", { name: "Hide chat", exact: true, }).click(); await expect(childChat).toBeHidden(); let childEditor = surface.getByRole("textbox", { name: "editable markdown" }); await childEditor.locator("p").first().selectText(); await page.getByRole("button", { name: "Comment on this passage", exact: true }) .evaluate(button => (button as HTMLButtonElement).click()); let commentDraft = page.getByRole("dialog", { name: "New comment" }); await expect(commentDraft.getByPlaceholder("Comment on this passage…")).toBeFocused(); await commentDraft.getByRole("button", { name: "Close comment" }).click(); await expect(commentDraft).toHaveCount(0); await surface.getByRole("button", { name: "Decisions", exact: true }).click(); await expect(surface.locator('[data-document-view="decisions"]')).toBeVisible(); await expect(parent).toContainText("Parent passage 36."); await childClose.evaluate(button => { (button as HTMLButtonElement).click(); (button as HTMLButtonElement).click(); }); await expect(surface).toHaveCount(0); await expect(childLink).toBeFocused(); await page.waitForTimeout(250); await expect(page).toHaveURL(url => url.pathname === child.path.replace(/\/children\/.*$/, "")); await expect.poll(() => parentScroll.evaluate(element => element.scrollTop)).toBe( parentScrollTop, ); await expect(parentChat).toContainText(parentRoomMessage); await childLink.click(); await expect(surface).toBeVisible(); // Closing the child's Chat is not saved; it reopens with the parent's state. await expect(childChat).toBeVisible(); await expect(childChat).not.toContainText(parentRoomMessage); await expect(parentChat).toContainText(parentRoomMessage); await expect(childChat.getByText(childRoomMessage, { exact: true })).toBeVisible(); await expect(childChat.getByText(childPlannerTranscript, { exact: true })).toBeVisible(); await childChat.getByRole("button", { name: "Hide chat", exact: true, }).click(); await parentHeader.getByRole("button", { name: `Return to Test ${room.slice(0, 8)}` }).click(); await expect(surface).toHaveCount(0); await expect(childLink).toBeFocused(); await childLink.click(); await expect(surface).toBeVisible(); await parentHeader.getByRole("button", { name: `Return to Test ${room.slice(0, 8)}` }).click(); await expect(surface).toHaveCount(0); await expect(childLink).toBeFocused(); await childLink.click(); await expect(surface).toBeVisible(); await surface.getByRole("button", { name: "Document", exact: true }).click(); let headerBox = await parentHeader.boundingBox(); let surfaceBox = await surface.boundingBox(); let backdrop = page.locator("[data-child-backdrop]"); let backdropBox = await backdrop.boundingBox(); let sidebarBox = await sidebar.boundingBox(); expect(headerBox).not.toBeNull(); expect(surfaceBox).not.toBeNull(); expect(backdropBox).not.toBeNull(); expect(sidebarBox).not.toBeNull(); expect(backdropBox!.x).toBeGreaterThanOrEqual(sidebarBox!.x + sidebarBox!.width); expect(backdropBox!.y).toBeGreaterThanOrEqual(headerBox!.y + headerBox!.height); await page.mouse.click(surfaceBox!.x - 6, surfaceBox!.y + 20); await expect(surface).toHaveCount(0); await expect(childLink).toBeFocused(); await childLink.click(); await expect(surface).toBeVisible(); await page.keyboard.press("Escape"); await expect(surface).toHaveCount(0); await expect(childLink).toBeFocused(); await childLink.click(); await expect(surface).toBeVisible(); await page.goBack(); await expect(surface).toHaveCount(0); await expect(childLink).toBeFocused(); }); test("an in-app child preserves and restores its mounted parent", async ({ baseURL, join, page, room, seed }) => { await seed(PARENT_SOURCE); let childTitle = `Source review ${room.slice(0, 8)}`; let child = await seedChildChannel( port(baseURL!), room, crypto.randomUUID(), childTitle, CHILD_SOURCE, ); await page.addInitScript(() => { localStorage.setItem("chopin:pane:chat:choice", "true"); localStorage.setItem("chopin:pane:chat", "384"); }); await join("ana"); let parent = page.locator(`[data-workspace-room="${room}"]`); let parentChat = parent.getByRole("complementary", { name: "Chat", includeHidden: true, }); await expect(parent.getByRole("button", { name: "Background Work", exact: true })).toHaveCount( 0, ); let parentScroll = parent.locator("[data-plan-scroll]"); await expect(parent.getByRole("separator", { name: "Resize chat" })) .toHaveAttribute("aria-valuenow", "384"); await expect(parentChat).toBeVisible(); await page.evaluate(() => localStorage.setItem("chopin:view:document", "decisions")); await parent.evaluate(element => element.setAttribute("data-mount-token", "preserved")); await parentScroll.evaluate(element => element.scrollTop = 240); let originalScroll = await parentScroll.evaluate(element => element.scrollTop); await page.evaluate(() => history.replaceState({ navigation: 4 }, "", "?view=plan#note")); let childLink = page.getByRole("complementary", { name: "Projects" }) .getByRole("link", { name: childTitle, exact: true }); await childLink.focus(); await childLink.click(); let surface = page.getByRole("region", { name: `Child document: ${childTitle}` }); await expect(page).toHaveURL(url => url.pathname === child.path); await expect(surface).toBeVisible(); await expect(surface).toBeFocused(); await expect(parent).toHaveAttribute("data-mount-token", "preserved"); await expect(parent.locator(".workspace-frame")).toHaveAttribute("inert", ""); await expect(parent.locator(".workspace-frame")).toHaveAttribute("aria-hidden", "true"); let childChatToggle = surface.getByRole("button", { name: "Show chat", exact: true, }); let childChat = surface.getByRole("complementary", { name: "Chat" }); await expect(childChat).toBeVisible(); await expect(parentChat).toBeVisible(); await expect(surface.getByRole("button", { name: "Decisions", exact: true })).toBeVisible(); await expect(surface.getByRole("button", { name: "Background Work", exact: true })).toHaveCount( 0, ); await expect(surface.getByRole("button", { name: "Tasks & Progress", exact: true })).toHaveCount( 0, ); await expect(surface.locator(`[data-workspace-room="${child.id}"]`)).toBeVisible(); await expect(surface.locator('[data-document-view="plan"]')).toBeVisible(); await expect(surface.locator('[data-document-view="decisions"]')).toBeHidden(); await childChat.getByRole("button", { name: "Hide chat", exact: true, }).click(); await expect(childChat).toBeHidden(); await expect(parentChat).toBeVisible(); await surface.getByRole("button", { name: "Decisions", exact: true }).click(); await expect(surface.locator('[data-document-view="decisions"]')).toBeVisible(); await expect(parent.locator('[data-document-view="decisions"]')).toBeHidden(); await surface.getByRole("button", { name: "Document", exact: true }).click(); await expect(childChatToggle).toBeVisible(); await page.keyboard.press("Escape"); await expect(page).toHaveURL(url => url.pathname.endsWith(`/test-${room.slice(0, 8)}`) && url.search === "?view=plan" && url.hash === "#note" ); await expect(surface).toHaveCount(0); await expect(childLink).toBeFocused(); await expect.poll(() => parentScroll.evaluate(element => element.scrollTop)).toBe(originalScroll); await expect(parentChat).toBeVisible(); await childLink.click(); await expect(surface).toBeVisible(); await expect(childChat).toBeVisible(); await expect(parentChat).toBeVisible(); await page.goBack(); await expect(surface).toHaveCount(0); await expect(childLink).toBeFocused(); await childLink.click(); await expect(surface).toBeVisible(); await parent.locator(".room-header") .getByRole("button", { name: `Return to Test ${room.slice(0, 8)}` }).click(); await expect(surface).toHaveCount(0); await expect(childLink).toBeFocused(); }); test("an in-app child opens and submits its own comment composer", async ({ baseURL, join, page, room, seed }) => { await seed(PARENT_SOURCE); let childTitle = `Commentable child ${room.slice(0, 8)}`; let passage = "This ordinary child has its own editable document, Decisions, and Chat."; await seedChildChannel( port(baseURL!), room, crypto.randomUUID(), childTitle, `# Source review\n\n${passage}\n`, ); await join("ana"); await page.getByRole("complementary", { name: "Projects" }) .getByRole("link", { name: childTitle, exact: true }) .click(); let surface = page.getByRole("region", { name: `Child document: ${childTitle}` }); await surface.getByRole("textbox", { name: "editable markdown" }) .getByText(passage, { exact: true }) .selectText(); await surface.getByRole("button", { name: "Comment on this passage", exact: true }).click(); let draft = surface.getByRole("dialog", { name: "New comment" }); await draft.getByPlaceholder("Comment on this passage…").fill("Keep this child passage."); await draft.getByRole("button", { name: "Post comment", exact: true }).click(); await expect(surface.getByRole("button", { name: /Comment on “This ordinary child/ })) .toBeVisible(); }); test("a delayed sibling route removes the previous editable child immediately", async ({ baseURL, join, page, room, seed }) => { await seed(PARENT_SOURCE); let firstTitle = `First source ${room.slice(0, 8)}`; let secondTitle = `Second source ${room.slice(0, 8)}`; let first = await seedChildChannel( port(baseURL!), room, crypto.randomUUID(), firstTitle, CHILD_SOURCE, ); let second = await seedChildChannel( port(baseURL!), room, crypto.randomUUID(), secondTitle, CHILD_SOURCE, ); await join("ana"); await page.evaluate(() => history.replaceState({ navigation: 9 }, "", "?view=plan#siblings")); let childLink = (title: string) => page.getByRole("complementary", { name: "Projects" }) .getByRole("link", { name: title, exact: true }); await childLink(firstTitle).click(); await expect(page.locator(`[data-workspace-room="${first.id}"]`)).toBeVisible(); let release!: () => void; let gate = new Promise(resolve => release = resolve); await page.route( `**/api/repositories/octo-org/score/documents/${second.slug}`, async route => { let response = await route.fetch(); await gate; await route.fulfill({ response }); }, ); await childLink(secondTitle).click(); await expect(page).toHaveURL(url => url.pathname === second.path); await expect(page.locator(`[data-workspace-room="${first.id}"]`)).toHaveCount(0); await expect(page.locator(".anchored-child-surface")).toContainText("Opening child document…"); release(); let secondSurface = page.getByRole("region", { name: `Child document: ${secondTitle}` }); await expect(secondSurface.locator(`[data-workspace-room="${second.id}"]`)).toBeVisible(); await expect(secondSurface).toBeFocused(); await page.locator(`[data-workspace-room="${room}"] .room-header`) .getByRole("button", { name: `Return to Test ${room.slice(0, 8)}` }).click(); await expect(page).toHaveURL(url => url.pathname.endsWith(`/test-${room.slice(0, 8)}`) && url.search === "?view=plan" && url.hash === "#siblings" ); await expect(secondSurface).toHaveCount(0); await expect(childLink(secondTitle)).toBeFocused(); await childLink(firstTitle).click(); await childLink(secondTitle).click(); await expect(secondSurface.locator(`[data-workspace-room="${second.id}"]`)).toBeVisible(); await page.evaluate(() => { let held = new Map(); let next = 1; let state = window as typeof window & { __chopinHeldFrames?: { cancel: typeof cancelAnimationFrame; held: Map; request: typeof requestAnimationFrame; }; }; state.__chopinHeldFrames = { cancel: window.cancelAnimationFrame, held, request: window.requestAnimationFrame, }; window.requestAnimationFrame = callback => { let id = next++; held.set(id, callback); return id; }; window.cancelAnimationFrame = id => held.delete(id); }); await page.keyboard.press("Escape"); await expect(page).toHaveURL(url => url.pathname.endsWith(`/test-${room.slice(0, 8)}`) && url.search === "?view=plan" && url.hash === "#siblings" ); await expect(secondSurface).toHaveCount(0); expect( await page.evaluate(() => { let state = window as typeof window & { __chopinHeldFrames?: { held: Map }; }; return state.__chopinHeldFrames?.held.size ?? 0; }), ).toBeGreaterThan(0); await childLink(firstTitle).click(); let firstSurface = page.getByRole("region", { name: `Child document: ${firstTitle}` }); await expect(firstSurface.locator(`[data-workspace-room="${first.id}"]`)).toBeVisible(); await page.evaluate(() => { let state = window as typeof window & { __chopinHeldFrames?: { cancel: typeof cancelAnimationFrame; held: Map; request: typeof requestAnimationFrame; }; }; let frames = state.__chopinHeldFrames!; window.requestAnimationFrame = frames.request; window.cancelAnimationFrame = frames.cancel; for (let callback of frames.held.values()) callback(performance.now()); delete state.__chopinHeldFrames; }); await expect(firstSurface).toBeFocused(); await expect(childLink(secondTitle)).not.toBeFocused(); await page.keyboard.press("Escape"); await expect(firstSurface).toHaveCount(0); await expect(childLink(firstTitle)).toBeFocused(); await childLink(firstTitle).click(); await childLink(secondTitle).click(); await expect(secondSurface.locator(`[data-workspace-room="${second.id}"]`)).toBeVisible(); await page.goBack(); await expect(page).toHaveURL(url => url.pathname.endsWith(`/test-${room.slice(0, 8)}`) && url.search === "?view=plan" && url.hash === "#siblings" ); await expect(page.locator(`[data-workspace-room="${first.id}"]`)).toHaveCount(0); await expect(childLink(secondTitle)).toBeFocused(); }); test("a direct child keeps direct-entry history when opening a sibling", async ({ baseURL, page, room, seed }) => { await seed(PARENT_SOURCE); let firstTitle = `Direct first ${room.slice(0, 8)}`; let secondTitle = `Direct second ${room.slice(0, 8)}`; let first = await seedChildChannel( port(baseURL!), room, crypto.randomUUID(), firstTitle, CHILD_SOURCE, ); let second = await seedChildChannel( port(baseURL!), room, crypto.randomUUID(), secondTitle, CHILD_SOURCE, ); await authenticate(page, "ana", baseURL!); await page.goto(first.path); await expect(page.locator(`[data-workspace-room="${first.id}"]`)).toBeVisible(); let historyLength = await page.evaluate(() => history.length); let secondLink = page.getByRole("complementary", { name: "Projects" }) .getByRole("link", { name: secondTitle, exact: true }); await secondLink.click(); let secondSurface = page.getByRole("region", { name: `Child document: ${secondTitle}` }); await expect(secondSurface.locator(`[data-workspace-room="${second.id}"]`)).toBeVisible(); expect(await page.evaluate(() => history.length)).toBe(historyLength); await page.locator(`[data-workspace-room="${room}"] .room-header`) .getByRole("button", { name: `Return to Test ${room.slice(0, 8)}` }).click(); await expect(page).toHaveURL(url => url.pathname.endsWith(`/test-${room.slice(0, 8)}`) && url.search === "" && url.hash === "" ); await expect(secondSurface).toHaveCount(0); await expect(secondLink).toBeFocused(); }); test("a recovered child id enters the canonical anchored child workspace", async ({ baseURL, page, room, seed }) => { await seed(PARENT_SOURCE); let childTitle = `Recovered source ${room.slice(0, 8)}`; let child = await seedChildChannel( port(baseURL!), room, crypto.randomUUID(), childTitle, CHILD_SOURCE, ); await authenticate(page, "ana", baseURL!); await page.goto(`/channels/${child.id}`); await expect(page).toHaveURL(url => url.pathname === child.path); let parent = page.locator(`[data-workspace-room="${room}"]`); let surface = page.getByRole("region", { name: `Child document: ${childTitle}` }); await expect(parent).toBeVisible(); await expect(surface.locator(`[data-workspace-room="${child.id}"]`)).toBeVisible(); // With no saved preference, Chat starts open in the parent and so in the child. await expect(surface.getByRole("complementary", { name: "Chat" })).toBeVisible(); let close = surface.getByRole("button", { name: `Close ${childTitle}`, exact: true }); await expect(close).toBeVisible(); await expect( parent.locator(".room-header").getByRole("button", { name: `Return to Test ${room.slice(0, 8)}`, exact: true, }), ).toBeVisible(); let editor = surface.getByRole("textbox", { name: "editable markdown", exact: true }); await editor.click(); await page.keyboard.press("Meta+End"); await page.keyboard.press("Enter"); await page.keyboard.type("/research"); await expect(surface.getByRole("button", { name: "Search public web", exact: true })) .toHaveCount(0); await close.click(); await expect(page).toHaveURL(url => url.pathname.endsWith(`/test-${room.slice(0, 8)}`)); await expect(surface).toHaveCount(0); }); test("a channel link opens one parent room that a child opens and closes over", async ({ baseURL, page, room, seed }) => { await seed(PARENT_SOURCE); let childTitle = `Channel link child ${room.slice(0, 8)}`; let child = await seedChildChannel( port(baseURL!), room, crypto.randomUUID(), childTitle, CHILD_SOURCE, ); let recorded = await recordRoomMounts(page); await authenticate(page, "ana", baseURL!); await page.goto(`/channels/${room}`); let parentPath = `/documents/octo-org/score/test-${room.slice(0, 8)}`; await expect(page).toHaveURL(url => url.pathname === parentPath); let parent = page.locator(`[data-workspace-room="${room}"]`); await expect(parent.locator("[data-plan-synced]")).toBeAttached(); await parent.evaluate(element => element.setAttribute("data-mount-token", "preserved")); let surface = page.getByRole("region", { name: `Child document: ${childTitle}` }); await page.getByRole("complementary", { name: "Projects" }) .getByRole("link", { name: childTitle, exact: true }).click(); await expect(page).toHaveURL(url => url.pathname === child.path); await expect(surface.locator(`[data-workspace-room="${child.id}"]`)).toBeVisible(); await page.keyboard.press("Escape"); await expect(page).toHaveURL(url => url.pathname === parentPath); await expect(surface).toHaveCount(0); await expect(parent).toHaveAttribute("data-mount-token", "preserved"); expect((await recorded.mounts()).filter(entry => entry.endsWith(room))).toEqual([ `mount ${room}`, ]); expect(recorded.reads(room)).toBe(1); }); test("a child load failure preserves its mounted parent through back and retry", async ({ baseURL, join, page, room, seed }) => { await seed(PARENT_SOURCE); let childTitle = `Unreliable source ${room.slice(0, 8)}`; let child = await seedChildChannel( port(baseURL!), room, crypto.randomUUID(), childTitle, CHILD_SOURCE, ); await join("ana"); let parent = page.locator(`[data-workspace-room="${room}"]`); await parent.evaluate(element => element.setAttribute("data-mount-token", "preserved")); let attempts = 0; await page.route( `**/api/repositories/octo-org/score/documents/${child.slug}`, async route => { attempts += 1; if (attempts <= 2) { await route.fulfill({ body: JSON.stringify({ error: "Child temporarily unavailable" }), contentType: "application/json", status: 503, }); return; } await route.continue(); }, ); let childLink = page.getByRole("complementary", { name: "Projects" }) .getByRole("link", { name: childTitle, exact: true }); await childLink.click(); let surface = page.locator(".anchored-child-surface"); await expect(surface).toContainText("Couldn't open this document"); await expect(parent).toHaveAttribute("data-mount-token", "preserved"); await expect(parent.locator(".workspace-frame")).toHaveAttribute("inert", ""); await parent.locator(".room-header") .getByRole("button", { name: `Return to Test ${room.slice(0, 8)}` }).click(); await expect(surface).toHaveCount(0); await expect(parent).toHaveAttribute("data-mount-token", "preserved"); await childLink.click(); await expect(surface).toContainText("Couldn't open this document"); await surface.getByRole("button", { name: "Try again" }).click(); await expect(surface.locator(`[data-workspace-room="${child.id}"]`)).toBeVisible(); await expect(parent).toHaveAttribute("data-mount-token", "preserved"); }); test("a direct compact child fills the canvas and reduces motion to a crossfade", async ({ baseURL, page, room, seed }) => { await seed(PARENT_SOURCE); let childTitle = `A substantially longer source review title ${room.slice(0, 8)}`; let child = await seedChildChannel( port(baseURL!), room, crypto.randomUUID(), childTitle, CHILD_SOURCE, ); await page.setViewportSize({ width: 390, height: 844 }); await page.emulateMedia({ reducedMotion: "reduce" }); await installPointerMedia(page, { coarse: true, primaryCoarse: true }); await authenticate(page, "ana", baseURL!); await page.goto(child.path); let surface = page.getByRole("region", { name: `Child document: ${childTitle}` }); await expect(surface).toBeVisible(); await expect(surface).toBeFocused(); let navigation = surface.getByRole("navigation", { name: "Workspace view" }); let destinations = navigation.getByRole("button"); await expect(destinations).toHaveCount(3); await expect(navigation.getByRole("button", { name: /^Chat/ })).toBeVisible(); await expect(navigation.getByRole("button", { name: "Document", exact: true })).toBeVisible(); await expect(navigation.getByRole("button", { name: /^Decisions/ })).toBeVisible(); await navigation.getByRole("button", { name: /^Chat/ }).click(); await expect(surface.getByRole("complementary", { name: "Chat" })).toBeVisible(); await expect(surface.locator('[data-document-view="plan"]')).toBeHidden(); await navigation.getByRole("button", { name: "Document", exact: true }).click(); await expect(surface.getByRole("complementary", { name: "Chat" })).toBeHidden(); await expect(surface.locator('[data-document-view="plan"]')).toBeVisible(); let projects = page.getByRole("button", { name: "Show sidebar" }); await projects.click(); let drawer = page.getByRole("dialog", { name: "Projects" }); await expect(drawer).toBeVisible(); await drawer.getByRole("link", { name: childTitle, exact: true }).click(); await expect(drawer).toBeHidden(); await expect(surface).toBeVisible(); let parentHeader = page.locator('[data-workspace-surface="document"] .room-header'); await expect(page.locator(".room-header:visible")).toHaveCount(1); await expect(parentHeader.getByText(childTitle, { exact: true })).toBeVisible(); await expect(surface.locator(".room-header")).toBeHidden(); await expect(parentHeader.getByRole("button", { name: `Back to Test ${room.slice(0, 8)}` })) .toHaveCount(0); await expect(parentHeader.getByRole("button", { name: `Return to Test ${room.slice(0, 8)}` })) .toBeVisible(); let childCrumb = parentHeader.getByText(childTitle, { exact: true }); let members = parentHeader.getByRole("group", { name: /People here:/ }); await expect(childCrumb).toHaveCSS("text-overflow", "ellipsis"); let crumbBox = await childCrumb.boundingBox(); let membersBox = await members.boundingBox(); expect(crumbBox).not.toBeNull(); expect(membersBox).not.toBeNull(); expect(crumbBox!.x + crumbBox!.width).toBeLessThanOrEqual(membersBox!.x); let parentPaper = page.locator('[data-workspace-surface="document"] .workspace-frame'); await expect(parentPaper).toHaveCSS("transform", "none"); expect( await page.locator(".anchored-child-parent").evaluate( element => getComputedStyle(element, "::after").transitionProperty, ), ).toBe("none"); let headerBox = await parentHeader.boundingBox(); let childBox = await surface.boundingBox(); expect(headerBox).not.toBeNull(); expect(childBox).not.toBeNull(); expect(Math.round(childBox!.y)).toBe(Math.round(headerBox!.y + headerBox!.height)); let geometry = await surface.evaluate(element => { let style = getComputedStyle(element); let box = element.getBoundingClientRect(); return { animationName: style.animationName, left: box.left, right: innerWidth - box.right, transform: style.transform, }; }); expect(geometry).toEqual({ animationName: "anchored-child-fade", left: 0, right: 0, transform: "none", }); await parentHeader.getByRole("button", { name: `Return to Test ${room.slice(0, 8)}` }).click(); await expect(surface).toHaveCSS("animation-name", "anchored-child-fade-out"); await expect(page).toHaveURL(url => url.pathname.endsWith(`/test-${room.slice(0, 8)}`)); }); test("Escape in a child editor dismisses the innermost thing before the sheet", async ({ baseURL, join, page, room, seed }) => { await seed(PARENT_SOURCE); let title = `Escape source ${room.slice(0, 8)}`; let child = await seedChildChannel( port(baseURL!), room, crypto.randomUUID(), title, CHILD_SOURCE, ); await join("ana"); await page.goto(child.path); let surface = page.getByRole("region", { name: `Child document: ${title}` }); let editor = surface.getByRole("textbox", { name: "editable markdown", exact: true }); await expect(editor).toBeVisible(); await surface.getByRole("heading", { level: 1 }).click(); await page.keyboard.press("End"); await page.keyboard.press("Enter"); await page.keyboard.type("/"); let menu = page.getByRole("listbox", { name: "Insert block" }); await expect(menu).toBeVisible(); await page.keyboard.press("Escape"); await expect(menu).toHaveCount(0); await expect(surface).toBeVisible(); await page.keyboard.press("Escape"); await expect(surface).toBeVisible(); await expect(editor).not.toBeFocused(); await expect(surface).toBeFocused(); await page.keyboard.press("Escape"); await expect(surface).toHaveCount(0); await expect(page).toHaveURL(url => url.pathname.endsWith(`/test-${room.slice(0, 8)}`)); });