import { mkdir, readFile, writeFile } from "node:fs/promises"; import path from "node:path"; import type { Locator } from "@playwright/test"; import { expect, test, type Page } from "../support/fixtures"; import { expandFolder, openFileExplorer, openFileFromExplorer, expectFileTabOpen, } from "../support/helpers/file-explorer"; import { installDaemonWebSocketGate } from "../support/helpers/daemon-websocket-gate"; import { gotoAppShell } from "../support/helpers/app"; import { openAgentRoute, seedMockAgentWorkspace } from "../support/helpers/mock-agent"; import { seedWorkspace } from "../support/helpers/seed-client"; import { getServerId } from "../support/helpers/server-id"; import { waitForSidebarHydration } from "../support/helpers/workspace-ui"; const APP_SETTINGS_KEY = "@paseo:app-settings"; const RED_PIXEL = Buffer.from( "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mP8/x8AAusB9Y9ZQmcAAAAASUVORK5CYII=", "base64", ); const BLUE_PIXEL = Buffer.from( "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNk+A8AAQUBAScY42YAAAAASUVORK5CYII=", "base64", ); const BLOCKED_PREVIEW_URL = "https://html-preview.invalid/leak"; interface LinkedFile { target: string; fileName: string; content: string; } function editor(page: Page) { return page.getByTestId("file-source-editor").filter({ visible: true }).locator(".cm-content"); } function hasHorizontalOverflow(element: HTMLElement): boolean { return element.scrollWidth > element.clientWidth; } function fitsViewportWidth(element: HTMLElement): boolean { return element.scrollWidth === element.clientWidth; } async function replaceEditorText(page: Page, content: string): Promise { await editor(page).fill(content); } async function openWorkspaceFile(page: Page, filename: string): Promise { const tree = page.getByTestId("file-explorer-tree-scroll"); if (!(await tree.isVisible())) await openFileExplorer(page); await openFileFromExplorer(page, filename); await expectFileTabOpen(page, filename); } function htmlPreview(page: Page) { return { host: page.getByTestId("file-html-preview"), document: page.frameLocator('[data-testid="file-html-preview"]'), }; } async function selectFileView(page: Page, view: "Preview" | "Source"): Promise { const option = page.getByTestId("file-panel-bar").getByRole("button", { name: view, exact: true, }); await option.click(); await expect(option).toHaveAttribute("aria-selected", "true"); } async function expectAbove(upper: Locator, lower: Locator): Promise { const [upperBox, lowerBox] = await Promise.all([upper.boundingBox(), lower.boundingBox()]); expect(upperBox).not.toBeNull(); expect(lowerBox).not.toBeNull(); expect(upperBox!.y + upperBox!.height).toBeLessThanOrEqual(lowerBox!.y); } /** How many times the app has read the file's contents from the daemon. */ function fileReadCount( gate: Awaited>, fileName: string, ): number { return gate .getClientRequests("file_explorer_request") .filter((request) => request.mode === "file" && String(request.path).endsWith(fileName)).length; } async function expectMarkdownImageLoaded(page: Page, name: string): Promise { const image = page.getByRole("button", { name, exact: true }); await expect(image).toBeVisible(); await expect .poll(() => image.evaluate((element: HTMLImageElement) => element.naturalWidth)) .toBeGreaterThan(0); } function watchRequestsTo(page: Page, origin: string): string[] { const requests: string[] = []; page.on("request", (request) => { if (request.url().startsWith(origin)) requests.push(request.url()); }); return requests; } async function seedAgentWithFileLink(input: LinkedFile) { const session = await seedMockAgentWorkspace({ repoPrefix: "file-editing-chat-link-", title: "Chat file link e2e", initialPrompt: [ "Generate a title and a git branch name for a coding agent from the user prompt and attachments.", "Return JSON only with fields 'title' and 'branch'.", "", "", `Open \`${input.target}\` now`, "", ].join("\n"), }); await writeFile(path.join(session.cwd, input.fileName), input.content, "utf8"); return session; } test.describe("CodeMirror workspace file editing", () => { test("opens explicit screenshot links and explains missing files", async ({ page }) => { const session = await seedMockAgentWorkspace({ repoPrefix: "screenshot-markdown-link-", title: "Screenshot file links", initialPrompt: "Show screenshots", featureValues: { mockAssistantResponse: "[Capture](.dev/tablet-review/capture.png) [Missing capture](.dev/tablet-review/missing.png)", }, }); try { await mkdir(path.join(session.cwd, ".dev/tablet-review"), { recursive: true }); await writeFile(path.join(session.cwd, ".dev/tablet-review/capture.png"), RED_PIXEL); await openAgentRoute(page, session); await test.step("Open the linked screenshot", async () => { await openAssistantMarkdownLink(page, "Capture"); await expectFileTabOpen(page, ".dev/tablet-review/capture.png"); await expect(page.getByTestId("image-file-preview-canvas")).toBeVisible(); }); await test.step("Explain why the missing screenshot cannot open", async () => { await returnToAssistant(page, session.agentId); await openAssistantMarkdownLink(page, "Missing capture"); await expectFileTabOpen(page, ".dev/tablet-review/missing.png"); await expect(page.getByText("File not found", { exact: true })).toBeVisible(); await expect(page.getByRole("button", { name: "Retry", exact: true })).toBeVisible(); }); } finally { await session.cleanup(); } }); test("opens the non-ASCII file named by an assistant markdown link", async ({ page }) => { const session = await seedMockAgentWorkspace({ repoPrefix: "encoded-markdown-link-", title: "Non-ASCII file link", initialPrompt: "Show the report link", featureValues: { mockAssistantResponse: "[报告](docs/reports/开户赠金.md)" }, }); try { await mkdir(path.join(session.cwd, "docs/reports"), { recursive: true }); await writeFile(path.join(session.cwd, "docs/reports/开户赠金.md"), "# Report opened\n"); await openAgentRoute(page, session); await openAssistantMarkdownLink(page, "报告"); await expectFileTabOpen(page, "docs/reports/开户赠金.md"); await expect( page .getByTestId("workspace-file-pane") .filter({ visible: true }) .getByText("Report opened", { exact: true }), ).toBeVisible(); await page.screenshot({ path: test.info().outputPath("non-ascii-file-opened.png") }); } finally { await session.cleanup(); } }); for (const order of ["raw-first", "href-first"] as const) { test(`opens literal and encoded percent filenames independently ${order}`, async ({ page }) => { const session = await seedMockAgentWorkspace({ repoPrefix: "percent-link-cache-", title: "Distinct file links", initialPrompt: "Show both filenames", featureValues: { mockAssistantResponse: "Raw `literal%20.md`. [Decoded](literal%20.md).", }, }); try { await writeFile(path.join(session.cwd, "literal%20.md"), "# Literal filename\n"); await writeFile(path.join(session.cwd, "literal .md"), "# Decoded filename\n"); await openAgentRoute(page, session); const links = order === "raw-first" ? ["raw", "href"] : ["href", "raw"]; for (const link of links) { await returnToAssistant(page, session.agentId); if (link === "raw") { await openAssistantInlinePath(page, "literal%20.md"); await expectFileTabOpen(page, "literal%20.md"); await expectPreviewText(page, "Literal filename"); } else { await openAssistantMarkdownLink(page, "Decoded"); await expectFileTabOpen(page, "literal .md"); await expectPreviewText(page, "Decoded filename"); } } } finally { await session.cleanup(); } }); } test("jumps to each relative reference to the same file", async ({ page }) => { const session = await seedMockAgentWorkspace({ repoPrefix: "relative-reference-qa-", title: "Relative line references", initialPrompt: "Show both references", featureValues: { mockAssistantResponse: "First `src/router.go:46`. Then `src/router.go:403`.", }, }); try { await mkdir(path.join(session.cwd, "src")); await writeFile( path.join(session.cwd, "src/router.go"), Array.from({ length: 450 }, (_, n) => `// line ${n + 1}`).join("\n"), ); await openAgentRoute(page, session); for (const line of [46, 403, 46]) { await page .getByTestId(`workspace-tab-agent_${session.agentId}`) .filter({ visible: true }) .click(); await page.getByText(`src/router.go:${line}`, { exact: true }).click(); await expect(page.getByLabel(`Line ${line}, column 1`)).toBeVisible(); } await page.screenshot({ path: test.info().outputPath("relative-line-target.png") }); } finally { await session.cleanup(); } }); test("shows an absolute POSIX assistant file link relative to the workspace on hover", async ({ page, }) => { const workspace = await seedWorkspace({ repoPrefix: "file-link-tooltip-" }); const filePath = path.join(workspace.repoPath, "src", "target.ts"); try { const agent = await workspace.client.createAgent({ provider: "mock", cwd: workspace.repoPath, workspaceId: workspace.workspaceId, title: "Absolute file link tooltip", modeId: "load-test", model: "e2e-fast-stream", initialPrompt: "Show the file link", featureValues: { mockAssistantResponse: `[Open target](${filePath}:42-45)` }, }); await gotoAppShell(page); await waitForSidebarHydration(page); const workspaceRow = page.getByTestId( `sidebar-workspace-row-${getServerId()}:${workspace.workspaceId}`, ); await expect(workspaceRow).toBeVisible({ timeout: 30_000 }); await workspaceRow.click(); await page.getByTestId(`workspace-tab-agent_${agent.id}`).filter({ visible: true }).click(); const fileLink = page.getByRole("link", { name: "Open target" }).first(); await expect(fileLink).toBeVisible({ timeout: 15_000 }); await fileLink.hover(); await expect(fileLink).toHaveAttribute("title", "src/target.ts:42-45"); } finally { await workspace.cleanup(); } }); test("renders a lockfile-sized read-only source with a bounded CodeMirror DOM", async ({ page, }) => { const session = await seedMockAgentWorkspace({ repoPrefix: "file-source-lockfile-", title: "Large source", initialPrompt: "Generate a title and a git branch name. Return JSON only.", }); const lockfile = `${'{"packages":['}${Array.from({ length: 42_000 }, (_, index) => `{"name":"package-${index}","version":"1.0.0"}`).join(",")}]}`; await writeFile(path.join(session.cwd, "package-lock.json"), lockfile, "utf8"); try { await openAgentRoute(page, session); await openWorkspaceFile(page, "package-lock.json"); await expect(page.getByTestId("file-source-editor")).toBeVisible(); await expect(editor(page)).toContainText('"package-0"'); await expect.poll(() => page.locator(".cm-line").count()).toBeLessThan(200); } finally { await session.cleanup(); } }); test("keeps the app interactive around a plain 11 MB source", async ({ page }) => { const session = await seedMockAgentWorkspace({ repoPrefix: "file-source-plain-", title: "Plain large source", initialPrompt: "Generate a title and a git branch name. Return JSON only.", }); await writeFile( path.join(session.cwd, "plain.txt"), "plain source\n".repeat(1_050_000), "utf8", ); try { await openAgentRoute(page, session); await openWorkspaceFile(page, "plain.txt"); await expect(page.getByTestId("file-source-editor")).toBeVisible(); await expect(editor(page)).toContainText("plain source"); await expect.poll(() => page.locator(".cm-line").count()).toBeLessThan(200); await page.getByTestId(`workspace-tab-agent_${session.agentId}`).first().click(); await expect(page.getByTestId("message-input-root")).toBeVisible(); await page.getByTestId("workspace-tab-file_plain.txt").first().click(); await expect(page.getByTestId("file-source-editor")).toBeVisible(); } finally { await session.cleanup(); } }); test("refuses a file above the display budget and keeps its tab recoverable", async ({ page, }) => { const session = await seedMockAgentWorkspace({ repoPrefix: "file-source-unsupported-", title: "Unsupported large source", initialPrompt: "Generate a title and a git branch name. Return JSON only.", }); await writeFile(path.join(session.cwd, "too-large.txt"), "x".repeat(51 * 1024 * 1024), "utf8"); try { await openAgentRoute(page, session); await openWorkspaceFile(page, "too-large.txt"); await expect(page.getByTestId("file-source-too-large")).toContainText( "This file is too large to display", ); await page.getByTestId(`workspace-tab-agent_${session.agentId}`).first().click(); await expect(page.getByTestId("message-input-root")).toBeVisible(); await page.getByTestId("workspace-tab-file_too-large.txt").first().click(); await expect(page.getByTestId("file-source-too-large")).toBeVisible(); } finally { await session.cleanup(); } }); test("opens an assistant file link at its referenced line", async ({ page }) => { const target = "target.ts:42"; const session = await seedAgentWithFileLink({ target, fileName: "target.ts", content: Array.from( { length: 80 }, (_, index) => `export const line${index + 1} = ${index + 1};`, ).join("\n"), }); try { await openAgentRoute(page, session); const fileLink = page.getByText(target, { exact: true }); await expect(fileLink).toBeVisible({ timeout: 15_000 }); await fileLink.click(); await expectFileTabOpen(page, "target.ts"); await expect(page.getByTestId("file-source-editor")).toBeVisible(); await expect(page.getByLabel("Line 42, column 1")).toBeVisible(); await expect( page.getByTestId("file-source-editor").locator(".cm-line", { hasText: "line42 = 42" }), ).toBeVisible(); const sourceEditor = editor(page); await sourceEditor.click(); await sourceEditor.press("ControlOrMeta+Home"); await expect(page.getByLabel(/^Line 1, column \d+$/)).toBeVisible(); await page .getByTestId(`workspace-tab-agent_${session.agentId}`) .filter({ visible: true }) .click(); await expect(fileLink).toBeVisible(); await fileLink.click(); await expect(page.getByLabel("Line 42, column 1")).toBeVisible(); await expect( page.getByTestId("file-source-editor").locator(".cm-line", { hasText: "line42 = 42" }), ).toBeVisible(); } finally { await session.cleanup(); } }); test("clicking the editor focuses its pane beside an agent", async ({ page }) => { await page.addInitScript((settingsKey) => { localStorage.setItem( settingsKey, JSON.stringify({ openInSidePane: { explorerFiles: true } }), ); }, APP_SETTINGS_KEY); const target = "target.ts:42"; const session = await seedAgentWithFileLink({ target, fileName: "target.ts", content: Array.from( { length: 80 }, (_, index) => `export const line${index + 1} = ${index + 1};`, ).join("\n"), }); try { await page.setViewportSize({ width: 1280, height: 900 }); await openAgentRoute(page, session); await openWorkspaceFile(page, "target.ts"); await expect( page .locator('[data-testid^="workspace-pane-"]') .getByTestId("workspace-tabs-row") .filter({ visible: true }), ).toHaveCount(2); await page .getByTestId(`workspace-tab-agent_${session.agentId}`) .filter({ visible: true }) .click(); await editor(page).click(); await page.keyboard.press("Alt+Shift+W"); await expect(page.getByTestId("workspace-tab-file_target.ts")).not.toBeVisible(); await expect( page.getByTestId(`workspace-tab-agent_${session.agentId}`).filter({ visible: true }), ).toBeVisible(); } finally { await session.cleanup(); } }); test("opens an HTML line target as source", async ({ page }) => { const target = "plan.html:2"; const session = await seedAgentWithFileLink({ target, fileName: "plan.html", content: [ "", "

Review this source line

", '', ].join("\n"), }); try { await openAgentRoute(page, session); await page.getByText(target, { exact: true }).click(); await expectFileTabOpen(page, "plan.html"); await expect(page.getByTestId("file-source-editor")).toBeVisible(); await expect(page.getByLabel("Line 2, column 1")).toBeVisible(); await expect(page.getByTestId("file-html-preview")).toHaveCount(0); } finally { await session.cleanup(); } }); test("shows the full file path and keeps editor controls stable", async ({ page, withWorkspace, }) => { await page.emulateMedia({ colorScheme: "dark" }); const workspace = await withWorkspace({ prefix: "file-editing-visuals-" }); const relativePath = "src/deep/visuals.md"; const sourcePath = path.join(workspace.repoPath, relativePath); await mkdir(path.dirname(sourcePath), { recursive: true }); await writeFile( sourcePath, [...Array.from({ length: 11 }, (_, index) => `line ${index + 1}`), "abcdefghijklmnop"].join( "\n", ), "utf8", ); await workspace.navigateTo(); await openFileExplorer(page); await page.getByTestId("file-explorer-tree-scroll").getByText("src", { exact: true }).click(); await page.getByTestId("file-explorer-tree-scroll").getByText("deep", { exact: true }).click(); await openFileFromExplorer(page, "visuals.md"); await expectFileTabOpen(page, relativePath); const fileTab = page.getByTestId(`workspace-tab-file_${relativePath}`).first(); await fileTab.hover(); await expect(page.getByTestId(`workspace-tab-tooltip-file_${relativePath}`)).toHaveText( relativePath, ); await expect(page.getByTestId("file-panel-bar")).not.toContainText("visuals.md"); const modeControl = page.getByTestId("file-preview-mode"); await expect(modeControl).toBeVisible(); await selectFileView(page, "Source"); const editorHost = page.getByTestId("file-source-editor"); const content = editor(page); await expect(editorHost).toHaveAttribute("data-pmono", ""); await expect(content).toHaveCSS("font-family", /SFMono-Regular/); await content.click(); const cursor = editorHost.locator(".cm-cursor-primary"); await expect(cursor).toBeVisible(); await expect(cursor).toHaveCSS("border-left-color", "rgb(250, 250, 250)"); const initialModeBox = await modeControl.boundingBox(); expect(initialModeBox).not.toBeNull(); const initialModeX = initialModeBox!.x; await content.press("ControlOrMeta+End"); await expect(page.getByLabel(/Line 12, column \d+/)).toBeVisible(); const movedModeBox = await modeControl.boundingBox(); expect(movedModeBox).not.toBeNull(); expect(movedModeBox!.x).toBe(initialModeX); await content.press("ControlOrMeta+a"); const selection = editorHost.locator(".cm-selectionBackground").first(); await expect(selection).toBeVisible(); await expect(selection).toHaveCSS("background-color", "rgba(255, 255, 255, 0.2)"); }); test("applies the interface font to portaled tooltips", async ({ page, withWorkspace }) => { await page.addInitScript(() => { localStorage.setItem("@paseo:app-settings", JSON.stringify({ uiFontFamily: "monospace" })); }); const workspace = await withWorkspace({ prefix: "file-tooltip-font-" }); const relativePath = "tooltip-font.txt"; await writeFile(path.join(workspace.repoPath, relativePath), "tooltip font\n", "utf8"); await workspace.navigateTo(); await openFileExplorer(page); await openFileFromExplorer(page, relativePath); await expectFileTabOpen(page, relativePath); await page.getByTestId(`workspace-tab-file_${relativePath}`).first().hover(); await expect( page .getByTestId(`workspace-tab-tooltip-file_${relativePath}`) .getByText(relativePath, { exact: true }), ).toHaveCSS("font-family", "monospace"); }); test("wraps Markdown while source code remains horizontally scrollable", async ({ page, withWorkspace, }) => { const workspace = await withWorkspace({ prefix: "file-editing-wrap-" }); const longLine = "word ".repeat(300); await writeFile(path.join(workspace.repoPath, "notes.md"), `${longLine}\n`, "utf8"); await writeFile( path.join(workspace.repoPath, "source.ts"), `const value = "${longLine}";\n`, "utf8", ); await workspace.navigateTo(); await openWorkspaceFile(page, "notes.md"); await selectFileView(page, "Source"); const markdownScroller = page .getByTestId("file-source-editor") .filter({ visible: true }) .locator(".cm-scroller"); await expect.poll(() => markdownScroller.evaluate(fitsViewportWidth)).toBe(true); await openWorkspaceFile(page, "source.ts"); const sourceScroller = page .getByTestId("file-source-editor") .filter({ visible: true }) .locator(".cm-scroller"); await expect.poll(() => sourceScroller.evaluate(hasHorizontalOverflow)).toBe(true); }); test("autosaves, saves immediately, resolves conflicts, and restores live updates after reconnect", async ({ page, withWorkspace, }) => { test.setTimeout(120_000); const gate = await installDaemonWebSocketGate(page); const workspace = await withWorkspace({ prefix: "file-editing-source-" }); const sourcePath = path.join(workspace.repoPath, "source.ts"); await writeFile(sourcePath, "const initial = 1;\n", "utf8"); await Promise.all( ["one.ts", "two.ts", "three.ts", "four.ts"].map((fileName) => writeFile(path.join(workspace.repoPath, fileName), `// ${fileName}\n`, "utf8"), ), ); await workspace.navigateTo(); await openWorkspaceFile(page, "source.ts"); await expect(page.getByTestId("file-source-editor")).toBeVisible(); await expect(page.getByLabel(/File size/)).toBeVisible(); await expect(page.getByLabel(/lines/)).toBeVisible(); await expect(page.getByTestId("file-preview-mode")).toHaveCount(0); await expectAbove( page.getByTestId("file-panel-bar").filter({ visible: true }), page.getByTestId("file-source-editor").filter({ visible: true }), ); await replaceEditorText(page, "const autosaved = 2;\n"); await expect(page.getByTestId("workspace-tab-modified-file_source.ts")).toBeVisible(); await expect(page.getByLabel("Editor status dirty")).toBeVisible(); await expect(page.getByLabel("Editor status clean")).toBeVisible({ timeout: 5_000 }); await expect(page.getByTestId("workspace-tab-modified-file_source.ts")).not.toBeVisible(); await expect.poll(() => readFile(sourcePath, "utf8")).toBe("const autosaved = 2;\n"); await replaceEditorText(page, "const immediate = 3;\n"); await editor(page).press("Control+s"); await expect.poll(() => readFile(sourcePath, "utf8")).toBe("const immediate = 3;\n"); await writeFile(sourcePath, "const external = 4;\nconst line = 2;\n", "utf8"); await expect(editor(page)).toContainText("const external = 4;"); await expect(page.getByLabel("3 lines")).toBeVisible(); await replaceEditorText(page, "const localWins = 5;\n"); await writeFile(sourcePath, "const diskLoses = 6;\n", "utf8"); await expect(page.getByTestId("file-conflict-alert")).toBeVisible(); await page.getByRole("button", { name: "Overwrite", exact: true }).click(); await expect.poll(() => readFile(sourcePath, "utf8")).toBe("const localWins = 5;\n"); for (const fileName of ["one.ts", "two.ts", "three.ts", "four.ts"]) { await openWorkspaceFile(page, fileName); } await openWorkspaceFile(page, "source.ts"); await expect(editor(page)).toContainText("const localWins = 5;"); await replaceEditorText(page, "const discarded = 7;\n"); await writeFile(sourcePath, "const diskWins = 8;\n", "utf8"); await expect(page.getByTestId("file-conflict-alert")).toBeVisible(); page.once("dialog", (dialog) => dialog.accept()); await page.getByRole("button", { name: "Reload", exact: true }).click(); await expect(editor(page)).toContainText("const diskWins = 8;"); const subscriptionCount = gate.getClientRequestCount("fs.file.subscribe.request"); await gate.drop(); gate.restore(); await expect .poll(() => gate.getClientRequestCount("fs.file.subscribe.request"), { timeout: 30_000 }) .toBeGreaterThan(subscriptionCount); await writeFile(sourcePath, "const afterReconnect = 9;\n", "utf8"); await expect(editor(page)).toContainText("const afterReconnect = 9;"); }); test("preserves a UTF-8 BOM and uses the first line separator after saving", async ({ page, withWorkspace, }) => { const workspace = await withWorkspace({ prefix: "file-editing-encoding-" }); const sourcePath = path.join(workspace.repoPath, "windows.ts"); await writeFile( sourcePath, Buffer.from("\uFEFFconst initial = true;\r\nconst mixed = true;\n", "utf8"), ); await workspace.navigateTo(); await openWorkspaceFile(page, "windows.ts"); await replaceEditorText(page, "const saved = true;\nconst normalized = true;\n"); await editor(page).press("Control+s"); const expected = Buffer.from( "\uFEFFconst saved = true;\r\nconst normalized = true;\r\n", "utf8", ).toString("hex"); await expect.poll(async () => (await readFile(sourcePath)).toString("hex")).toBe(expected); }); test("warns before closing a panel with an unsaved draft", async ({ page, withWorkspace }) => { const workspace = await withWorkspace({ prefix: "file-editing-draft-" }); const sourcePath = path.join(workspace.repoPath, "draft.ts"); await writeFile(sourcePath, "const initial = 1;\n", "utf8"); await workspace.navigateTo(); await openWorkspaceFile(page, "draft.ts"); await replaceEditorText(page, "const local = 2;\n"); await writeFile(sourcePath, "const external = 3;\n", "utf8"); await expect(page.getByTestId("file-conflict-alert")).toBeVisible(); await expect(page.getByTestId("workspace-tab-modified-file_draft.ts")).toBeVisible(); let closePrompt = ""; page.once("dialog", async (dialog) => { closePrompt = dialog.message(); await dialog.dismiss(); }); await page .getByTestId("workspace-tab-file_draft.ts") .filter({ visible: true }) .first() .click({ button: "right" }); await page.getByRole("menuitem", { name: "Close", exact: true }).click(); expect(closePrompt).toContain("Closing it will discard the draft."); await expect(page.getByTestId("file-source-editor")).toBeVisible(); await expect(page.getByTestId("workspace-tab-modified-file_draft.ts")).toBeVisible(); }); test("refreshes Markdown and images while preserving Preview and Source behavior", async ({ page, withWorkspace, }) => { test.setTimeout(90_000); const gate = await installDaemonWebSocketGate(page); const workspace = await withWorkspace({ prefix: "file-editing-preview-" }); const markdownPath = path.join(workspace.repoPath, "notes.md"); const imagePath = path.join(workspace.repoPath, "pixel.png"); await writeFile(markdownPath, "# First heading\n", "utf8"); await writeFile(imagePath, RED_PIXEL); await workspace.navigateTo(); await openWorkspaceFile(page, "notes.md"); const visibleFilePane = page.getByTestId("workspace-file-pane").filter({ visible: true }); const toolbar = page.getByTestId("file-panel-bar").filter({ visible: true }); await expect(visibleFilePane.getByText("First heading", { exact: true })).toBeVisible(); await expect(page.getByRole("button", { name: "Preview", exact: true })).toBeVisible(); await expect(toolbar.getByLabel(/^File size/)).toBeVisible(); await expect(toolbar.getByLabel("2 lines")).toBeVisible(); await writeFile(markdownPath, "# Updated heading\n", "utf8"); await expect(visibleFilePane.getByText("Updated heading", { exact: true })).toBeVisible(); // Source shows the same read in the editor; the file is not read again. const reads = fileReadCount(gate, "notes.md"); await selectFileView(page, "Source"); await expect(page.getByTestId("file-source-editor")).toBeVisible(); await expect(editor(page)).toContainText("# Updated heading"); await expect(toolbar.getByLabel("Line 1, column 1")).toBeVisible(); expect(fileReadCount(gate, "notes.md")).toBe(reads); // Preview shows the edits made in Source. await replaceEditorText(page, "# Saved from source\n"); await expect(page.getByTestId("workspace-tab-modified-file_notes.md")).toBeVisible(); await selectFileView(page, "Preview"); await expect(visibleFilePane.getByText("Saved from source", { exact: true })).toBeVisible(); await expect(toolbar.getByLabel("Line 1, column 1")).toHaveCount(0); await expect.poll(() => readFile(markdownPath, "utf8")).toBe("# Saved from source\n"); await expect(page.getByTestId("workspace-tab-modified-file_notes.md")).not.toBeVisible(); await openWorkspaceFile(page, "pixel.png"); const image = visibleFilePane.locator("img"); await expect(image).toBeVisible(); const imageCanvas = page.getByTestId("image-file-preview-canvas"); await expect(imageCanvas).toBeVisible(); await imageCanvas.hover(); const transformedContent = imageCanvas.locator(":scope > div").first(); const fittedImageBox = await transformedContent.boundingBox(); expect(fittedImageBox).not.toBeNull(); await page.getByRole("button", { name: "Zoom in", exact: true }).click(); await expect .poll(async () => (await transformedContent.boundingBox())?.width ?? 0) .toBeGreaterThan(fittedImageBox!.width * 1.2); const initialSource = await image.getAttribute("src"); await writeFile(imagePath, BLUE_PIXEL); await expect.poll(() => image.getAttribute("src")).not.toBe(initialSource); }); test("renders local relative images inside Markdown preview", async ({ page, withWorkspace }) => { test.setTimeout(90_000); const workspace = await withWorkspace({ prefix: "file-md-relative-image-" }); await mkdir(path.join(workspace.repoPath, "docs"), { recursive: true }); await mkdir(path.join(workspace.repoPath, "data", "cache"), { recursive: true }); await writeFile(path.join(workspace.repoPath, "docs", "sibling.png"), RED_PIXEL); await writeFile(path.join(workspace.repoPath, "data", "cache", "hero.png"), RED_PIXEL); await writeFile( path.join(workspace.repoPath, "docs", "guide.md"), [ "# Guide", "", "![sibling](./sibling.png)", "", "![hero](../data/cache/hero.png?v=2#preview)", "", ].join("\n"), "utf8", ); await workspace.navigateTo(); await openFileExplorer(page); await expandFolder(page, "docs"); await openFileFromExplorer(page, "guide.md"); await expectFileTabOpen(page, "docs/guide.md"); const visibleFilePane = page.getByTestId("workspace-file-pane").filter({ visible: true }); await expect(visibleFilePane.getByText("Guide", { exact: true })).toBeVisible(); await expectMarkdownImageLoaded(page, "sibling"); await expectMarkdownImageLoaded(page, "hero"); await visibleFilePane.screenshot({ path: test.info().outputPath("markdown-relative-images.png"), }); }); test("previews and refreshes an HTML plan while preserving source access", async ({ page, withWorkspace, }) => { test.setTimeout(90_000); const workspace = await withWorkspace({ prefix: "file-editing-html-preview-" }); const htmlPath = path.join(workspace.repoPath, "plan.html"); await writeFile( htmlPath, "

Visual plan

", "utf8", ); await workspace.navigateTo(); await openWorkspaceFile(page, "plan.html"); const preview = htmlPreview(page); await expect(preview.host).toBeVisible(); await expect(preview.host).toHaveAttribute("sandbox", /allow-scripts/); await expect(preview.host).not.toHaveAttribute("sandbox", /allow-same-origin/); await expect(preview.document.getByRole("heading", { name: "Visual plan" })).toBeVisible(); await writeFile( htmlPath, "

Updated plan

", "utf8", ); await expect(preview.document.getByRole("heading", { name: "Updated plan" })).toBeVisible(); await selectFileView(page, "Source"); await expect(page.getByTestId("file-source-editor")).toBeVisible(); await expect(preview.host).toHaveCount(0); await selectFileView(page, "Preview"); await expect(preview.host).toBeVisible(); }); test("runs inline scripts without allowing fetch egress", async ({ page, withWorkspace }) => { test.setTimeout(90_000); const workspace = await withWorkspace({ prefix: "file-editing-html-csp-" }); await writeFile( path.join(workspace.repoPath, "probe.html"), `probe

Inline script did not run

Network request not attempted

Standards mode not detected

`, "utf8", ); await workspace.navigateTo(); const blockedRequests = watchRequestsTo(page, BLOCKED_PREVIEW_URL); await openWorkspaceFile(page, "probe.html"); const preview = htmlPreview(page); await expect( preview.document.getByRole("heading", { name: "Inline script ran" }), ).toBeVisible(); await expect( preview.document.getByText("Standards mode enabled", { exact: true }), ).toBeVisible(); await expect( preview.document.getByText("Network request blocked", { exact: true }), ).toBeVisible(); expect(blockedRequests).toEqual([]); }); test("isolates HTML plans from the app origin and storage", async ({ page, withWorkspace }) => { test.setTimeout(90_000); const workspace = await withWorkspace({ prefix: "file-editing-html-origin-" }); await writeFile( path.join(workspace.repoPath, "origin.html"), `

?

?

`, "utf8", ); await workspace.navigateTo(); await openWorkspaceFile(page, "origin.html"); const preview = htmlPreview(page); await expect(preview.document.getByText("Parent DOM blocked", { exact: true })).toBeVisible(); await expect(preview.document.getByText("Storage blocked", { exact: true })).toBeVisible(); await expect(preview.document.getByText("Cookies blocked", { exact: true })).toBeVisible(); }); test("persists Vim keybindings and reports Vim mode with cursor position", async ({ page, withWorkspace, }) => { test.setTimeout(90_000); const workspace = await withWorkspace({ prefix: "file-editing-vim-" }); await writeFile(path.join(workspace.repoPath, "vim.ts"), "const vim = true;\n", "utf8"); await page.goto("/settings/editor"); const toggle = page.getByRole("switch", { name: "Vim keybindings" }); await expect(toggle).toBeVisible(); await toggle.click(); await expect(toggle).toBeChecked(); await page.reload(); await expect(page.getByRole("switch", { name: "Vim keybindings" })).toBeChecked(); await workspace.navigateTo(); await openWorkspaceFile(page, "vim.ts"); await expect(page.getByLabel("Vim mode NORMAL")).toBeVisible(); await expect(page.getByLabel("Line 1, column 1")).toBeVisible(); await editor(page).click(); await editor(page).press("i"); await expect(page.getByLabel("Vim mode INSERT")).toBeVisible(); await editor(page).press("Escape"); await expect(page.getByLabel("Vim mode NORMAL")).toBeVisible(); }); }); async function openAssistantMarkdownLink(page: Page, name: string): Promise { await page.getByRole("link", { name, exact: true }).first().click(); } async function returnToAssistant(page: Page, agentId: string): Promise { await page.getByTestId(`workspace-tab-agent_${agentId}`).filter({ visible: true }).click(); } async function openAssistantInlinePath(page: Page, token: string): Promise { await page.getByText(token, { exact: true }).click(); } async function expectPreviewText(page: Page, text: string): Promise { await expect( page .getByTestId("workspace-file-pane") .filter({ visible: true }) .getByText(text, { exact: true }), ).toBeVisible(); }