import type { Locator, Page, Route } from "@playwright/test" import { FLOWCHART_XML } from "./fixtures/diagrams" import { expect, getAttachmentInput, getChatInput, getIframe, getIframeContent, 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, }, ) } 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") 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("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 version can be restored", 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 no longer says v2 is on the canvas const restore = page .locator('[data-testid="version-card"]') .nth(1) .locator('[data-testid="version-restore"]') await expect(restore).toBeEnabled() await restore.click() await expect(canvas.getByText("Review Step")).toBeVisible() }) 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("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("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("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") }) })