import { expect } from "vitest"; import { control, createAndSelectWorkspace, evalIn, waitFor } from "@openwork/behaviors"; import type { Surface } from "@openwork/cdp"; import { screenshot, validate } from "@openwork/test-evidence"; import { desktop } from "@openwork/hosts"; import { needs, test } from "@openwork/testkit"; const e2eTestsEnabled = process.env.OPENWORK_EVAL_E2E_TESTS === "1"; const title = e2eTestsEnabled ? "markdown artifacts autosave without Save/Discard buttons and format from the right-click menu" : "markdown editor autosave skipped — needs: set OPENWORK_EVAL_E2E_TESTS=1"; const activeArtifactPath = "artifacts/overflow-tab-12.md"; const untouchedArtifactPath = "artifacts/overflow-tab-11.md"; const typedSentence = "Autosaved without buttons."; const sleep = (ms: number): Promise => new Promise((resolve) => setTimeout(resolve, ms)); function isRecord(value: unknown): value is Record { return typeof value === "object" && value !== null && !Array.isArray(value); } function readFileExpression(workspaceId: string, path: string): string { return `(async () => { const port = localStorage.getItem("openwork.server.port"); const token = localStorage.getItem("openwork.server.token"); if (!port || !token) return ""; const response = await fetch( "http://127.0.0.1:" + port + "/workspace/" + encodeURIComponent(${JSON.stringify(workspaceId)}) + "/files/content?path=" + encodeURIComponent(${JSON.stringify(path)}), { headers: { Authorization: "Bearer " + token } }, ); if (!response.ok) return ""; const json = await response.json(); return typeof json.content === "string" ? json.content : ""; })()`; } async function waitForFileContent( app: Surface, workspaceId: string, path: string, predicate: (content: string) => boolean, label: string, ): Promise { let last = ""; for (let attempt = 0; attempt < 60; attempt += 1) { const value = await evalIn(app, readFileExpression(workspaceId, path), { awaitPromise: true }); last = typeof value === "string" ? value : ""; if (predicate(last)) return last; await sleep(250); } throw new Error(`${label}: file ${path} never matched; last content was ${JSON.stringify(last)}`); } async function rightClick(app: Surface, selector: string): Promise { const rawPoint = await evalIn(app, `(() => { const target = document.querySelector(${JSON.stringify(selector)}); if (!(target instanceof HTMLElement)) return ""; target.scrollIntoView({ block: "center" }); const rect = target.getBoundingClientRect(); return JSON.stringify({ x: rect.left + rect.width / 2, y: rect.top + rect.height / 2 }); })()`); if (typeof rawPoint !== "string" || !rawPoint) throw new Error(`Right-click target ${selector} did not return coordinates.`); const point: unknown = JSON.parse(rawPoint); if (!isRecord(point) || typeof point.x !== "number" || typeof point.y !== "number") { throw new Error(`Right-click target ${selector} returned invalid coordinates: ${rawPoint}`); } await app.client.send("Input.dispatchMouseEvent", { type: "mouseMoved", x: point.x, y: point.y }); await app.client.send("Input.dispatchMouseEvent", { type: "mousePressed", x: point.x, y: point.y, button: "right", clickCount: 1 }); await app.client.send("Input.dispatchMouseEvent", { type: "mouseReleased", x: point.x, y: point.y, button: "right", clickCount: 1 }); } test.skipIf(!e2eTestsEnabled)(title, async ({ evidence }) => { needs({ optIn: ["OPENWORK_EVAL_E2E_TESTS"] }); await using app = await desktop({ name: "markdown-editor-autosave", mode: process.env.OPENWORK_EVAL_CDP_URL?.trim() ? "attach" : "spawn", }); const workspace = await createAndSelectWorkspace(app, { path: `/tmp/openwork-markdown-editor-autosave-${Date.now()}`, }); await waitFor(app, `window.__openworkControl.listActions().some((action) => action.id === "session.create_task" && !action.disabled)`, { timeoutMs: 30_000, label: "new task action enabled", }); await control(app, "session.create_task"); await waitFor(app, `String(window.__openworkControl.snapshot().route || "").includes("/session/")`, { timeoutMs: 30_000, label: "session route open", }); // Mount the side panel so the dev seed action registers, then open markdown artifacts. const seedReady = await evalIn(app, `window.__openworkControl.listActions().some((action) => action.id === "eval.artifact_tabs.seed_overflow" && !action.disabled)`); if (seedReady !== true) { // browser.open_url sets the side panel open before navigating; the // navigation itself can race the internal tab bootstrap, which is fine — // we only need the panel mounted so its dev seed action registers. await control(app, "browser.open_url", { url: "about:blank" }).catch(() => undefined); } await waitFor(app, `window.__openworkControl.listActions().some((action) => action.id === "eval.artifact_tabs.seed_overflow" && !action.disabled)`, { timeoutMs: 30_000, label: "artifact seed action enabled", }); await control(app, "eval.artifact_tabs.seed_overflow", { count: 12 }); await waitFor(app, "Boolean(window.__artifactEditorView)", { timeoutMs: 30_000, label: "markdown artifact opened directly in the editor", }); const header = await evalIn(app, `(() => { const buttons = Array.from(document.querySelectorAll("button")).map((button) => (button.textContent ?? "").trim()); return JSON.stringify({ hasSave: buttons.includes("Save") || buttons.includes("Saving"), hasDiscard: buttons.includes("Discard"), mentionsBytes: /\\d+(\\.\\d+)?\\s*(bytes|KB|MB|GB|TB)/.test(document.body.innerText), }); })()`); if (typeof header !== "string") throw new Error("Header probe did not return JSON."); const headerState: unknown = JSON.parse(header); if (!isRecord(headerState)) throw new Error(`Header probe returned invalid JSON: ${header}`); expect(headerState.hasSave).toBe(false); expect(headerState.hasDiscard).toBe(false); expect(headerState.mentionsBytes).toBe(false); evidence.recordAssertionEvidence( "The markdown editor exposes no Save or Discard buttons and no file byte count", "With a markdown artifact open for direct editing, no button was labelled Save, Saving, or Discard, and no byte/KB size readout was visible.", true, ); const baseline = await waitForFileContent( app, workspace.workspaceId, activeArtifactPath, (content) => content.startsWith("# Overflow tab 12"), "seeded artifact on disk", ); const untouchedBaseline = await waitForFileContent( app, workspace.workspaceId, untouchedArtifactPath, (content) => content.startsWith("# Overflow tab 11"), "sibling artifact on disk", ); const typed = await evalIn(app, `(() => { const view = window.__artifactEditorView; if (!view) return false; view.dispatch({ changes: { from: view.state.doc.length, insert: "\\n" + ${JSON.stringify(typedSentence)} } }); return true; })()`); expect(typed).toBe(true); const savedContent = await waitForFileContent( app, workspace.workspaceId, activeArtifactPath, (content) => content.includes(typedSentence), "autosave after typing", ); expect(savedContent).toBe(`${baseline}\n${typedSentence}`); await waitFor(app, `document.body.innerText.includes("Saved")`, { timeoutMs: 15_000, label: "Saved indicator after autosave", }); evidence.recordAssertionEvidence( "Typing in the markdown editor persists to disk without pressing any button", `The artifact file content became exactly the baseline plus ${JSON.stringify(typedSentence)} and the header showed Saved.`, true, ); // Format the first line via the right-click menu. const selected = await evalIn(app, `(() => { const view = window.__artifactEditorView; if (!view) return false; view.dispatch({ selection: { anchor: 0 } }); return true; })()`); expect(selected).toBe(true); await rightClick(app, ".cm-line"); await waitFor(app, `(() => { const labels = [...document.querySelectorAll('[role="menuitem"]')].map((item) => item.textContent?.trim()); return ["Heading 1", "Heading 2", "Heading 3", "Bold", "Bullet list", "Quote"].every((label) => labels.includes(label)); })()`, { timeoutMs: 10_000, label: "markdown formatting context menu open" }); { const shot = await screenshot(app); const seen = await validate(shot, [ "A markdown editor is open with a right-click context menu offering Heading 1, Heading 2, Heading 3 and other formatting options", "The editor header shows a Saved status label and contains no button labelled Save or Discard", "No error dialog or crash message is visible", ]); expect(seen.ok, seen.why).toBe(true); } const clickedHeading = await evalIn(app, `(() => { const item = [...document.querySelectorAll('[role="menuitem"]')] .find((entry) => entry.textContent?.trim() === "Heading 2"); if (!(item instanceof HTMLElement)) return false; item.click(); return true; })()`); expect(clickedHeading).toBe(true); await waitFor(app, `window.__artifactEditorView?.state.doc.line(1).text.startsWith("## ")`, { timeoutMs: 10_000, label: "first line converted to a level-2 heading", }); const headingContent = await waitForFileContent( app, workspace.workspaceId, activeArtifactPath, (content) => content.startsWith("## Overflow tab 12"), "autosave after heading formatting", ); expect(headingContent).toBe(`#${baseline}\n${typedSentence}`); evidence.recordAssertionEvidence( "The right-click menu converts the current line to the chosen heading level and the change autosaves", "Heading 2 rewrote the first line from `# Overflow tab 12` to `## Overflow tab 12`, and the file on disk matched exactly.", true, ); const untouchedAfter = await evalIn(app, readFileExpression(workspace.workspaceId, untouchedArtifactPath), { awaitPromise: true }); expect(untouchedAfter).toBe(untouchedBaseline); evidence.recordAssertionEvidence( "Autosave only writes the artifact being edited", "A sibling markdown artifact opened in another tab kept byte-identical content after edits and formatting in the active tab.", true, ); });