mirror of
https://github.com/DayuanJiang/next-ai-draw-io.git
synced 2026-10-09 19:19:50 +08:00
- A call the server runs (get_shape_library) still reaches the browser's tool handler, and it dropped the stored diagram of an earlier broken edit, whose preview then stayed. Only the tools that draw take it now. - A display_diagram whose final XML fails the checks loads the diagram from before its preview again, as a failed edit does. - After Stop, a tool result that arrives later (a screenshot check still running) no longer sends a new request; Stop also skips calls the tool handler already took. - New chat and opening another chat kept nothing of a diagram drawn without messages when it could not be saved; now they stay on it. - The settings dialog drops a model list or test result whose provider credentials changed meanwhile, also in another tab. - A saved provider this version does not know crashed the whole page on load; it is skipped. - The input emptied a moment after the message showed in the chat, so it briefly appeared twice (seen as a flaky e2e test). - The desktop app's preset switch on the same port refetches the server models instead of reloading the page, which lost unsent attachments.
341 lines
13 KiB
TypeScript
341 lines
13 KiB
TypeScript
import type { Page } from "@playwright/test"
|
|
import { SINGLE_BOX_XML } from "./fixtures/diagrams"
|
|
import {
|
|
expect,
|
|
getChatInput,
|
|
getIframe,
|
|
getIframeContent,
|
|
openSettings,
|
|
sendMessage,
|
|
test,
|
|
waitForComplete,
|
|
waitForText,
|
|
} from "./lib/fixtures"
|
|
import { createMockSSEResponse } from "./lib/helpers"
|
|
|
|
test.describe("History and Session Restore", () => {
|
|
test("new chat button clears conversation", async ({ page }) => {
|
|
await page.route("**/api/chat", async (route) => {
|
|
await route.fulfill({
|
|
status: 200,
|
|
contentType: "text/event-stream",
|
|
body: createMockSSEResponse(
|
|
SINGLE_BOX_XML,
|
|
"Created your test diagram.",
|
|
),
|
|
})
|
|
})
|
|
|
|
await page.goto("/", { waitUntil: "networkidle" })
|
|
await getIframe(page).waitFor({ state: "visible", timeout: 30000 })
|
|
|
|
await test.step("create a conversation", async () => {
|
|
await sendMessage(page, "Create a test diagram")
|
|
await waitForText(page, "Created your test diagram.")
|
|
})
|
|
|
|
await test.step("click new chat button", async () => {
|
|
const newChatButton = page.locator(
|
|
'[data-testid="new-chat-button"]',
|
|
)
|
|
await expect(newChatButton).toBeVisible({ timeout: 5000 })
|
|
await newChatButton.click()
|
|
})
|
|
|
|
await test.step("verify conversation is cleared", async () => {
|
|
await expect(
|
|
page.locator('text="Created your test diagram."'),
|
|
).not.toBeVisible({ timeout: 5000 })
|
|
})
|
|
})
|
|
|
|
test("new chat keeps a conversation that could not be saved", async ({
|
|
page,
|
|
}) => {
|
|
await page.route("**/api/chat", async (route) => {
|
|
await route.fulfill({
|
|
status: 200,
|
|
contentType: "text/event-stream",
|
|
body: createMockSSEResponse(
|
|
SINGLE_BOX_XML,
|
|
"Created your test diagram.",
|
|
),
|
|
})
|
|
})
|
|
await page.goto("/", { waitUntil: "networkidle" })
|
|
await getIframe(page).waitFor({ state: "visible", timeout: 30000 })
|
|
await sendMessage(page, "Create a test diagram")
|
|
await waitForText(page, "Created your test diagram.")
|
|
|
|
// Browser storage is full from now on
|
|
await page.evaluate(() => {
|
|
IDBObjectStore.prototype.put = () => {
|
|
throw new DOMException("Storage is full", "QuotaExceededError")
|
|
}
|
|
})
|
|
await page.locator('[data-testid="new-chat-button"]').click()
|
|
|
|
await expect(
|
|
page.getByText(/Could not save this chat/).first(),
|
|
).toBeVisible({ timeout: 5000 })
|
|
await page.waitForTimeout(1000)
|
|
// Still the conversation and its diagram, not the empty chat's examples
|
|
await expect(page.getByText("Paper to Diagram")).toHaveCount(0)
|
|
await expect(
|
|
getIframeContent(page).getByText("Test Box", { exact: true }),
|
|
).toBeVisible()
|
|
})
|
|
|
|
// A diagram drawn by hand, without chat messages: loaded into draw.io
|
|
// directly, then moved with an arrow key, which draw.io reports as an
|
|
// edit like any manual change
|
|
async function drawByHand(page: Page, label: string) {
|
|
const shape = getIframeContent(page).getByText(label, { exact: true })
|
|
// draw.io may still be starting (after a new chat): send until shown
|
|
for (let i = 0; i < 10 && (await shape.count()) === 0; i++) {
|
|
await page.evaluate((label) => {
|
|
const xml = `<mxfile><diagram id="p" name="Page-1"><mxGraphModel><root><mxCell id="0"/><mxCell id="1" parent="0"/><mxCell id="h" value="${label}" style="rounded=1;" vertex="1" parent="1"><mxGeometry x="60" y="60" width="140" height="60" as="geometry"/></mxCell></root></mxGraphModel></diagram></mxfile>`
|
|
document
|
|
.querySelector("iframe")
|
|
?.contentWindow?.postMessage(
|
|
JSON.stringify({ action: "load", xml, autosave: 1 }),
|
|
"*",
|
|
)
|
|
}, label)
|
|
await page.waitForTimeout(1000)
|
|
}
|
|
await shape.click({ timeout: 10000 })
|
|
await page.keyboard.press("ArrowRight")
|
|
// The app saves the edit about a second later
|
|
await page.waitForTimeout(2000)
|
|
}
|
|
const storageFull = (page: Page) =>
|
|
page.evaluate(() => {
|
|
IDBObjectStore.prototype.put = () => {
|
|
throw new DOMException("Storage is full", "QuotaExceededError")
|
|
}
|
|
})
|
|
|
|
test("new chat keeps a diagram without messages that could not be saved", async ({
|
|
page,
|
|
}) => {
|
|
await page.goto("/", { waitUntil: "networkidle" })
|
|
await getIframe(page).waitFor({ state: "visible", timeout: 30000 })
|
|
await drawByHand(page, "Hand drawn")
|
|
await expect(
|
|
getIframeContent(page).getByText("Hand drawn", { exact: true }),
|
|
).toBeVisible({ timeout: 10000 })
|
|
await storageFull(page)
|
|
await page.locator('[data-testid="new-chat-button"]').click()
|
|
await expect(
|
|
page.getByText(/Could not save this chat/).first(),
|
|
).toBeVisible({ timeout: 5000 })
|
|
await expect(
|
|
getIframeContent(page).getByText("Hand drawn", { exact: true }),
|
|
).toBeVisible()
|
|
})
|
|
|
|
test("opening another chat keeps a diagram that could not be saved", async ({
|
|
page,
|
|
}) => {
|
|
await page.route("**/api/chat", async (route) => {
|
|
await route.fulfill({
|
|
status: 200,
|
|
contentType: "text/event-stream",
|
|
body: createMockSSEResponse(
|
|
SINGLE_BOX_XML,
|
|
"Created your test diagram.",
|
|
),
|
|
})
|
|
})
|
|
await page.goto("/", { waitUntil: "networkidle" })
|
|
await getIframe(page).waitFor({ state: "visible", timeout: 30000 })
|
|
await sendMessage(page, "Create a test diagram")
|
|
await waitForText(page, "Created your test diagram.")
|
|
await page.waitForTimeout(1500)
|
|
await page.locator('[data-testid="new-chat-button"]').click()
|
|
// The empty chat lists the first one; draw something by hand
|
|
const firstChat = page.getByRole("button", {
|
|
name: /Create a test diagram/,
|
|
})
|
|
await expect(firstChat).toBeVisible({ timeout: 10000 })
|
|
await drawByHand(page, "Hand drawn")
|
|
await expect(
|
|
getIframeContent(page).getByText("Hand drawn", { exact: true }),
|
|
).toBeVisible({ timeout: 10000 })
|
|
await storageFull(page)
|
|
await firstChat.click()
|
|
await expect(
|
|
page.getByText(/Could not save this chat/).first(),
|
|
).toBeVisible({ timeout: 5000 })
|
|
await page.waitForTimeout(1000)
|
|
await expect(
|
|
getIframeContent(page).getByText("Hand drawn", { exact: true }),
|
|
).toBeVisible()
|
|
})
|
|
|
|
test("chat history sidebar shows past conversations", async ({ page }) => {
|
|
await page.goto("/", { waitUntil: "networkidle" })
|
|
await getIframe(page).waitFor({ state: "visible", timeout: 30000 })
|
|
|
|
const historyButton = page.locator(
|
|
'button[aria-label*="History"]:not([disabled]), button:has(svg.lucide-history):not([disabled]), button:has(svg.lucide-menu):not([disabled]), button:has(svg.lucide-sidebar):not([disabled]), button:has(svg.lucide-panel-left):not([disabled])',
|
|
)
|
|
|
|
const buttonCount = await historyButton.count()
|
|
if (buttonCount === 0) {
|
|
test.skip()
|
|
return
|
|
}
|
|
|
|
await historyButton.first().click()
|
|
await expect(getChatInput(page)).toBeVisible({ timeout: 3000 })
|
|
})
|
|
|
|
test("conversation persists after page reload", async ({ page }) => {
|
|
await page.route("**/api/chat", async (route) => {
|
|
await route.fulfill({
|
|
status: 200,
|
|
contentType: "text/event-stream",
|
|
body: createMockSSEResponse(
|
|
SINGLE_BOX_XML,
|
|
"This message should persist.",
|
|
),
|
|
})
|
|
})
|
|
|
|
await page.goto("/", { waitUntil: "networkidle" })
|
|
await getIframe(page).waitFor({ state: "visible", timeout: 30000 })
|
|
|
|
await test.step("create conversation", async () => {
|
|
await sendMessage(page, "Create persistent diagram")
|
|
await waitForText(page, "This message should persist.")
|
|
})
|
|
|
|
await test.step("verify message appears before reload", async () => {
|
|
await expect(getChatInput(page)).toBeVisible({ timeout: 10000 })
|
|
await expect(
|
|
page.locator('text="This message should persist."'),
|
|
).toBeVisible({ timeout: 10000 })
|
|
})
|
|
|
|
// Note: After reload, mocked responses won't persist since we're not
|
|
// testing with real localStorage. We just verify the app loads correctly.
|
|
await test.step("verify app loads after reload", async () => {
|
|
await page.reload({ waitUntil: "networkidle" })
|
|
await getIframe(page).waitFor({ state: "visible", timeout: 30000 })
|
|
await expect(getChatInput(page)).toBeVisible({ timeout: 10000 })
|
|
})
|
|
})
|
|
|
|
test("diagram state persists after reload", async ({ page }) => {
|
|
await page.route("**/api/chat", async (route) => {
|
|
await route.fulfill({
|
|
status: 200,
|
|
contentType: "text/event-stream",
|
|
body: createMockSSEResponse(
|
|
SINGLE_BOX_XML,
|
|
"Created a diagram that should be saved.",
|
|
),
|
|
})
|
|
})
|
|
|
|
await page.goto("/", { waitUntil: "networkidle" })
|
|
await getIframe(page).waitFor({ state: "visible", timeout: 30000 })
|
|
|
|
await sendMessage(page, "Create saveable diagram")
|
|
await waitForComplete(page)
|
|
|
|
await page.reload({ waitUntil: "networkidle" })
|
|
await getIframe(page).waitFor({ state: "visible", timeout: 30000 })
|
|
|
|
const frame = getIframeContent(page)
|
|
await expect(
|
|
frame
|
|
.locator(".geMenubarContainer, .geDiagramContainer, canvas")
|
|
.first(),
|
|
).toBeVisible({ timeout: 30000 })
|
|
})
|
|
|
|
test("can restore from browser back/forward", async ({ page }) => {
|
|
await page.route("**/api/chat", async (route) => {
|
|
await route.fulfill({
|
|
status: 200,
|
|
contentType: "text/event-stream",
|
|
body: createMockSSEResponse(
|
|
SINGLE_BOX_XML,
|
|
"Testing browser navigation.",
|
|
),
|
|
})
|
|
})
|
|
|
|
await page.goto("/", { waitUntil: "networkidle" })
|
|
await getIframe(page).waitFor({ state: "visible", timeout: 30000 })
|
|
|
|
await sendMessage(page, "Test navigation")
|
|
await waitForText(page, "Testing browser navigation.")
|
|
|
|
await page.goto("/about", { waitUntil: "networkidle" })
|
|
await page.goBack({ waitUntil: "networkidle" })
|
|
await getIframe(page).waitFor({ state: "visible", timeout: 30000 })
|
|
|
|
await expect(getChatInput(page)).toBeVisible({ timeout: 10000 })
|
|
})
|
|
|
|
test("settings are restored after reload", async ({ page }) => {
|
|
await page.goto("/", { waitUntil: "networkidle" })
|
|
await getIframe(page).waitFor({ state: "visible", timeout: 30000 })
|
|
|
|
await openSettings(page)
|
|
await page.keyboard.press("Escape")
|
|
|
|
await page.reload({ waitUntil: "networkidle" })
|
|
await getIframe(page).waitFor({ state: "visible", timeout: 30000 })
|
|
|
|
await openSettings(page)
|
|
})
|
|
|
|
test("model selection persists", async ({ page }) => {
|
|
await page.goto("/", { waitUntil: "networkidle" })
|
|
await getIframe(page).waitFor({ state: "visible", timeout: 30000 })
|
|
|
|
const modelSelector = page.locator(
|
|
'button[aria-label*="Model"], [data-testid="model-selector"], button:has-text("Claude")',
|
|
)
|
|
|
|
const selectorCount = await modelSelector.count()
|
|
if (selectorCount === 0) {
|
|
test.skip()
|
|
return
|
|
}
|
|
|
|
const initialModel = await modelSelector.first().textContent()
|
|
|
|
await page.reload({ waitUntil: "networkidle" })
|
|
await getIframe(page).waitFor({ state: "visible", timeout: 30000 })
|
|
|
|
const modelAfterReload = await modelSelector.first().textContent()
|
|
expect(modelAfterReload).toBe(initialModel)
|
|
})
|
|
|
|
test("handles localStorage quota exceeded gracefully", async ({ page }) => {
|
|
await page.goto("/", { waitUntil: "networkidle" })
|
|
await getIframe(page).waitFor({ state: "visible", timeout: 30000 })
|
|
|
|
await page.evaluate(() => {
|
|
try {
|
|
const largeData = "x".repeat(5 * 1024 * 1024)
|
|
localStorage.setItem("test-large-data", largeData)
|
|
} catch {
|
|
// Expected to fail on some browsers
|
|
}
|
|
})
|
|
|
|
await expect(getChatInput(page)).toBeVisible({ timeout: 10000 })
|
|
|
|
await page.evaluate(() => {
|
|
localStorage.removeItem("test-large-data")
|
|
})
|
|
})
|
|
})
|