mirror of
https://github.com/DayuanJiang/next-ai-draw-io.git
synced 2026-10-07 02:07: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.
137 lines
5.6 KiB
TypeScript
137 lines
5.6 KiB
TypeScript
import { describe, expect, it } from "vitest"
|
|
import {
|
|
cn,
|
|
extractCompleteMxCells,
|
|
isMxCellXmlComplete,
|
|
isRealDiagram,
|
|
} from "@/lib/utils"
|
|
import { BLANK_MXFILE } from "@/packages/mcp-server/src/pages.ts"
|
|
|
|
describe("isRealDiagram", () => {
|
|
it("counts a small diagram with one shape", () => {
|
|
// 234 characters: valid, shown, and saved with its chat
|
|
const xml =
|
|
'<mxfile><diagram id="p"><mxGraphModel><root><mxCell id="0"/><mxCell id="1" parent="0"/><mxCell id="2" value="A" vertex="1" parent="1"><mxGeometry width="80" height="30" as="geometry"/></mxCell></root></mxGraphModel></diagram></mxfile>'
|
|
expect(xml.length).toBeLessThan(300)
|
|
expect(isRealDiagram(xml)).toBe(true)
|
|
})
|
|
|
|
it("does not count a blank page or nothing", () => {
|
|
expect(isRealDiagram(BLANK_MXFILE)).toBe(false)
|
|
expect(isRealDiagram("")).toBe(false)
|
|
expect(isRealDiagram(null)).toBe(false)
|
|
})
|
|
|
|
it("still counts a longer document of empty named pages", () => {
|
|
// Pages and page settings are worth keeping, as before
|
|
const pages = Array.from(
|
|
{ length: 3 },
|
|
(_, i) =>
|
|
`<diagram id="p${i}" name="Page ${i}"><mxGraphModel pageWidth="1600" pageHeight="900"><root><mxCell id="0"/><mxCell id="1" parent="0"/></root></mxGraphModel></diagram>`,
|
|
).join("")
|
|
expect(isRealDiagram(`<mxfile>${pages}</mxfile>`)).toBe(true)
|
|
})
|
|
})
|
|
|
|
describe("isMxCellXmlComplete", () => {
|
|
it("returns false for empty/null input", () => {
|
|
expect(isMxCellXmlComplete("")).toBe(false)
|
|
expect(isMxCellXmlComplete(null)).toBe(false)
|
|
expect(isMxCellXmlComplete(undefined)).toBe(false)
|
|
})
|
|
|
|
it("returns true for self-closing mxCell", () => {
|
|
const xml =
|
|
'<mxCell id="2" value="Hello" style="rounded=1;" vertex="1" parent="1"/>'
|
|
expect(isMxCellXmlComplete(xml)).toBe(true)
|
|
// A raw "<" in a value (escaped later by the auto-fix)
|
|
expect(
|
|
isMxCellXmlComplete(
|
|
'<mxCell id="3" value="<b>Title</b>" style="text;html=1;" vertex="1" parent="1"/>',
|
|
),
|
|
).toBe(true)
|
|
})
|
|
|
|
it("returns true for mxCell with closing tag", () => {
|
|
const xml = `<mxCell id="2" value="Hello" vertex="1" parent="1">
|
|
<mxGeometry x="100" y="100" width="120" height="60" as="geometry"/>
|
|
</mxCell>`
|
|
expect(isMxCellXmlComplete(xml)).toBe(true)
|
|
})
|
|
|
|
it("returns false for truncated mxCell", () => {
|
|
const xml =
|
|
'<mxCell id="2" value="Hello" style="rounded=1;" vertex="1" parent'
|
|
expect(isMxCellXmlComplete(xml)).toBe(false)
|
|
})
|
|
|
|
it("returns false for mxCell with unclosed geometry", () => {
|
|
const xml = `<mxCell id="2" value="Hello" vertex="1" parent="1">
|
|
<mxGeometry x="100" y="100" width="120"`
|
|
expect(isMxCellXmlComplete(xml)).toBe(false)
|
|
})
|
|
|
|
it("returns false when output stops after a child of an open mxCell", () => {
|
|
const xml = `<mxCell id="2" value="A" vertex="1" parent="1">
|
|
<mxGeometry x="0" y="0" width="80" height="40" as="geometry"/>
|
|
</mxCell>
|
|
<mxCell id="3" value="B" vertex="1" parent="1">
|
|
<mxGeometry x="100" y="0" width="80" height="40" as="geometry"/>`
|
|
expect(isMxCellXmlComplete(xml)).toBe(false)
|
|
})
|
|
|
|
it("returns false when output stops after </mxGeometry> of an open mxCell", () => {
|
|
const xml = `<mxCell id="e1" edge="1" parent="1" source="2" target="3">
|
|
<mxGeometry relative="1" as="geometry">
|
|
<mxPoint x="10" y="10" as="sourcePoint"/>
|
|
</mxGeometry>`
|
|
expect(isMxCellXmlComplete(xml)).toBe(false)
|
|
})
|
|
|
|
it("returns true for a self-closing last mxCell with > in its value", () => {
|
|
const xml = `<mxCell id="2" value="A" vertex="1" parent="1">
|
|
<mxGeometry as="geometry"/>
|
|
</mxCell>
|
|
<mxCell id="3" value="A -> B" vertex="1" parent="1"/></root>`
|
|
expect(isMxCellXmlComplete(xml)).toBe(true)
|
|
})
|
|
|
|
it("returns true for multiple complete mxCells", () => {
|
|
const xml = `<mxCell id="2" value="A" vertex="1" parent="1"/>
|
|
<mxCell id="3" value="B" vertex="1" parent="1"/>`
|
|
expect(isMxCellXmlComplete(xml)).toBe(true)
|
|
})
|
|
})
|
|
|
|
describe("cn (class name utility)", () => {
|
|
it("merges class names", () => {
|
|
expect(cn("foo", "bar")).toBe("foo bar")
|
|
})
|
|
|
|
it("handles conditional classes", () => {
|
|
expect(cn("foo", false && "bar", "baz")).toBe("foo baz")
|
|
})
|
|
|
|
it("merges tailwind classes correctly", () => {
|
|
expect(cn("px-2", "px-4")).toBe("px-4")
|
|
expect(cn("text-red-500", "text-blue-500")).toBe("text-blue-500")
|
|
})
|
|
})
|
|
|
|
describe("extractCompleteMxCells", () => {
|
|
it("keeps the cell right after self-closing root cells", () => {
|
|
const xml = `<mxfile><diagram id="p1"><mxGraphModel><root><mxCell id="0"/><mxCell id="1" parent="0"/><mxCell id="2" value="A" vertex="1" parent="1"><mxGeometry as="geometry"/></mxCell><mxCell id="3" value="B" vertex="1" parent="1"><mxGeometry as="geometry"/></mxCell></root></mxGraphModel></diagram></mxfile>`
|
|
const ids = [
|
|
...extractCompleteMxCells(xml).matchAll(/<mxCell id="([^"]+)"/g),
|
|
].map((m) => m[1])
|
|
expect(ids).toEqual(["0", "1", "2", "3"])
|
|
})
|
|
|
|
it("drops an incomplete trailing cell", () => {
|
|
const xml = `<mxCell id="2" vertex="1" parent="1"/><mxCell id="3" vertex="1" parent="1"><mxGeometry as="geometry"/>`
|
|
expect(extractCompleteMxCells(xml)).toBe(
|
|
'<mxCell id="2" vertex="1" parent="1"/>',
|
|
)
|
|
})
|
|
})
|