mirror of
https://github.com/DayuanJiang/next-ai-draw-io.git
synced 2026-10-13 04:59:51 +08:00
fix(mcp-server): keep both pages when get_diagram meets a page export, and more review fixes
Found by the PR review, each with a test that failed first:
- get_diagram during a page export returned the one-page projection on
screen as the whole document (6 of 6 times when timed so). The preview
page no longer answers a sync while a projection shows, and syncs after
reloading, so the poll that restores the real document exports it.
- Exports are numbered on the server too: a late result of an export that
timed out was saved as the next export's file.
- In Chrome, a new_xml with a syntax error counted the <parsererror>
element as a second cell, so the web app rejected edits that auto-fix
repairs ("must contain exactly one cell").
- hasCells missed single-quoted ids, so screenshot_diagram called such a
diagram empty and auto-save never created its file.
- A literal \n directly under a <diagram> that has a model passed
validation; only text-only pages are compressed data.
- A wrapped mxCell repeating its UserObject's id took the wrapper's place
in edits, so delete and update left an empty or nested wrapper.
- Bare cells with a shape or edge id of "0" or "1" are rejected with a
clear message instead of being renamed, which broke their edges.
- DRAWIO_DATA_DIR expands ~, which JSON configs pass on as it is.
This commit is contained in:
@@ -124,3 +124,28 @@ describe("pages without a <root>", () => {
|
||||
expect(errors[0]?.message).toContain("could not be decompressed")
|
||||
})
|
||||
})
|
||||
|
||||
describe("a wrapped mxCell with its wrapper's id", () => {
|
||||
const doc = `<mxfile><diagram id="p" name="P"><mxGraphModel><root><mxCell id="0"/><mxCell id="1" parent="0"/><UserObject id="u" label="A" link="https://example.com"><mxCell id="u" vertex="1" parent="1"><mxGeometry as="geometry"/></mxCell></UserObject></root></mxGraphModel></diagram></mxfile>`
|
||||
|
||||
it("deletes the whole wrapper", () => {
|
||||
const { result, errors } = applyDiagramOperations(doc, [
|
||||
{ operation: "delete", cell_id: "u" },
|
||||
])
|
||||
expect(errors).toEqual([])
|
||||
expect(result).not.toContain("UserObject")
|
||||
})
|
||||
|
||||
it("replaces the wrapper on update", () => {
|
||||
const { result, errors } = applyDiagramOperations(doc, [
|
||||
{
|
||||
operation: "update",
|
||||
cell_id: "u",
|
||||
new_xml: `<UserObject id="u" label="B" link="https://example.com"><mxCell vertex="1" parent="1"><mxGeometry as="geometry"/></mxCell></UserObject>`,
|
||||
},
|
||||
])
|
||||
expect(errors).toEqual([])
|
||||
expect(result.match(/<UserObject/g)).toHaveLength(1)
|
||||
expect(result).toContain('label="B"')
|
||||
})
|
||||
})
|
||||
|
||||
@@ -258,9 +258,28 @@ describe("export requests", () => {
|
||||
await postJson("/api/state", {
|
||||
sessionId: id,
|
||||
exportData: "data:image/png;base64,AAAA",
|
||||
exportId: poll.exportId,
|
||||
})
|
||||
expect(getState(id)?.exportOptions).toBeUndefined()
|
||||
})
|
||||
|
||||
it("ignores a late result of an export that already timed out", async () => {
|
||||
const id = "mcp-export-late"
|
||||
setState(id, "<mxfile>x</mxfile>")
|
||||
requestExport(id, "png")
|
||||
const first = JSON.parse(
|
||||
(await request(`/api/state?sessionId=${id}`)).body,
|
||||
)
|
||||
// The server gave up on the first export and asked for the next
|
||||
requestExport(id, "svg")
|
||||
await postJson("/api/state", {
|
||||
sessionId: id,
|
||||
exportData: "data:image/png;base64,LATE",
|
||||
exportId: first.exportId,
|
||||
})
|
||||
expect(getState(id)?.exportData).toBeUndefined()
|
||||
expect(getState(id)?.exportFormat).toBe("svg")
|
||||
})
|
||||
})
|
||||
|
||||
describe("preview page", () => {
|
||||
|
||||
@@ -10,7 +10,7 @@ import {
|
||||
utimesSync,
|
||||
writeFileSync,
|
||||
} from "node:fs"
|
||||
import { tmpdir } from "node:os"
|
||||
import { homedir, tmpdir } from "node:os"
|
||||
import { join } from "node:path"
|
||||
import { afterEach, describe, expect, it } from "vitest"
|
||||
import { Autosaver, defaultDataDir } from "../src/persistence.ts"
|
||||
@@ -88,4 +88,9 @@ describe("defaultDataDir", () => {
|
||||
delete process.env.DRAWIO_DATA_DIR
|
||||
expect(defaultDataDir()).toMatch(/\.next-ai-drawio$/)
|
||||
})
|
||||
|
||||
it("expands ~, which JSON configs pass on as it is", () => {
|
||||
process.env.DRAWIO_DATA_DIR = "~/drawio-saves"
|
||||
expect(defaultDataDir()).toBe(join(homedir(), "drawio-saves"))
|
||||
})
|
||||
})
|
||||
|
||||
@@ -10,7 +10,8 @@ beforeAll(() => {
|
||||
installDomPolyfill()
|
||||
})
|
||||
|
||||
import { wrapCellsInModel } from "../src/pages.ts"
|
||||
import { prepareNewDiagram } from "../src/new-diagram.ts"
|
||||
import { hasCells, wrapCellsInModel } from "../src/pages.ts"
|
||||
import { validateAndFixXml } from "../src/xml-validation.ts"
|
||||
|
||||
const A = `<mxCell id="2" value="A" vertex="1" parent="1"><mxGeometry x="0" y="0" width="80" height="40" as="geometry"/></mxCell>`
|
||||
@@ -66,3 +67,33 @@ describe("wrapCellsInModel", () => {
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe("hasCells", () => {
|
||||
it("counts cells written with single quotes", () => {
|
||||
expect(hasCells(`<mxCell id='2' vertex='1' parent='1'/>`)).toBe(true)
|
||||
expect(hasCells(`<mxCell id='0'/><mxCell id='1' parent='0'/>`)).toBe(
|
||||
false,
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe("prepareNewDiagram", () => {
|
||||
it("rejects a shape that uses a root cell id", () => {
|
||||
// It would clash with the added root cell "1" and be renamed,
|
||||
// which breaks the edges that point to it
|
||||
const cells =
|
||||
`<mxCell id="1" value="Start" vertex="1" parent="1"><mxGeometry x="0" y="0" width="80" height="40" as="geometry"/></mxCell>` +
|
||||
`<mxCell id="3" edge="1" parent="1" source="1" target="2"><mxGeometry relative="1" as="geometry"/></mxCell>`
|
||||
const out = prepareNewDiagram(cells)
|
||||
expect(out.ok).toBe(false)
|
||||
if (out.ok) return
|
||||
expect(out.error).toMatch(/"0" and "1"/)
|
||||
})
|
||||
|
||||
it("still accepts the root cells sent along with the shapes", () => {
|
||||
const out = prepareNewDiagram(
|
||||
`<mxCell id="0"/><mxCell id="1" parent="0"/>${A}`,
|
||||
)
|
||||
expect(out.ok).toBe(true)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -308,3 +308,13 @@ describe("text between tags", () => {
|
||||
).toBe(true)
|
||||
})
|
||||
})
|
||||
|
||||
describe("text directly under a page", () => {
|
||||
it("fixes a literal \\n before the model of a page", () => {
|
||||
const r = validateAndFixXml(
|
||||
`<mxfile><diagram id="p" name="P">\\n${model(`<mxCell id="2" vertex="1" parent="1"/>`)}</diagram></mxfile>`,
|
||||
)
|
||||
expect(r.valid).toBe(true)
|
||||
expect(r.fixed).not.toContain("\\n")
|
||||
})
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user