mirror of
https://github.com/DayuanJiang/next-ai-draw-io.git
synced 2026-10-08 18:57:47 +08:00
- A redirect followed for a custom base URL also drops the key headers of providers that do not use Authorization (x-api-key, x-goog-api-key, api-key) when it goes to another origin. - A second Enter or click while a message is being prepared (attachments read, diagram exported) no longer sends it twice. - The admin Test on the deployment's own endpoints (EdgeOne, the server's keyless Ollama, an address on the server's network) counts toward the quota like a chat; the chat and the Test share one rule for it. The Test of an Azure entry set up by AZURE_RESOURCE_NAME only goes where chat goes. - EdgeOne's function is called at the site root again, as on main: EdgeOne serves edge functions there, outside Next's base path. - MCP History: the state before a write is kept unless the browser saved no change of the user's since the last server write (draw.io's sync copy of it adds no entry), and the dedupe compares the exact text again, so a change of page size or other settings only is its own version. - MCP: an edit keeps untouched labels as draw.io shows them (a literal line break in an attribute is a space); a new document of empty pages the user named is auto-saved; load_diagram reads only regular files, so a pipe cannot hold up the other write tools; the preview does not load back its own push still on its way (an undo made meanwhile is saved). - Two overlapping saves of a new chat no longer reload the canvas from the older copy. - At most three screenshot checks per user turn, passed or failed, as documented. - Desktop: the main window navigates only within the app (draw.io stays in its frame); a presets file that is not JSON and cannot be moved aside is not overwritten. - A last self-closing cell with a raw "<" in a value is not taken for cut off output. - README: Material Design shapes load their icons from fonts.gstatic.com.
136 lines
5.2 KiB
TypeScript
136 lines
5.2 KiB
TypeScript
import {
|
|
expect,
|
|
getChatInput,
|
|
getIframe,
|
|
sendMessage,
|
|
test,
|
|
} from "./lib/fixtures"
|
|
import { createTextOnlyResponse } from "./lib/helpers"
|
|
|
|
test.describe("Chat Panel", () => {
|
|
test.beforeEach(async ({ page }) => {
|
|
await page.goto("/", { waitUntil: "networkidle" })
|
|
await getIframe(page).waitFor({ state: "visible", timeout: 30000 })
|
|
})
|
|
|
|
test("page has interactive elements", async ({ page }) => {
|
|
const buttons = page.locator("button")
|
|
const count = await buttons.count()
|
|
expect(count).toBeGreaterThan(0)
|
|
})
|
|
|
|
test("draw.io iframe is interactive", async ({ page }) => {
|
|
const iframe = getIframe(page)
|
|
await expect(iframe).toBeVisible()
|
|
|
|
const src = await iframe.getAttribute("src")
|
|
expect(src).toBeTruthy()
|
|
})
|
|
})
|
|
|
|
test.describe("Crossing the mobile breakpoint", () => {
|
|
test.beforeEach(async ({ page }) => {
|
|
// A text answer that arrives in parts over a few seconds
|
|
await page.addInitScript(() => {
|
|
const realFetch = window.fetch
|
|
window.fetch = async (input, init) => {
|
|
const url = input instanceof Request ? input.url : String(input)
|
|
if (!url.endsWith("/api/chat")) return realFetch(input, init)
|
|
const events = [
|
|
{ type: "start" },
|
|
{ type: "text-start", id: "t" },
|
|
{ type: "text-delta", id: "t", delta: "Once upon" },
|
|
{ type: "text-delta", id: "t", delta: " a time." },
|
|
{ type: "text-end", id: "t" },
|
|
{ type: "finish" },
|
|
]
|
|
const body = new ReadableStream({
|
|
async start(controller) {
|
|
for (const event of events) {
|
|
controller.enqueue(
|
|
new TextEncoder().encode(
|
|
`data: ${JSON.stringify(event)}\n\n`,
|
|
),
|
|
)
|
|
await new Promise((r) => setTimeout(r, 1500))
|
|
}
|
|
controller.enqueue(
|
|
new TextEncoder().encode("data: [DONE]\n\n"),
|
|
)
|
|
controller.close()
|
|
},
|
|
})
|
|
return new Response(body, {
|
|
headers: { "content-type": "text/event-stream" },
|
|
})
|
|
}
|
|
})
|
|
await page.setViewportSize({ width: 1280, height: 800 })
|
|
await page.goto("/", { waitUntil: "networkidle" })
|
|
await getIframe(page).waitFor({ state: "visible", timeout: 30000 })
|
|
})
|
|
|
|
test("keeps the chat and its streaming answer", async ({ page }) => {
|
|
const chat = page.locator('[data-panel-id="chat-panel"]')
|
|
await sendMessage(page, "Tell me a story")
|
|
await expect(page.getByText("Once upon")).toBeVisible({
|
|
timeout: 10000,
|
|
})
|
|
|
|
await page.setViewportSize({ width: 600, height: 900 })
|
|
await expect(page.getByText("Tell me a story")).toBeVisible()
|
|
// Half the height on mobile
|
|
await expect
|
|
.poll(async () => (await chat.boundingBox())?.height ?? 0)
|
|
.toBeCloseTo(450, -1)
|
|
|
|
await page.setViewportSize({ width: 1280, height: 800 })
|
|
// A third of the width on desktop
|
|
await expect
|
|
.poll(async () => (await chat.boundingBox())?.width ?? 0)
|
|
.toBeCloseTo(1280 / 3, -1)
|
|
await expect(page.getByText("Once upon a time.")).toBeVisible({
|
|
timeout: 10000,
|
|
})
|
|
await expect(page.getByText("Tell me a story")).toBeVisible()
|
|
})
|
|
|
|
test("opens a chat collapsed on desktop", async ({ page }) => {
|
|
await page.locator("button:has(svg.lucide-panel-right-close)").click()
|
|
await expect(getChatInput(page)).toBeHidden()
|
|
|
|
await page.setViewportSize({ width: 600, height: 900 })
|
|
await expect(getChatInput(page)).toBeVisible()
|
|
|
|
await page.setViewportSize({ width: 1280, height: 800 })
|
|
await expect(getChatInput(page)).toBeVisible()
|
|
})
|
|
})
|
|
|
|
test.describe("Sending", () => {
|
|
test("a double Enter sends the message once", async ({ page }) => {
|
|
let requests = 0
|
|
await page.route("**/api/chat", async (route) => {
|
|
requests++
|
|
await route.fulfill({
|
|
status: 200,
|
|
contentType: "text/event-stream",
|
|
body: createTextOnlyResponse("Hello there."),
|
|
})
|
|
})
|
|
await page.goto("/", { waitUntil: "networkidle" })
|
|
await getIframe(page).waitFor({ state: "visible", timeout: 30000 })
|
|
const input = getChatInput(page)
|
|
await input.fill("Hi")
|
|
// The second press comes while the diagram is being exported
|
|
await input.press("ControlOrMeta+Enter")
|
|
await input.press("ControlOrMeta+Enter")
|
|
await expect(page.getByText("Hello there.")).toBeVisible({
|
|
timeout: 10000,
|
|
})
|
|
await page.waitForTimeout(1500)
|
|
expect(requests).toBe(1)
|
|
await expect(page.getByText("Hello there.")).toHaveCount(1)
|
|
})
|
|
})
|