import type { Locator, Page, Route } from "@playwright/test" import { FLOWCHART_XML } from "./fixtures/diagrams" import { expect, getAttachmentInput, getChatInput, getIframe, getIframeContent, getSendButton, openDrawioFile, openSettingsTab, sendMessage, test, waitForComplete, waitForCompleteCount, } from "./lib/fixtures" import { createMockSSEResponse } from "./lib/helpers" /** An edit_diagram response that adds one node next to "Start" */ function createEditResponse() { const toolCallId = `call_edit_${Date.now()}` const newNode = `` const events = [ { type: "start", messageId: `msg_${Date.now()}` }, { type: "tool-input-start", toolCallId, toolName: "edit_diagram" }, { type: "tool-input-available", toolCallId, toolName: "edit_diagram", input: { operations: [ { operation: "add", cell_id: "review", new_xml: newNode }, ], }, }, { type: "finish" }, ] return ( events.map((e) => `data: ${JSON.stringify(e)}\n\n`).join("") + "data: [DONE]\n\n" ) } /** First request draws the flowchart, later ones add a node */ function drawThenEdit(onRequest?: (body: any) => void) { let count = 0 return async (route: Route) => { onRequest?.(route.request().postDataJSON()) const body = count === 0 ? createMockSSEResponse(FLOWCHART_XML, "Here is the flowchart.") : createEditResponse() count++ await route.fulfill({ status: 200, contentType: "text/event-stream", body, }) } } async function openApp(page: Page) { await page.goto("/", { waitUntil: "networkidle" }) await getIframe(page).waitFor({ state: "visible", timeout: 30000 }) // The cover goes away once draw.io has loaded and been styled await expect(page.locator('[data-testid="canvas-loading"]')).toHaveCount( 0, { timeout: 30000, }, ) } // A page with a name and a background, and no shapes const BLANK_SHEET = '' const TWO_PAGE_FILE = `${["Old A", "Old B"] .map( (label, i) => ``, ) .join("")}` /** When each saved chat was last saved, by title */ function sessionTimes(page: Page) { return page.evaluate(async () => { const db: IDBDatabase = await new Promise((resolve) => { const request = indexedDB.open("next-ai-drawio") request.onsuccess = () => resolve(request.result) }) const sessions: any[] = await new Promise((resolve) => { const request = db .transaction("sessions") .objectStore("sessions") .getAll() request.onsuccess = () => resolve(request.result) }) db.close() return Object.fromEntries(sessions.map((s) => [s.title, s.updatedAt])) }) } /** Where an element is laid out (fails the test when it is not) */ async function boxOf(locator: Locator) { const box = await locator.boundingBox() if (!box) throw new Error("element is not laid out") return box } async function drawAndEdit(page: Page) { await sendMessage(page, "Create a flowchart") await waitForComplete(page) await sendMessage(page, "Add a review step") await waitForCompleteCount(page, 2) } test.describe("Workspace", () => { test("start screen shows on an empty canvas and examples fill the input", async ({ page, }) => { await openApp(page) await expect(page.locator('[data-testid="lobby-hero"]')).toBeVisible() await page.locator('[data-testid="example-cat"]').click() await expect(getChatInput(page)).toHaveValue("Draw a cat for me") }) test("each AI change becomes a version card; the latest undoes and redoes it", async ({ page, }) => { await page.route("**/api/chat", drawThenEdit()) await openApp(page) await drawAndEdit(page) const cards = page.locator('[data-testid="version-card"]') await expect(cards).toHaveCount(2) await expect(cards.nth(1)).toContainText("v2") // Only the added shape counts: the others kept their XML as saved await expect(cards.nth(1)).toContainText("Added 1 shape") await expect(cards.nth(1)).not.toContainText("changed") await expect(page.locator('[data-testid="version-thumb"]')).toHaveCount( 2, ) const canvas = getIframeContent(page) await expect(canvas.getByText("Review Step")).toBeVisible() await page.locator('[data-testid="version-undo"]').click() await expect(canvas.getByText("Review Step")).toHaveCount(0) await expect(page.getByText("Change undone")).toBeVisible() await page.locator('[data-testid="version-undo"]').click() await expect(canvas.getByText("Review Step")).toBeVisible() }) test("a renamed diagram keeps its name after reload", async ({ page }) => { await page.route("**/api/chat", drawThenEdit()) await openApp(page) await sendMessage(page, "Create a flowchart") await waitForComplete(page) await page.locator('[data-testid="session-title"]').click() await page.locator('[data-testid="rename-session"]').click() const input = page.getByRole("textbox", { name: "Rename" }) await input.fill("Order flow") await input.press("Enter") await page.keyboard.press("Escape") await expect( page.locator('[data-testid="session-title"]'), ).toContainText("Order flow") await expect(page).toHaveURL(/session=/) await page.waitForTimeout(1500) await page.reload({ waitUntil: "networkidle" }) await expect( page.locator('[data-testid="session-title"]'), ).toContainText("Order flow", { timeout: 15000 }) await expect(page.locator('[data-testid="version-card"]')).toHaveCount( 1, ) }) test("opening a .drawio file loads it into a new diagram", async ({ page, }) => { await openApp(page) const file = `` const [chooser] = await Promise.all([ page.waitForEvent("filechooser"), page .getByRole("button", { name: "Open .drawio file" }) .first() .click(), ]) await chooser.setFiles({ name: "order-flow.drawio", mimeType: "application/xml", buffer: Buffer.from(file), }) await expect( getIframeContent(page).getByText("From File"), ).toBeVisible() await expect( page.locator('[data-testid="session-title"]'), ).toContainText("order-flow") }) test("a file with a comment or DOCTYPE before its root opens", async ({ page, }) => { await openApp(page) await openDrawioFile( page, "commented.drawio", `\n\n\n`, ) await expect( getIframeContent(page).getByText("Commented"), ).toBeVisible() }) test("attachments do not go through the file opener", async ({ page }) => { await openApp(page) await getAttachmentInput(page).setInputFiles({ name: "notes.txt", mimeType: "text/plain", buffer: Buffer.from("hello"), }) await expect(page.getByText("notes.txt")).toBeVisible() await expect(page.locator('[data-testid="lobby-hero"]')).toBeVisible() }) test("an error shows its text and can be retried", async ({ page }) => { let calls = 0 await page.route("**/api/chat", async (route) => { calls++ if (calls === 1) { await route.fulfill({ status: 500, contentType: "application/json", body: JSON.stringify({ error: "Model is overloaded" }), }) return } await route.fulfill({ status: 200, contentType: "text/event-stream", body: createMockSSEResponse(FLOWCHART_XML, "Here it is."), }) }) await openApp(page) await sendMessage(page, "Create a flowchart") const alert = page .getByRole("alert") .filter({ hasText: "Model is overloaded" }) await expect(alert).toBeVisible({ timeout: 10000 }) await expect(alert).not.toContainText("{") await page.locator('[data-testid="retry-button"]').click() await waitForComplete(page) expect(calls).toBe(2) }) test("a file opened inside a saved diagram stays on the canvas", async ({ page, }) => { await page.route("**/api/chat", drawThenEdit()) await openApp(page) await sendMessage(page, "Create a flowchart") await waitForComplete(page) // The auto-save gives the chat a session first await expect(page).toHaveURL(/session=/, { timeout: 10000 }) const file = `` await openDrawioFile(page, "mine.drawio", file) await page.waitForTimeout(1500) await expect( getIframeContent(page).getByText("From File"), ).toBeVisible() }) test("Ctrl+B in the canvas stays with draw.io", async ({ page }) => { await page.route("**/api/chat", drawThenEdit()) await openApp(page) await sendMessage(page, "Create a flowchart") await waitForComplete(page) await getIframeContent(page) .getByText("Process", { exact: true }) .click() await page.keyboard.press("ControlOrMeta+b") await page.waitForTimeout(500) await expect(page.locator('[data-testid="chat-panel"]')).toBeVisible() // ⌘⇧B outside the canvas stays with the browser too await page.locator('[data-testid="session-title"]').focus() await page.keyboard.press("ControlOrMeta+Shift+B") await page.waitForTimeout(500) await expect(page.locator('[data-testid="chat-panel"]')).toBeVisible() }) test("selected shapes are shown, sent, and attached again when picked again", async ({ page, }) => { const bodies: any[] = [] await page.route( "**/api/chat", drawThenEdit((body) => bodies.push(body)), ) await openApp(page) await sendMessage(page, "Create a flowchart") await waitForComplete(page) const shape = getIframeContent(page).getByText("Process", { exact: true, }) await shape.click() await expect( page.locator('[data-testid="selection-chip"]'), ).toContainText("Process") await expect( page.locator('[data-testid="selection-ask"]'), ).toBeVisible() await sendMessage(page, "Make this red") await waitForCompleteCount(page, 2) expect(bodies[1].selectedCells).toEqual([ { id: "process", label: "Process" }, ]) // Click away, then pick the same shape await page.keyboard.press("Escape") await shape.click() await expect( page.locator('[data-testid="selection-chip"]'), ).toContainText("Process") await sendMessage(page, "Make it bigger") // (Its edit repeats the earlier one, so the retries that follow // are requests too) await expect.poll(() => bodies.length).toBeGreaterThanOrEqual(3) expect(bodies[2].selectedCells).toEqual([ { id: "process", label: "Process" }, ]) }) test("restoring a version keeps the zoom", async ({ page }) => { await page.route("**/api/chat", drawThenEdit()) await openApp(page) await drawAndEdit(page) // The percentage shown in the toolbar const zoom = () => page.getByRole("button", { name: "Zoom", exact: true }).innerText() const initial = await zoom() await page.getByRole("button", { name: "Zoom out" }).click() await expect.poll(zoom).not.toBe(initial) const before = await zoom() await page.locator('[data-testid="version-restore"]').first().click() await page.waitForTimeout(800) expect(await zoom()).toBe(before) await expect(page.getByText("Restored v1")).toBeVisible() }) test("other diagrams can't be opened while an answer streams", async ({ page, }) => { await page.route("**/api/chat", drawThenEdit()) await openApp(page) await sendMessage(page, "Create a flowchart") await waitForComplete(page) await page.waitForTimeout(1500) await page.locator('[data-testid="new-chat-button"]').click() await expect(page.locator('[data-testid="lobby-hero"]')).toBeVisible() // An answer that takes a while await page.unroute("**/api/chat") await page.route("**/api/chat", async (route) => { await new Promise((r) => setTimeout(r, 4000)) await route.fulfill({ status: 200, contentType: "text/event-stream", body: createMockSSEResponse(FLOWCHART_XML, "Second one."), }) }) await sendMessage(page, "Create another flowchart") await page.locator('[data-testid="session-title"]').click() const other = page.getByRole("button", { name: /Create a flowchart/ }) await expect(other).toHaveAttribute("aria-disabled", "true") await other.click({ force: true }) await waitForComplete(page) await expect(page.getByText("Second one.")).toBeVisible() }) test("the Shapes button and its shortcut open the shape library", async ({ page, }) => { await openApp(page) await page.locator('[data-testid="draw-yourself"]').click() const library = getIframeContent(page).locator( ".geSidebarContainer:not(.geFormatContainer)", ) await expect(library).toBeHidden() await page.locator('[data-testid="toolbar-shapes"]').click() await expect(library).toBeVisible() await page.locator('[data-testid="toolbar-shapes"]').click() await expect(library).toBeHidden() await page.keyboard.press("ControlOrMeta+Shift+K") await expect(library).toBeVisible() // Also with the focus in the canvas await getIframeContent(page) .locator(".geDiagramContainer") .click({ position: { x: 300, y: 300 } }) await page.keyboard.press("ControlOrMeta+Shift+K") await expect(library).toBeHidden() }) test("resizing the window leaves draw.io's style panel closed", async ({ page, }) => { await page.route("**/api/chat", drawThenEdit()) await openApp(page) await sendMessage(page, "Create a flowchart") await waitForComplete(page) const formatWidth = () => getIframeContent(page) .locator(".geFormatContainer") .evaluate((el) => (el as HTMLElement).offsetWidth) expect(await formatWidth()).toBe(0) // draw.io's own rule opens it when its window gets wide again await page.setViewportSize({ width: 1000, height: 800 }) await page.waitForTimeout(600) await page.setViewportSize({ width: 1440, height: 900 }) await page.waitForTimeout(1500) expect(await formatWidth()).toBe(0) }) test("crossing the phone width keeps draw.io and its undo history", async ({ page, }) => { await page.route("**/api/chat", drawThenEdit()) await openApp(page) await sendMessage(page, "Create a flowchart") await waitForComplete(page) // A mark on draw.io's window: a reload would make a new window const mark = (set: boolean) => getIframe(page).evaluate((el, set) => { const w = (el as HTMLIFrameElement).contentWindow as any if (set) w.__naiMark = 1 return w.__naiMark }, set) await mark(true) await page.setViewportSize({ width: 700, height: 800 }) await page.waitForTimeout(800) await page.setViewportSize({ width: 1280, height: 800 }) await page.waitForTimeout(800) expect(await mark(false)).toBe(1) await expect(page.locator('[data-testid="toolbar-undo"]')).toBeEnabled() }) test("Ctrl+Z in the canvas undoes the AI change in one step; the card follows undo and redo", async ({ page, }) => { await page.route("**/api/chat", drawThenEdit()) await openApp(page) await drawAndEdit(page) await expect( page.locator('[data-testid="version-card"] img'), ).toHaveCount(2) await expect(page).toHaveURL(/session=/) await page.waitForTimeout(1500) const canvas = getIframeContent(page) await canvas .locator(".geDiagramContainer") .click({ position: { x: 10, y: 10 } }) await page.keyboard.press("ControlOrMeta+z") await expect(canvas.getByText("Review Step")).toHaveCount(0) await expect( canvas.getByText("Process", { exact: true }).first(), ).toBeVisible() // The card says v2's change is undone, as its own Undo would const button = page .locator('[data-testid="version-card"]') .nth(1) .locator('[data-testid="version-undo"]') await expect(button).toHaveText("Redo change") // Redo in the canvas puts v2 back on it await page.keyboard.press("ControlOrMeta+Shift+z") await expect(canvas.getByText("Review Step")).toBeVisible() await expect(button).toHaveText("Undo change") await page.keyboard.press("ControlOrMeta+z") await expect(button).toHaveText("Redo change") await button.click() await expect(canvas.getByText("Review Step")).toBeVisible() await expect(button).toHaveText("Undo change") }) test("past 20 versions the numbers stay; a compare open on the dropped one lets go", async ({ page, }) => { const boxFile = (label: string) => `` await page.route("**/api/chat", async (route) => { await new Promise((resolve) => setTimeout(resolve, 1500)) await route.fulfill({ status: 200, contentType: "text/event-stream", body: createMockSSEResponse(FLOWCHART_XML, "Flowchart."), }) }) await openApp(page) // A chat that already has 20 versions await page.evaluate( async ({ versions, diagramXml }) => { const db: IDBDatabase = await new Promise((resolve) => { const request = indexedDB.open("next-ai-drawio") request.onsuccess = () => resolve(request.result) }) const now = Date.now() await new Promise((resolve) => { const tx = db.transaction("sessions", "readwrite") tx.objectStore("sessions").put({ id: "many", title: "Many versions", createdAt: now, updatedAt: now, messages: [ { id: "u1", role: "user", parts: [{ type: "text", text: "Draw boxes" }], }, { id: "a1", role: "assistant", parts: [{ type: "text", text: "Boxes." }], }, ], xmlSnapshots: [], diagramXml, versions, }) tx.oncomplete = resolve }) db.close() }, { versions: Array.from({ length: 20 }, (_, i) => ({ id: `v${i + 1}`, number: i + 1, xml: boxFile(`Box ${i + 1}`), svg: "", turnIndex: 0, summary: { shapesAdded: 0, shapesRemoved: 0, shapesChanged: 1, edgesAdded: 0, edgesRemoved: 0, edgesChanged: 0, }, })), diagramXml: boxFile("Box 20"), }, ) await page.goto("/?session=many", { waitUntil: "networkidle" }) const canvas = getIframeContent(page) await expect(canvas.getByText("Box 20")).toBeVisible({ timeout: 30000 }) const thumbs = page.locator('[data-testid="version-thumb"]') await expect(thumbs).toHaveCount(20) // Compare v1 while the next answer is on its way await sendMessage(page, "Draw a flowchart") await thumbs.first().click() await expect(page.getByRole("dialog")).toBeVisible() await waitForComplete(page) await expect(page.getByRole("dialog")).toHaveCount(0) await expect(thumbs.first()).toContainText("v2") await expect(thumbs.last()).toContainText("v21") await expect( page.locator('[data-testid="version-card"]').last(), ).toContainText("v21") // The first Escape in the canvas is draw.io's again: it ends editing await canvas.getByText("Start", { exact: true }).dblclick() await expect(canvas.locator(".mxCellEditor")).toBeVisible() await page.keyboard.press("Escape") await expect(canvas.locator(".mxCellEditor:visible")).toHaveCount(0) }) test("a drawing cut off by a dropped connection does not show as running", async ({ page, }) => { // The stream ends in the middle of the tool call const events = [ { type: "start", messageId: "m1" }, { type: "tool-input-start", toolCallId: "d1", toolName: "display_diagram", }, { type: "tool-input-delta", toolCallId: "d1", inputTextDelta: '{"xml":" route.fulfill({ status: 200, contentType: "text/event-stream", body: events .map((e) => `data: ${JSON.stringify(e)}\n\n`) .join(""), }), ) await openApp(page) await sendMessage(page, "Draw") await expect(getSendButton(page)).toBeVisible({ timeout: 15000 }) await expect(page.getByText("Stopped", { exact: true })).toBeVisible() await expect(page.getByText("Drawing the diagram…")).toHaveCount(0) }) test("a short window keeps the send button on screen with a long message", async ({ page, }) => { await page.route("**/api/chat", drawThenEdit()) await page.setViewportSize({ width: 1280, height: 320 }) await openApp(page) await sendMessage(page, "Create a flowchart") await waitForComplete(page) await getChatInput(page).fill( Array.from({ length: 30 }, (_, i) => `line ${i}`).join("\n"), ) const send = await boxOf(getSendButton(page)) expect(send.y + send.height).toBeLessThanOrEqual(320) }) test("settings put their sections on top in a narrow window", async ({ page, }) => { await page.setViewportSize({ width: 800, height: 800 }) await openApp(page) await page.locator('[data-testid="settings-button"]').first().click() const models = page.locator('[data-testid="settings-tab-models"]') const appearance = page.locator( '[data-testid="settings-tab-appearance"]', ) await expect(models).toBeVisible() const a = await boxOf(models) const b = await boxOf(appearance) expect(Math.abs(a.y - b.y)).toBeLessThan(2) }) test("New chat brings the start screen back", async ({ page }) => { await openApp(page) await page.locator('[data-testid="draw-yourself"]').click() await expect(page.locator('[data-testid="lobby-hero"]')).toHaveCount(0) await page.locator('[data-testid="new-chat-button"]').click() await expect(page.locator('[data-testid="lobby-hero"]')).toBeVisible() }) test("Enter that picks an IME candidate does not end a rename", async ({ page, }) => { await page.route("**/api/chat", drawThenEdit()) await openApp(page) await sendMessage(page, "Create a flowchart") await waitForComplete(page) await page.locator('[data-testid="session-title"]').click() await page.locator('[data-testid="rename-session"]').click() const input = page.getByRole("textbox", { name: "Rename" }) await input.fill("流程") await input.dispatchEvent("keydown", { key: "Enter", isComposing: true, }) await expect(input).toBeVisible() // Safari ends the composition first, then sends keyCode 229 await input.dispatchEvent("keydown", { key: "Enter", keyCode: 229 }) await expect(input).toBeVisible() await input.fill("流程图") await input.press("Enter") await expect(input).toHaveCount(0) await page.keyboard.press("Escape") await expect( page.locator('[data-testid="session-title"]'), ).toContainText("流程图") }) test("the restore message's Undo goes away with the chat", async ({ page, }) => { await page.route("**/api/chat", drawThenEdit()) await openApp(page) await drawAndEdit(page) await page.locator('[data-testid="version-thumb"]').first().click() await page.locator('[data-testid="compare-restore"]').click() const toast = page.getByText("Restored v1") await expect(toast).toBeVisible() await page.locator('[data-testid="new-chat-button"]').click() await expect(toast).toHaveCount(0) }) test("a multi-page diagram's AI change can be undone from its card", async ({ page, }) => { await page.route("**/api/chat", drawThenEdit()) await openApp(page) await openDrawioFile(page, "two.drawio", TWO_PAGE_FILE) const canvas = getIframeContent(page) await expect(canvas.getByText("Old A")).toBeVisible() await sendMessage(page, "Create a flowchart") await waitForComplete(page) // Multi-page documents are loaded in full: the card is the way back await page.waitForTimeout(2000) await page.locator('[data-testid="version-undo"]').click() await expect(canvas.getByText("Old A")).toBeVisible() await expect(page.getByText("Change undone")).toBeVisible() }) test("reopening a chat keeps its card's undo and does not save it again", async ({ page, }) => { await page.route("**/api/chat", drawThenEdit()) await openApp(page) await sendMessage(page, "Create a flowchart") await waitForComplete(page) await expect(page).toHaveURL(/session=/) await page.waitForTimeout(2000) const updatedAt = () => sessionTimes(page) // Leaving the page saves the chat; opening it must not save again await page.reload({ waitUntil: "networkidle" }) await expect(page.locator('[data-testid="version-undo"]')).toBeVisible({ timeout: 15000, }) const opened = await updatedAt() await page.waitForTimeout(3000) await expect(page.locator('[data-testid="version-undo"]')).toBeVisible() expect(await updatedAt()).toEqual(opened) }) test("restoring from the compare dialog; a new answer takes its Undo away", async ({ page, }) => { await page.route("**/api/chat", drawThenEdit()) await openApp(page) await drawAndEdit(page) await page.locator('[data-testid="version-thumb"]').first().click() await expect(page.getByRole("dialog")).toContainText("Version 1") await page.locator('[data-testid="compare-restore"]').click() await expect( getIframeContent(page).getByText("Review Step"), ).toHaveCount(0) const toast = page.getByText("Restored v1") await expect(toast).toBeVisible() await sendMessage(page, "Add a review step") await expect(toast).toHaveCount(0) }) test("a broken file leaves the chat on screen", async ({ page }) => { await page.route("**/api/chat", drawThenEdit()) await openApp(page) await sendMessage(page, "Create a flowchart") await waitForComplete(page) await openDrawioFile( page, "cut.drawio", '', ) await expect( page.getByText("cut.drawio isn't a draw.io diagram."), ).toBeVisible() // A page that is neither a model nor draw.io's compressed form await openDrawioFile( page, "packed.drawio", 'not-base64!', ) await expect( page.getByText("packed.drawio isn't a draw.io diagram."), ).toBeVisible() await expect(page.getByText("Create a flowchart").first()).toBeVisible() await expect(page.locator('[data-testid="version-card"]')).toHaveCount( 1, ) }) test("a file that is not a diagram leaves the start screen", async ({ page, }) => { await openApp(page) const [chooser] = await Promise.all([ page.waitForEvent("filechooser"), page .getByRole("button", { name: "Open .drawio file" }) .first() .click(), ]) await chooser.setFiles({ name: "notes.xml", mimeType: "application/xml", buffer: Buffer.from("hi"), }) await expect( page.getByText("notes.xml isn't a draw.io diagram."), ).toBeVisible() await expect(page.locator('[data-testid="lobby-hero"]')).toBeVisible() }) test("draw.io loads once when the saved language is another", async ({ page, }) => { await page.addInitScript(() => { localStorage.setItem("next-ai-draw-io-locale", "ja") }) // Requests for draw.io's page, also one dropped for a newer frame let loads = 0 page.on("request", (request) => { if (new URL(request.url()).pathname.endsWith("/drawio/index.html")) loads++ }) await page.goto("/en", { waitUntil: "networkidle" }) await expect(page).toHaveURL(/\/ja/) await expect( page.locator('[data-testid="canvas-loading"]'), ).toHaveCount(0, { timeout: 30000 }) await page.waitForTimeout(1000) expect(loads).toBe(1) }) test("in a small window the chat panel leaves the canvas room and the toolbar fits", async ({ page, }) => { await page.addInitScript(() => { localStorage.setItem("next-ai-draw-io-panel-width", "560") }) await page.setViewportSize({ width: 800, height: 800 }) await openApp(page) await page.locator('[data-testid="draw-yourself"]').click() const toolbar = page.getByRole("toolbar") await expect(toolbar).toBeVisible() await page.waitForTimeout(600) const panel = await boxOf(page.locator('[data-testid="chat-panel"]')) const stage = await boxOf(page.locator('[data-testid="canvas-stage"]')) expect(panel.width).toBeLessThanOrEqual(400) expect(stage.width).toBeGreaterThanOrEqual(370) // The short toolbar fits the canvas, export included const bar = await boxOf(toolbar) expect(bar.x).toBeGreaterThanOrEqual(stage.x) expect(bar.x + bar.width).toBeLessThanOrEqual(stage.x + stage.width) await expect( page.locator('[data-testid="toolbar-export"]'), ).toBeVisible() }) test("the latest card keeps its undo when the AI writes default values", async ({ page, }) => { // x="0" and a 0,0 label offset: draw.io leaves these out when it // saves the diagram again const xml = `` await page.route("**/api/chat", (route) => route.fulfill({ status: 200, contentType: "text/event-stream", body: createMockSSEResponse(xml, "Here it is."), }), ) await openApp(page) await sendMessage(page, "Draw two boxes") await waitForComplete(page) await expect(page).toHaveURL(/session=/) await page.waitForTimeout(2000) await expect(page.locator('[data-testid="version-undo"]')).toBeVisible() }) test("the latest card keeps its undo on a page with its own paper size", async ({ page, }) => { await page.route("**/api/chat", drawThenEdit()) await openApp(page) // A3 paper; the AI's diagram names no paper size, so the page keeps it await openDrawioFile( page, "a3.drawio", ``, ) await expect(getIframeContent(page).getByText("Old")).toBeVisible() await sendMessage(page, "Create a flowchart") await waitForComplete(page) await page.waitForTimeout(2000) await expect(page.locator('[data-testid="version-undo"]')).toHaveText( "Undo change", ) }) test("a paper size changed by hand after an AI change takes its Undo away", async ({ page, }) => { await page.route("**/api/chat", drawThenEdit()) await openApp(page) await sendMessage(page, "Create a flowchart") await waitForComplete(page) await page.waitForTimeout(1500) await expect(page.locator('[data-testid="version-undo"]')).toHaveText( "Undo change", ) // A3 in draw.io's own format panel, nothing else changed await page.getByRole("button", { name: "Style" }).click() const paper = getIframeContent(page) .locator(".geFormatContainer select") .first() await expect(paper).toBeAttached() await paper.evaluate((select: HTMLSelectElement) => { select.value = "a3" select.dispatchEvent(new Event("change", { bubbles: true })) }) await expect(page.locator('[data-testid="version-undo"]')).toHaveCount( 0, ) await expect( page.locator('[data-testid="version-restore"]'), ).toBeEnabled() }) test("a hand edit after a restore takes the restore message's Undo away", async ({ page, }) => { await page.route("**/api/chat", drawThenEdit()) await openApp(page) await drawAndEdit(page) await page.locator('[data-testid="version-thumb"]').first().click() await page.locator('[data-testid="compare-restore"]').click() const toast = page.getByText("Restored v1") await expect(toast).toBeVisible() const canvas = getIframeContent(page) await canvas.getByText("Process", { exact: true }).click() await page.keyboard.press("Delete") await expect(canvas.getByText("Process", { exact: true })).toHaveCount( 0, ) await expect(toast).toHaveCount(0) }) test("changing the language saves the chat and keeps it open", async ({ page, }) => { await page.route("**/api/chat", drawThenEdit()) await openApp(page) await sendMessage(page, "Create a flowchart") await waitForComplete(page) // Right after the answer: its auto-save may not have run yet await openSettingsTab(page, "appearance") await page.locator("#language-select").click() await page.getByRole("option", { name: "日本語", exact: true }).click() await expect(page).toHaveURL(/\/ja\?session=/, { timeout: 15000 }) await expect(page.getByText("Create a flowchart").first()).toBeVisible({ timeout: 15000, }) await expect(page.locator('[data-testid="version-card"]')).toHaveCount( 1, ) }) test("the language can't change while an answer runs", async ({ page }) => { await page.route("**/api/chat", async (route) => { await new Promise((resolve) => setTimeout(resolve, 5000)) await route .fulfill({ status: 200, contentType: "text/event-stream", body: createMockSSEResponse(FLOWCHART_XML, "Slow one."), }) .catch(() => {}) }) await openApp(page) await sendMessage(page, "Create a flowchart") await openSettingsTab(page, "appearance") await expect(page.locator("#language-select")).toBeDisabled() }) test("keys reach the canvas after the app's toolbar was used", async ({ page, }) => { await page.route("**/api/chat", drawThenEdit()) await openApp(page) await sendMessage(page, "Create a flowchart") await waitForComplete(page) const canvas = getIframeContent(page) await canvas.getByText("Process", { exact: true }).click() await page.getByRole("button", { name: "Zoom in" }).click() await page.keyboard.press("Delete") await expect(canvas.getByText("Process", { exact: true })).toHaveCount( 0, ) }) test("page menu: rename takes typing, duplicate names the copy", async ({ page, }) => { await openApp(page) await page.locator('[data-testid="draw-yourself"]').click() const tabs = page.locator('[data-testid="page-tabs"]') await tabs.getByRole("button", { name: "Page-1" }).click() await page.getByRole("button", { name: "Duplicate" }).click() await expect(tabs).toContainText("Copy of Page-1") // The copy is the current page now: rename it await tabs.getByRole("button", { name: "Copy of Page-1" }).click() await page.getByRole("button", { name: "Rename" }).click() await page.waitForTimeout(300) await page.keyboard.press("ControlOrMeta+a") await page.keyboard.type("Second") await page.keyboard.press("Enter") await expect(tabs).toContainText("Second") }) test("the layers window lists the layers", async ({ page }) => { await openApp(page) await page.locator('[data-testid="draw-yourself"]').click() await page.locator('[data-testid="toolbar-more"]').click() await page.getByRole("button", { name: "Layers" }).click() await expect( getIframeContent(page) .locator(".mxWindowPane .geToolbarContainer") .first(), ).toBeVisible() }) test("the toolbar follows freehand drawing started in the canvas", async ({ page, }) => { await openApp(page) await page.locator('[data-testid="draw-yourself"]').click() const freehand = page.getByRole("button", { name: "Freehand" }) await getIframeContent(page) .locator(".geDiagramContainer") .click({ position: { x: 300, y: 300 } }) await page.keyboard.press("x") await expect(freehand).toHaveAttribute("aria-pressed", "true") // The Select button stops it, and draw.io's freehand window goes await page.getByRole("button", { name: "Select" }).click() await expect(freehand).not.toHaveAttribute("aria-pressed", "true") await expect( getIframeContent(page).locator(".mxWindow:visible"), ).toHaveCount(0) }) test("the zoom menu shows draw.io's own shortcuts", async ({ page }) => { await openApp(page) await page.locator('[data-testid="draw-yourself"]').click() await page.getByRole("button", { name: "Zoom", exact: true }).click() await expect(page.getByRole("button", { name: /Home/ })).toBeVisible() }) test("opening a chat from the list does not save it again", async ({ page, }) => { let n = 0 await page.route("**/api/chat", (route) => { n++ return route.fulfill({ status: 200, contentType: "text/event-stream", body: createMockSSEResponse(FLOWCHART_XML, `Answer ${n}.`), }) }) await openApp(page) await sendMessage(page, "First chat") await waitForComplete(page) await expect(page).toHaveURL(/session=/) await page.waitForTimeout(1500) await page.locator('[data-testid="new-chat-button"]').click() await sendMessage(page, "Second chat") await waitForComplete(page) await page.waitForTimeout(2500) await page.locator('[data-testid="session-title"]').click() await page.getByRole("button", { name: /First chat/ }).click() await expect(page.getByText("Answer 1.")).toBeVisible() await page.waitForTimeout(500) const opened = await sessionTimes(page) await page.waitForTimeout(3000) expect(await sessionTimes(page)).toEqual(opened) }) test("retrying the first message does not flash the start screen", async ({ page, }) => { let calls = 0 await page.route("**/api/chat", async (route) => { calls++ if (calls === 1) { await route.fulfill({ status: 500, contentType: "application/json", body: JSON.stringify({ error: "Model is overloaded" }), }) return } await new Promise((resolve) => setTimeout(resolve, 800)) await route.fulfill({ status: 200, contentType: "text/event-stream", body: createMockSSEResponse(FLOWCHART_XML, "Here it is."), }) }) await openApp(page) await sendMessage(page, "Create a flowchart") await expect(page.getByText("Model is overloaded").first()).toBeVisible( { timeout: 10000, }, ) // Counts every moment the start screen is in the page await page.evaluate(() => { const w = window as any w.__heroSeen = 0 new MutationObserver(() => { if (document.querySelector('[data-testid="lobby-hero"]')) { w.__heroSeen++ } }).observe(document.body, { childList: true, subtree: true }) }) await page.locator('[data-testid="retry-button"]').click() await waitForComplete(page) expect(await page.evaluate(() => (window as any).__heroSeen)).toBe(0) }) test("a retry stays with its chat when another one comes on screen during the export", async ({ page, }) => { // draw.io's export replies wait until released await page.addInitScript(() => { const w = window as any const held: MessageEvent[] = [] w.__releaseExports = () => { w.__holdExports = false for (const e of held.splice(0)) { const copy = new MessageEvent("message", { data: e.data, origin: e.origin, source: e.source, }) ;(copy as any).__released = true window.dispatchEvent(copy) } } window.addEventListener("message", (event) => { if ( w.__holdExports && !(event as any).__released && typeof event.data === "string" && event.data.includes('"event":"export"') ) { event.stopImmediatePropagation() held.push(event) } }) }) const prompts: string[] = [] await page.route("**/api/chat", async (route) => { const body = route.request().postDataJSON() const last = body.messages.at(-1) prompts.push(last.parts.find((p: any) => p.type === "text").text) if (prompts.length === 2) { await route.fulfill({ status: 500, contentType: "application/json", body: JSON.stringify({ error: "Model is overloaded" }), }) return } await route.fulfill({ status: 200, contentType: "text/event-stream", body: createMockSSEResponse(FLOWCHART_XML, "Answer for B."), }) }) await openApp(page) await sendMessage(page, "Chat B") await waitForComplete(page) await expect(page).toHaveURL(/session=/) const chatB = new URL(page.url()).searchParams.get("session") await page.waitForTimeout(1200) await page.locator('[data-testid="session-title"]').click() await page .getByRole("dialog") .getByRole("button", { name: "New diagram" }) .click() await sendMessage(page, "Chat A") await expect(page.locator('[data-testid="retry-button"]')).toBeVisible({ timeout: 10000, }) await page.waitForTimeout(1200) await page.evaluate(() => { ;(window as any).__holdExports = true }) await page.locator('[data-testid="retry-button"]').click() // Chat B comes on screen through the URL (as browser back would) await page.evaluate((id) => { ;(window as any).next.router.replace(`?session=${id}`) }, chatB) await expect(page.getByText("Answer for B.")).toBeVisible() await page.evaluate(() => (window as any).__releaseExports()) await page.waitForTimeout(1500) expect(prompts).toHaveLength(2) await expect(page.getByText("Answer for B.")).toBeVisible() await expect(page.getByText("Chat A", { exact: true })).toHaveCount(0) }) test("a retry keeps what was drawn by hand after the error", async ({ page, }) => { const bodies: any[] = [] await page.route("**/api/chat", async (route) => { bodies.push(route.request().postDataJSON()) if (bodies.length === 2) { await route.fulfill({ status: 500, contentType: "application/json", body: JSON.stringify({ error: "Model is overloaded" }), }) return } await route.fulfill({ status: 200, contentType: "text/event-stream", body: bodies.length === 1 ? createMockSSEResponse(FLOWCHART_XML, "Flowchart.") : createEditResponse(), }) }) await openApp(page) await sendMessage(page, "Create a flowchart") await waitForComplete(page) await sendMessage(page, "Add a review step") await expect(page.locator('[data-testid="retry-button"]')).toBeVisible({ timeout: 10000, }) // A copy of "Start", made by hand const canvas = getIframeContent(page) await canvas.getByText("Start", { exact: true }).click() await page.keyboard.press("ControlOrMeta+d") await expect(canvas.getByText("Start", { exact: true })).toHaveCount(2) await page.waitForTimeout(1200) await page.locator('[data-testid="retry-button"]').click() await expect(canvas.getByText("Review Step")).toBeVisible() await expect(canvas.getByText("Start", { exact: true })).toHaveCount(2) // The model was sent the canvas with the copy expect(bodies[2].xml.match(/value="Start"/g)).toHaveLength(2) await expect(page.locator('[data-testid="version-undo"]')).toHaveText( "Undo change", ) }) test("page settings draw.io keeps in place follow the AI's diagram", async ({ page, }) => { await page.route("**/api/chat", drawThenEdit()) await openApp(page) // A page shadow, which replacing the page in place would keep await openDrawioFile( page, "shadow.drawio", ``, ) const canvas = getIframeContent(page) await expect(canvas.getByText("Shady")).toBeVisible() const shadowed = () => canvas .locator(".geDiagramContainer svg g") .evaluateAll((els) => els.some((el) => (el as SVGElement).style.filter.includes("drop-shadow"), ), ) expect(await shadowed()).toBe(true) await sendMessage(page, "Create a flowchart") await waitForComplete(page) await expect(canvas.getByText("Process", { exact: true })).toBeVisible() await expect.poll(shadowed).toBe(false) }) test("a restored version gets its page id back, for links to the page", async ({ page, }) => { await page.route("**/api/chat", (route) => route.fulfill({ status: 200, contentType: "text/event-stream", body: createEditResponse(), }), ) await openApp(page) await openDrawioFile( page, "linked.drawio", ``, ) const canvas = getIframeContent(page) await expect(canvas.getByText("Home")).toBeVisible() await sendMessage(page, "Add a review step") await waitForComplete(page) await expect(canvas.getByText("Review Step")).toBeVisible() // Another page takes the place of the first one const tabs = page.locator('[data-testid="page-tabs"]') await page.getByRole("button", { name: "Add page" }).click() await tabs.getByRole("button", { name: "Main" }).click() await tabs.getByRole("button", { name: "Main" }).click() await page.getByRole("button", { name: "Delete page" }).click() await expect(tabs.getByRole("button", { name: "Main" })).toHaveCount(0) await page.locator('[data-testid="version-restore"]').click() await expect(canvas.getByText("Review Step")).toBeVisible() const savedPageId = () => page.evaluate(async () => { const db: IDBDatabase = await new Promise((resolve) => { const request = indexedDB.open("next-ai-drawio") request.onsuccess = () => resolve(request.result) }) const sessions: any[] = await new Promise((resolve) => { const request = db .transaction("sessions") .objectStore("sessions") .getAll() request.onsuccess = () => resolve(request.result) }) db.close() const xml = sessions[0]?.diagramXml ?? "" return new DOMParser() .parseFromString(xml, "text/xml") .querySelector("diagram") ?.getAttribute("id") }) await expect.poll(savedPageId, { timeout: 10000 }).toBe("orig") }) test("Ctrl+Z after an AI drawing gives the page its id back", async ({ page, }) => { await page.route("**/api/chat", drawThenEdit()) await openApp(page) await openDrawioFile( page, "linked.drawio", ``, ) const canvas = getIframeContent(page) await expect(canvas.getByText("Home")).toBeVisible() await sendMessage(page, "Create a flowchart") await waitForComplete(page) await expect(canvas.getByText("Process", { exact: true })).toBeVisible() const savedPageId = () => page.evaluate(async () => { const db: IDBDatabase = await new Promise((resolve) => { const request = indexedDB.open("next-ai-drawio") request.onsuccess = () => resolve(request.result) }) const sessions: any[] = await new Promise((resolve) => { const request = db .transaction("sessions") .objectStore("sessions") .getAll() request.onsuccess = () => resolve(request.result) }) db.close() return new DOMParser() .parseFromString(sessions[0]?.diagramXml ?? "", "text/xml") .querySelector("diagram") ?.getAttribute("id") }) await expect.poll(savedPageId, { timeout: 10000 }).toBe("page-1") await canvas .locator(".geDiagramContainer") .click({ position: { x: 10, y: 10 } }) await page.keyboard.press("ControlOrMeta+z") await expect(canvas.getByText("Home")).toBeVisible() await expect.poll(savedPageId, { timeout: 10000 }).toBe("orig") await page.keyboard.press("ControlOrMeta+Shift+z") await expect(canvas.getByText("Process", { exact: true })).toBeVisible() await expect.poll(savedPageId, { timeout: 10000 }).toBe("page-1") }) test("page settings the AI writes in single quotes are applied too", async ({ page, }) => { const shady = `` let count = 0 await page.route("**/api/chat", (route) => route.fulfill({ status: 200, contentType: "text/event-stream", body: count++ === 0 ? createMockSSEResponse(FLOWCHART_XML, "Flowchart.") : createMockSSEResponse(shady, "With a shadow."), }), ) await openApp(page) await sendMessage(page, "Create a flowchart") await waitForComplete(page) await sendMessage(page, "Redraw it with a shadow") await waitForCompleteCount(page, 2) const canvas = getIframeContent(page) await expect(canvas.getByText("Shady")).toBeVisible() await expect .poll(() => canvas .locator(".geDiagramContainer svg g") .evaluateAll((els) => els.some((el) => (el as SVGElement).style.filter.includes( "drop-shadow", ), ), ), ) .toBe(true) }) test("an opened blank file is shown also from a chat with messages", async ({ page, }) => { await page.route("**/api/chat", drawThenEdit()) await openApp(page) await sendMessage(page, "Create a flowchart") await waitForComplete(page) await openDrawioFile(page, "blank.drawio", BLANK_SHEET) await expect(page.locator('[data-testid="page-tabs"]')).toContainText( "Sheet", ) await page.waitForTimeout(800) await expect(page.locator('[data-testid="lobby-hero"]')).toHaveCount(0) await expect( page.locator('[data-testid="session-title"]'), ).toContainText("blank") }) test("an opened blank file is saved: a language switch and a reload bring it back", async ({ page, }) => { await openApp(page) await openDrawioFile(page, "blank.drawio", BLANK_SHEET) const tabs = page.locator('[data-testid="page-tabs"]') await expect(tabs).toContainText("Sheet") // Right away, before the chat is saved on its own await openSettingsTab(page, "appearance") await page.locator("#language-select").click() await page.getByRole("option", { name: "日本語", exact: true }).click() await expect(page).toHaveURL(/\/ja\?session=/, { timeout: 15000 }) const background = () => getIframeContent(page) .locator(".geBackgroundPage") .evaluate((el) => getComputedStyle(el).backgroundColor) for (const step of ["switch", "reload"]) { if (step === "reload") { await page.reload({ waitUntil: "networkidle" }) } await expect(tabs).toContainText("Sheet", { timeout: 30000 }) await expect.poll(background).toBe("rgb(255, 238, 204)") await expect( page.locator('[data-testid="lobby-hero"]'), ).toHaveCount(0) await expect( page.locator('[data-testid="session-title"]'), ).toContainText("blank") } }) test("a rename while the first save is being written is kept", async ({ page, }) => { await page.route("**/api/chat", drawThenEdit()) await openApp(page) await sendMessage(page, "Create a flowchart") await waitForComplete(page) // Hold the sessions store: the first save waits to be written await page.evaluate(async () => { const w = window as any const db: IDBDatabase = await new Promise((resolve) => { const request = indexedDB.open("next-ai-drawio") request.onsuccess = () => resolve(request.result) }) const store = db .transaction("sessions", "readwrite") .objectStore("sessions") const keepBusy = () => { if (w.__release) return db.close() store.get("none").onsuccess = keepBusy } keepBusy() }) await page.waitForTimeout(1500) await expect(page).not.toHaveURL(/session=/) await page.locator('[data-testid="session-title"]').click() await page.locator('[data-testid="rename-session"]').click() const input = page.getByRole("textbox", { name: "Rename" }) await input.fill("Order flow") await input.press("Enter") await page.keyboard.press("Escape") await page.evaluate(() => { ;(window as any).__release = true }) await expect(page).toHaveURL(/session=/, { timeout: 10000 }) await expect .poll(() => sessionTimes(page).then((t) => Object.keys(t))) .toEqual(["Order flow"]) await expect( page.locator('[data-testid="session-title"]'), ).toContainText("Order flow") }) test("choosing a chat deleted in another tab keeps this chat's session in the URL", async ({ page, }) => { await page.route("**/api/chat", drawThenEdit()) await openApp(page) await sendMessage(page, "Create a flowchart") await waitForComplete(page) await expect(page).toHaveURL(/session=/) await page.waitForTimeout(1200) await page.locator('[data-testid="session-title"]').click() await page .getByRole("dialog") .getByRole("button", { name: "New diagram" }) .click() await expect(getChatInput(page)).toBeFocused() // Another tab deletes the flowchart chat await page.evaluate(async () => { const db: IDBDatabase = await new Promise((resolve) => { const request = indexedDB.open("next-ai-drawio") request.onsuccess = () => resolve(request.result) }) await new Promise((resolve) => { const tx = db.transaction("sessions", "readwrite") tx.objectStore("sessions").clear() tx.oncomplete = resolve }) db.close() }) await openDrawioFile(page, "mine.drawio", TWO_PAGE_FILE) await expect(getIframeContent(page).getByText("Old A")).toBeVisible() // Before this chat saves on its own: choosing the gone chat saves it await expect(page).not.toHaveURL(/session=/) await page.locator('[data-testid="session-title"]').click() await page.getByRole("button", { name: /Create a flowchart/ }).click() await expect(page).toHaveURL(/session=/, { timeout: 10000 }) await page.locator('[data-testid="session-title"]').click() await expect( page.getByRole("button", { name: /Create a flowchart/ }), ).toHaveCount(0) await page.keyboard.press("Escape") await page.reload({ waitUntil: "networkidle" }) await expect(getIframeContent(page).getByText("Old A")).toBeVisible({ timeout: 30000, }) }) test("a title typed for an empty chat stays with that chat", async ({ page, }) => { await page.route("**/api/chat", drawThenEdit()) await openApp(page) await sendMessage(page, "Create a flowchart") await waitForComplete(page) await expect(page).toHaveURL(/session=/) await page.waitForTimeout(1200) // A new chat, named before anything is in it await page.locator('[data-testid="session-title"]').click() await page .getByRole("dialog") .getByRole("button", { name: "New diagram" }) .click() await expect(page.locator('[data-testid="lobby-hero"]')).toBeVisible() // The new chat puts the focus in the composer await expect(getChatInput(page)).toBeFocused() const recent = page.getByRole("button", { name: "Recent diagrams" }) await recent.click() await page.locator('[data-testid="rename-session"]').click() const input = page.getByRole("textbox", { name: "Rename" }) await input.fill("Foo") await input.press("Enter") // Open the flowchart chat, then delete it await page.getByRole("button", { name: /Create a flowchart/ }).click() await expect( getIframeContent(page).getByText("Process", { exact: true }), ).toBeVisible() await page.locator('[data-testid="session-title"]').click() await page .getByRole("button", { name: /Create a flowchart/ }) .getByRole("button", { name: "Delete this diagram?" }) .click() await page.getByRole("button", { name: "Delete", exact: true }).click() await expect(page.locator('[data-testid="lobby-hero"]')).toBeVisible() // The empty chat on screen now has no title of its own; "Foo" was // kept for the chat it was typed in await recent.click() await expect(page.locator('[data-testid="rename-session"]')).toHaveText( "Untitled diagram", ) await expect(page.getByRole("button", { name: /^Foo/ })).toBeVisible() }) test("an opened file without shapes is shown, page settings included", async ({ page, }) => { await openApp(page) const [chooser] = await Promise.all([ page.waitForEvent("filechooser"), page .getByRole("button", { name: "Open .drawio file" }) .first() .click(), ]) await chooser.setFiles({ name: "blank.drawio", mimeType: "application/xml", buffer: Buffer.from( '', ), }) await expect(page.locator('[data-testid="lobby-hero"]')).toHaveCount(0) await expect(page.locator('[data-testid="page-tabs"]')).toContainText( "Sheet", ) await expect .poll(() => getIframeContent(page) .locator(".geBackgroundPage") .evaluate((el) => getComputedStyle(el).backgroundColor), ) .toBe("rgb(255, 238, 204)") }) test("the canvas edge follows a panel narrowed by the window at once", async ({ page, }) => { await page.addInitScript(() => { localStorage.setItem("next-ai-draw-io-panel-width", "560") }) await page.setViewportSize({ width: 1280, height: 800 }) await openApp(page) await page.locator('[data-testid="draw-yourself"]').click() await page.waitForTimeout(800) // 900 px leaves the panel 500 px: the canvas edge moves with it await page.setViewportSize({ width: 900, height: 800 }) await page.waitForTimeout(50) const panel = await boxOf(page.locator('[data-testid="chat-panel"]')) const stage = await boxOf(page.locator('[data-testid="canvas-stage"]')) expect(Math.abs(stage.x + stage.width - (panel.x - 12))).toBeLessThan(3) }) test("closing the panel after dragging its edge keeps the view's middle", async ({ page, }) => { await page.route("**/api/chat", drawThenEdit()) await openApp(page) await sendMessage(page, "Create a flowchart") await waitForComplete(page) // Past the time a resize fits the new diagram again; a zoom by // the user keeps the view from then on await page.waitForTimeout(4500) await page.getByRole("button", { name: "Zoom out" }).click() await page.waitForTimeout(500) const handle = await boxOf(page.getByRole("separator")) await page.mouse.move(handle.x + handle.width / 2, handle.y + 100) await page.mouse.down() await page.mouse.move(handle.x - 150, handle.y + 100, { steps: 8 }) await page.mouse.up() await page.waitForTimeout(600) const offset = async () => { const stage = await boxOf( page.locator('[data-testid="canvas-stage"]'), ) const shape = await boxOf( getIframeContent(page).getByText("Process", { exact: true }), ) return shape.x + shape.width / 2 - (stage.x + stage.width / 2) } const before = await offset() await page.getByRole("button", { name: "Hide chat panel" }).click() await page.waitForTimeout(900) expect(Math.abs((await offset()) - before)).toBeLessThan(6) }) test("compare explains a change in the order of shapes", async ({ page, }) => { await page.route("**/api/chat", drawThenEdit()) await openApp(page) await sendMessage(page, "Create a flowchart") await waitForComplete(page) await page.waitForTimeout(1500) // Bring the first shape to the front: only the order changes await getIframeContent(page).getByText("Start", { exact: true }).click() await page.keyboard.press("ControlOrMeta+Shift+F") await page.waitForTimeout(1000) await page .locator('[data-testid="version-card"]') .getByRole("button", { name: "Compare" }) .click() await expect(page.getByRole("dialog")).toContainText("their order") await expect( page.locator('[data-testid="compare-restore"]'), ).toBeEnabled() }) test("a file name typed in the export dialog stays when the title comes", async ({ page, }) => { await page.route("**/api/chat", async (route) => { await new Promise((resolve) => setTimeout(resolve, 2500)) await route .fulfill({ status: 200, contentType: "text/event-stream", body: createMockSSEResponse(FLOWCHART_XML, "Here it is."), }) .catch(() => {}) }) await openApp(page) await sendMessage(page, "Create a flowchart") await page.locator('[data-testid="toolbar-export"]').click() const name = page.getByRole("dialog").getByRole("textbox") await name.fill("my-arch") // The answer ends and the chat is saved: it gets its title now await waitForComplete(page) await expect(page).toHaveURL(/session=/, { timeout: 10000 }) await page.waitForTimeout(1000) await expect(name).toHaveValue("my-arch") }) })