mirror of
https://github.com/DayuanJiang/next-ai-draw-io.git
synced 2026-10-09 03:07:46 +08:00
The canvas now fills the window and the chat floats on its right. draw.io is served from our own origin (public/drawio, downloaded at build time and checked against the release's SHA-256), so the app can drive the editor directly: - every AI change becomes a version card with a thumbnail; compare, restore or undo it, and Ctrl+Z on the canvas takes an AI change back in one step - shapes the AI just changed are highlighted on the canvas - selected shapes appear as a chip and are sent to the model - the app's own canvas toolbar, shape library and page tabs replace draw.io's chrome - new start screen, session rename and switcher, open .drawio files - settings split into Models & keys, Appearance, Advanced and About - phones switch between canvas and chat - dark mode keeps the diagram on a white sheet and no longer flashes With an external draw.io (NEXT_PUBLIC_DRAWIO_BASE_URL) the app falls back to draw.io's own toolbar and the features above that need same origin are off. The chat logic moved from the 1500-line chat panel into a chat engine plus small UI components; zustand holds canvas, versions, settings and UI state.
595 lines
23 KiB
TypeScript
595 lines
23 KiB
TypeScript
import type { Page, Route } from "@playwright/test"
|
|
import { FLOWCHART_XML } from "./fixtures/diagrams"
|
|
import {
|
|
expect,
|
|
getAttachmentInput,
|
|
getChatInput,
|
|
getIframe,
|
|
getIframeContent,
|
|
sendMessage,
|
|
test,
|
|
waitForComplete,
|
|
waitForCompleteCount,
|
|
} from "./lib/fixtures"
|
|
import { createMockSSEResponse, createToolErrorResponse } from "./lib/helpers"
|
|
|
|
/** An edit_diagram response that adds one node next to "Start" */
|
|
function createEditResponse() {
|
|
const toolCallId = `call_edit_${Date.now()}`
|
|
const newNode = `<mxCell id="review" value="Review Step" style="rounded=1;whiteSpace=wrap;html=1;" vertex="1" parent="1"><mxGeometry x="360" y="130" width="100" height="40" as="geometry"/></mxCell>`
|
|
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,
|
|
},
|
|
)
|
|
}
|
|
|
|
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", 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,
|
|
)
|
|
await expect(
|
|
getIframeContent(page).getByText("Review Step"),
|
|
).toBeVisible()
|
|
})
|
|
|
|
test("the latest card undoes and redoes the AI change", async ({
|
|
page,
|
|
}) => {
|
|
await page.route("**/api/chat", drawThenEdit())
|
|
await openApp(page)
|
|
await drawAndEdit(page)
|
|
|
|
const canvas = getIframeContent(page)
|
|
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("Ctrl+Z in the canvas undoes the AI change in one step", async ({
|
|
page,
|
|
}) => {
|
|
await page.route("**/api/chat", drawThenEdit())
|
|
await openApp(page)
|
|
await drawAndEdit(page)
|
|
|
|
// The thumbnail appears once draw.io has finished with this version
|
|
await expect(
|
|
page.locator('[data-testid="version-card"] img'),
|
|
).toHaveCount(2)
|
|
// draw.io ignores keys while it exports, as for the auto-save's
|
|
// thumbnail a second after the answer: let that one finish
|
|
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()
|
|
})
|
|
|
|
test("restoring a version from the compare dialog", 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)
|
|
})
|
|
|
|
test("selected shapes are shown and sent with the message", 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)
|
|
|
|
await getIframeContent(page)
|
|
.getByText("Process", { exact: true })
|
|
.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" },
|
|
])
|
|
})
|
|
|
|
test("the chat panel can be hidden and shown", async ({ page }) => {
|
|
await page.route("**/api/chat", drawThenEdit())
|
|
await openApp(page)
|
|
await sendMessage(page, "Create a flowchart")
|
|
await waitForComplete(page)
|
|
|
|
const panel = page.locator('[data-testid="chat-panel"]')
|
|
await page.getByRole("button", { name: "Hide chat panel" }).click()
|
|
await expect(panel).toBeHidden()
|
|
await page.locator('[data-testid="show-panel"]').click()
|
|
await expect(panel).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 = `<mxfile><diagram name="P" id="p"><mxGraphModel><root><mxCell id="0"/><mxCell id="1" parent="0"/><mxCell id="a" value="From File" style="rounded=1;" vertex="1" parent="1"><mxGeometry x="40" y="40" width="120" height="60" as="geometry"/></mxCell></root></mxGraphModel></diagram></mxfile>`
|
|
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 = `<mxfile><diagram name="P" id="p"><mxGraphModel><root><mxCell id="0"/><mxCell id="1" parent="0"/><mxCell id="a" value="From File" style="rounded=1;" vertex="1" parent="1"><mxGeometry x="40" y="40" width="120" height="60" as="geometry"/></mxCell></root></mxGraphModel></diagram></mxfile>`
|
|
await page
|
|
.locator('input[type="file"][accept^=".drawio"]')
|
|
.first()
|
|
.setInputFiles({
|
|
name: "mine.drawio",
|
|
mimeType: "application/xml",
|
|
buffer: Buffer.from(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()
|
|
})
|
|
|
|
test("picking the same shapes again attaches them 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 sendMessage(page, "Make this red")
|
|
await waitForCompleteCount(page, 2)
|
|
// 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 opens 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()
|
|
})
|
|
|
|
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("after Ctrl+Z in the canvas 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)
|
|
// 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("the toolbar fits a narrow window with the chat panel open", async ({
|
|
page,
|
|
}) => {
|
|
await page.setViewportSize({ width: 800, height: 800 })
|
|
await openApp(page)
|
|
await page.locator('[data-testid="draw-yourself"]').click()
|
|
await expect(page.locator('[data-testid="chat-panel"]')).toBeVisible()
|
|
const toolbar = page.getByRole("toolbar")
|
|
await expect(toolbar).toBeVisible()
|
|
await page.waitForTimeout(600)
|
|
const bar = await toolbar.boundingBox()
|
|
const stage = await page
|
|
.locator('[data-testid="canvas-stage"]')
|
|
.boundingBox()
|
|
expect(bar && stage).toBeTruthy()
|
|
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("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 models.boundingBox()
|
|
const b = await appearance.boundingBox()
|
|
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("the shape library shortcut works", 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.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("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()
|
|
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)
|
|
})
|
|
})
|