mirror of
https://github.com/DayuanJiang/next-ai-draw-io.git
synced 2026-09-01 17:10:24 +08:00
Compare commits
3 Commits
renovate/m
...
feat/mcp-l
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4ea4944bb9 | ||
|
|
d99b0cf9d1 | ||
|
|
88536b145c |
@@ -116,9 +116,14 @@ Use the standard MCP configuration with:
|
|||||||
|------|-------------|
|
|------|-------------|
|
||||||
| `start_session` | Opens browser with real-time diagram preview |
|
| `start_session` | Opens browser with real-time diagram preview |
|
||||||
| `create_new_diagram` | Create a new diagram from XML (requires `xml` argument) |
|
| `create_new_diagram` | Create a new diagram from XML (requires `xml` argument) |
|
||||||
|
| `load_diagram` | Load a `.drawio` file from disk into the session (handles compressed files) |
|
||||||
| `edit_diagram` | Edit diagram by ID-based operations (update/add/delete cells) |
|
| `edit_diagram` | Edit diagram by ID-based operations (update/add/delete cells) |
|
||||||
| `get_diagram` | Get the current diagram XML |
|
| `get_diagram` | Get the current diagram XML |
|
||||||
| `export_diagram` | Save diagram to a `.drawio` file |
|
| `export_diagram` | Save diagram to a `.drawio`, `.png`, or `.svg` file |
|
||||||
|
| `list_pages` | List every page (tab) with id, name, index, and cell count |
|
||||||
|
| `add_page` | Append a new page without touching existing ones |
|
||||||
|
| `rename_page` | Rename a page |
|
||||||
|
| `delete_page` | Delete a page (refuses to delete the last one) |
|
||||||
|
|
||||||
## How It Works
|
## How It Works
|
||||||
|
|
||||||
|
|||||||
4
packages/mcp-server/package-lock.json
generated
4
packages/mcp-server/package-lock.json
generated
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "@next-ai-drawio/mcp-server",
|
"name": "@next-ai-drawio/mcp-server",
|
||||||
"version": "0.2.2",
|
"version": "0.2.3",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "@next-ai-drawio/mcp-server",
|
"name": "@next-ai-drawio/mcp-server",
|
||||||
"version": "0.2.2",
|
"version": "0.2.3",
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@modelcontextprotocol/sdk": "^1.0.4",
|
"@modelcontextprotocol/sdk": "^1.0.4",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@next-ai-drawio/mcp-server",
|
"name": "@next-ai-drawio/mcp-server",
|
||||||
"version": "0.2.2",
|
"version": "0.2.3",
|
||||||
"description": "MCP server for Next AI Draw.io - AI-powered diagram generation with real-time browser preview",
|
"description": "MCP server for Next AI Draw.io - AI-powered diagram generation with real-time browser preview",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"main": "dist/index.js",
|
"main": "dist/index.js",
|
||||||
|
|||||||
@@ -36,6 +36,7 @@ class XMLSerializerPolyfill {
|
|||||||
}
|
}
|
||||||
;(globalThis as any).XMLSerializer = XMLSerializerPolyfill
|
;(globalThis as any).XMLSerializer = XMLSerializerPolyfill
|
||||||
|
|
||||||
|
import { createRequire } from "node:module"
|
||||||
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"
|
||||||
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"
|
||||||
import open from "open"
|
import open from "open"
|
||||||
@@ -55,6 +56,7 @@ import {
|
|||||||
startHttpServer,
|
startHttpServer,
|
||||||
waitForSync,
|
waitForSync,
|
||||||
} from "./http-server.js"
|
} from "./http-server.js"
|
||||||
|
import { parseDrawioFileContent } from "./load-diagram.js"
|
||||||
import { log } from "./logger.js"
|
import { log } from "./logger.js"
|
||||||
import {
|
import {
|
||||||
addPageToDoc,
|
addPageToDoc,
|
||||||
@@ -88,10 +90,17 @@ let currentSession: {
|
|||||||
lastSeenXml: string
|
lastSeenXml: string
|
||||||
} | null = null
|
} | null = null
|
||||||
|
|
||||||
// Create MCP server
|
// Create MCP server. The version reported in the MCP handshake is read from
|
||||||
|
// package.json so it can never drift from the published npm version again
|
||||||
|
// (it sat hardcoded at stale values for most of this package's history).
|
||||||
|
// Both src/ (tsx dev) and dist/ (published build) live one level below the
|
||||||
|
// package root, so the relative path works in either runtime.
|
||||||
|
const require = createRequire(import.meta.url)
|
||||||
|
const packageVersion: string = require("../package.json").version
|
||||||
|
|
||||||
const server = new McpServer({
|
const server = new McpServer({
|
||||||
name: "next-ai-drawio",
|
name: "next-ai-drawio",
|
||||||
version: "0.3.0",
|
version: packageVersion,
|
||||||
})
|
})
|
||||||
|
|
||||||
// Shared Zod schema fragment for page-targeting parameters.
|
// Shared Zod schema fragment for page-targeting parameters.
|
||||||
@@ -164,6 +173,10 @@ server.prompt(
|
|||||||
1. Call start_session to open the browser preview
|
1. Call start_session to open the browser preview
|
||||||
2. Use create_new_diagram with either a bare <mxGraphModel> (single page) or a full <mxfile> with one or more <diagram> children (multi-page)
|
2. Use create_new_diagram with either a bare <mxGraphModel> (single page) or a full <mxfile> with one or more <diagram> children (multi-page)
|
||||||
|
|
||||||
|
## Opening an Existing .drawio File
|
||||||
|
- Use load_diagram with the file path — the server reads and decompresses the file itself; don't read it and pass the XML through create_new_diagram
|
||||||
|
- After loading, call get_diagram once before editing (you haven't seen the file's cell IDs yet)
|
||||||
|
|
||||||
## Working with Multiple Pages
|
## Working with Multiple Pages
|
||||||
- Use list_pages to discover existing pages (id, name, index)
|
- Use list_pages to discover existing pages (id, name, index)
|
||||||
- Use add_page to append a new page (without losing existing ones — unlike create_new_diagram which REPLACES everything)
|
- Use add_page to append a new page (without losing existing ones — unlike create_new_diagram which REPLACES everything)
|
||||||
@@ -421,6 +434,123 @@ COMMON STYLES:
|
|||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// Tool: load_diagram
|
||||||
|
server.registerTool(
|
||||||
|
"load_diagram",
|
||||||
|
{
|
||||||
|
description:
|
||||||
|
"Load a .drawio file from disk into the current session, REPLACING the entire diagram (all pages). " +
|
||||||
|
"The server reads the file directly — you do NOT need to read the file yourself or pass its XML through create_new_diagram. " +
|
||||||
|
"Handles both plain-XML and draw.io's compressed save format.\n\n" +
|
||||||
|
"After loading, call get_diagram before edit_diagram — you haven't seen the file's cell IDs or structure yet.",
|
||||||
|
inputSchema: {
|
||||||
|
path: z
|
||||||
|
.string()
|
||||||
|
.describe(
|
||||||
|
"Path to the .drawio file to load (e.g., ./diagram.drawio)",
|
||||||
|
),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
async ({ path }) => {
|
||||||
|
try {
|
||||||
|
if (!currentSession) {
|
||||||
|
return {
|
||||||
|
content: [
|
||||||
|
{
|
||||||
|
type: "text",
|
||||||
|
text: "Error: No active session. Please call start_session first.",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
isError: true,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const fs = await import("node:fs/promises")
|
||||||
|
const nodePath = await import("node:path")
|
||||||
|
const absolutePath = nodePath.resolve(path)
|
||||||
|
|
||||||
|
let content: string
|
||||||
|
try {
|
||||||
|
content = await fs.readFile(absolutePath, "utf-8")
|
||||||
|
} catch (e) {
|
||||||
|
const msg = e instanceof Error ? e.message : String(e)
|
||||||
|
return {
|
||||||
|
content: [
|
||||||
|
{
|
||||||
|
type: "text",
|
||||||
|
text: `Error: Cannot read file ${absolutePath}: ${msg}`,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
isError: true,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const loaded = parseDrawioFileContent(content)
|
||||||
|
if (!loaded.ok) {
|
||||||
|
return {
|
||||||
|
content: [{ type: "text", text: `Error: ${loaded.error}` }],
|
||||||
|
isError: true,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const xml = loaded.xml
|
||||||
|
|
||||||
|
log.info(
|
||||||
|
`Loading diagram from ${absolutePath} (${xml.length} chars)`,
|
||||||
|
)
|
||||||
|
|
||||||
|
// Save the user's current state before replacing (same flow as
|
||||||
|
// create_new_diagram).
|
||||||
|
const browserState = getState(currentSession.id)
|
||||||
|
if (browserState?.xml) {
|
||||||
|
currentSession.xml = browserState.xml
|
||||||
|
}
|
||||||
|
if (currentSession.xml) {
|
||||||
|
addHistory(
|
||||||
|
currentSession.id,
|
||||||
|
currentSession.xml,
|
||||||
|
browserState?.svg || "",
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
currentSession.xml = xml
|
||||||
|
currentSession.version++
|
||||||
|
setState(currentSession.id, xml)
|
||||||
|
// Deliberately NOT marking the loaded XML as seen: the model only
|
||||||
|
// supplied a path, so it doesn't know the file's cell IDs. The
|
||||||
|
// edit gate will require one get_diagram before edits.
|
||||||
|
currentSession.lastSeenXml = ""
|
||||||
|
|
||||||
|
addHistory(currentSession.id, xml, "")
|
||||||
|
|
||||||
|
const doc = parseMxfile(xml)
|
||||||
|
const pages = doc ? listPagesFromDoc(doc) : []
|
||||||
|
const pageSummary =
|
||||||
|
pages.length > 0
|
||||||
|
? `Pages (${pages.length}): ${pages.map((p) => `[${p.index}] id=${p.id} name="${p.name}" cells=${p.cellCount}`).join(" | ")}`
|
||||||
|
: "no pages parsed"
|
||||||
|
|
||||||
|
log.info(`Diagram loaded from file (${pageSummary})`)
|
||||||
|
|
||||||
|
return {
|
||||||
|
content: [
|
||||||
|
{
|
||||||
|
type: "text",
|
||||||
|
text: `Diagram loaded from ${absolutePath}!\n\nThe diagram is now visible in your browser.\n\n${pageSummary}\n\nCall get_diagram before edit_diagram — you haven't seen this file's cell IDs yet.`,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
const message =
|
||||||
|
error instanceof Error ? error.message : String(error)
|
||||||
|
log.error("load_diagram failed:", message)
|
||||||
|
return {
|
||||||
|
content: [{ type: "text", text: `Error: ${message}` }],
|
||||||
|
isError: true,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
// Tool: edit_diagram
|
// Tool: edit_diagram
|
||||||
server.registerTool(
|
server.registerTool(
|
||||||
"edit_diagram",
|
"edit_diagram",
|
||||||
|
|||||||
101
packages/mcp-server/src/load-diagram.ts
Normal file
101
packages/mcp-server/src/load-diagram.ts
Normal file
@@ -0,0 +1,101 @@
|
|||||||
|
/**
|
||||||
|
* File-loading helpers for the load_diagram tool.
|
||||||
|
*
|
||||||
|
* A .drawio file is an <mxfile> whose <diagram> children hold each page's
|
||||||
|
* <mxGraphModel> either as plain XML or — draw.io's default save format —
|
||||||
|
* compressed: encodeURIComponent(xml) → raw deflate → base64 as the
|
||||||
|
* diagram's text content. The rest of the server assumes plain XML inside
|
||||||
|
* every <diagram>, so loading decompresses all pages up front.
|
||||||
|
*/
|
||||||
|
import { inflateRawSync } from "node:zlib"
|
||||||
|
import { DOMParser } from "linkedom"
|
||||||
|
import {
|
||||||
|
isMxFile,
|
||||||
|
isMxGraphModel,
|
||||||
|
normalizeToMxfile,
|
||||||
|
parseMxfile,
|
||||||
|
serializeMxfile,
|
||||||
|
} from "./pages.js"
|
||||||
|
|
||||||
|
export type LoadResult =
|
||||||
|
| { ok: true; xml: string }
|
||||||
|
| { ok: false; error: string }
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Decode one compressed page body (base64 → raw deflate → URI-decode).
|
||||||
|
* Returns null if the text isn't in that format.
|
||||||
|
*/
|
||||||
|
export function decompressPageContent(compressed: string): string | null {
|
||||||
|
try {
|
||||||
|
const inflated = inflateRawSync(
|
||||||
|
Buffer.from(compressed.trim(), "base64"),
|
||||||
|
).toString("utf-8")
|
||||||
|
try {
|
||||||
|
return decodeURIComponent(inflated)
|
||||||
|
} catch {
|
||||||
|
// Not URI-encoded (older files) — the inflated text is the XML.
|
||||||
|
return inflated
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parse the content of a .drawio file into the canonical session shape:
|
||||||
|
* an <mxfile> whose every page holds plain <mxGraphModel> XML. Accepts a
|
||||||
|
* bare <mxGraphModel> (wrapped into a one-page mxfile) and decompresses
|
||||||
|
* any compressed pages.
|
||||||
|
*/
|
||||||
|
export function parseDrawioFileContent(content: string): LoadResult {
|
||||||
|
const trimmed = content.trim()
|
||||||
|
if (!trimmed) return { ok: false, error: "File is empty." }
|
||||||
|
|
||||||
|
if (isMxGraphModel(trimmed)) {
|
||||||
|
const normalized = normalizeToMxfile(trimmed)
|
||||||
|
return normalized
|
||||||
|
? { ok: true, xml: normalized }
|
||||||
|
: { ok: false, error: "Failed to parse <mxGraphModel> XML." }
|
||||||
|
}
|
||||||
|
if (!isMxFile(trimmed)) {
|
||||||
|
return {
|
||||||
|
ok: false,
|
||||||
|
error: "Not a draw.io file: expected an <mxfile> or <mxGraphModel> root element.",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const doc = parseMxfile(trimmed)
|
||||||
|
if (!doc) return { ok: false, error: "Failed to parse <mxfile> XML." }
|
||||||
|
|
||||||
|
let decompressedAny = false
|
||||||
|
for (const d of Array.from(doc.querySelectorAll("diagram"))) {
|
||||||
|
if (d.querySelector("mxGraphModel")) continue
|
||||||
|
const text = (d.textContent || "").trim()
|
||||||
|
if (!text) continue // an empty page is valid
|
||||||
|
const pageLabel =
|
||||||
|
d.getAttribute("name") || d.getAttribute("id") || "unnamed"
|
||||||
|
const xml = decompressPageContent(text)
|
||||||
|
if (!xml || !isMxGraphModel(xml)) {
|
||||||
|
return {
|
||||||
|
ok: false,
|
||||||
|
error: `Page "${pageLabel}" has content that is neither plain <mxGraphModel> XML nor draw.io's compressed format.`,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const inner = new DOMParser().parseFromString(xml, "text/xml")
|
||||||
|
if (
|
||||||
|
inner.querySelector("parsererror") ||
|
||||||
|
inner.documentElement?.tagName !== "mxGraphModel"
|
||||||
|
) {
|
||||||
|
return {
|
||||||
|
ok: false,
|
||||||
|
error: `Page "${pageLabel}" decompressed but its XML failed to parse.`,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
d.textContent = ""
|
||||||
|
d.appendChild(
|
||||||
|
doc.importNode(inner.documentElement as unknown as Node, true),
|
||||||
|
)
|
||||||
|
decompressedAny = true
|
||||||
|
}
|
||||||
|
// Nothing changed — keep the file's own serialisation.
|
||||||
|
return { ok: true, xml: decompressedAny ? serializeMxfile(doc) : trimmed }
|
||||||
|
}
|
||||||
126
packages/mcp-server/tests/load-diagram.test.ts
Normal file
126
packages/mcp-server/tests/load-diagram.test.ts
Normal file
@@ -0,0 +1,126 @@
|
|||||||
|
/**
|
||||||
|
* Unit tests for load_diagram's file parsing (load-diagram.ts).
|
||||||
|
*
|
||||||
|
* A .drawio file stores each page's <mxGraphModel> either as plain XML or
|
||||||
|
* as draw.io's compressed default (encodeURIComponent → raw deflate →
|
||||||
|
* base64 text content). The loader must produce the canonical session
|
||||||
|
* shape: an <mxfile> whose every page is plain XML.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { deflateRawSync } from "node:zlib"
|
||||||
|
import { DOMParser } from "linkedom"
|
||||||
|
import { beforeAll, describe, expect, it } from "vitest"
|
||||||
|
|
||||||
|
// Install the DOM polyfills exactly as index.ts does at runtime.
|
||||||
|
beforeAll(() => {
|
||||||
|
;(globalThis as any).DOMParser = DOMParser
|
||||||
|
class XMLSerializerPolyfill {
|
||||||
|
serializeToString(node: any): string {
|
||||||
|
if (node.outerHTML !== undefined) return node.outerHTML
|
||||||
|
if (node.documentElement) return node.documentElement.outerHTML
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
;(globalThis as any).XMLSerializer = XMLSerializerPolyfill
|
||||||
|
})
|
||||||
|
|
||||||
|
import {
|
||||||
|
decompressPageContent,
|
||||||
|
parseDrawioFileContent,
|
||||||
|
} from "../src/load-diagram.js"
|
||||||
|
|
||||||
|
const MODEL_XML = `<mxGraphModel><root><mxCell id="0"/><mxCell id="1" parent="0"/><mxCell id="box1" value="Hello" style="rounded=0;" vertex="1" parent="1"><mxGeometry x="40" y="40" width="120" height="60" as="geometry"/></mxCell></root></mxGraphModel>`
|
||||||
|
|
||||||
|
/** Compress a page body exactly the way draw.io does when saving. */
|
||||||
|
function drawioCompress(xml: string): string {
|
||||||
|
return deflateRawSync(
|
||||||
|
Buffer.from(encodeURIComponent(xml), "utf-8"),
|
||||||
|
).toString("base64")
|
||||||
|
}
|
||||||
|
|
||||||
|
const PLAIN_MXFILE = `<mxfile host="app.diagrams.net"><diagram id="p1" name="Page-1">${MODEL_XML}</diagram></mxfile>`
|
||||||
|
const COMPRESSED_MXFILE = `<mxfile host="app.diagrams.net" compressed="true"><diagram id="p1" name="Page-1">${drawioCompress(MODEL_XML)}</diagram></mxfile>`
|
||||||
|
|
||||||
|
describe("decompressPageContent", () => {
|
||||||
|
it("round-trips draw.io's compressed format", () => {
|
||||||
|
expect(decompressPageContent(drawioCompress(MODEL_XML))).toBe(MODEL_XML)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("handles non-URI-encoded legacy payloads", () => {
|
||||||
|
const legacy = deflateRawSync(Buffer.from(MODEL_XML, "utf-8")).toString(
|
||||||
|
"base64",
|
||||||
|
)
|
||||||
|
expect(decompressPageContent(legacy)).toBe(MODEL_XML)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("returns null for garbage", () => {
|
||||||
|
expect(decompressPageContent("not base64 deflate")).toBeNull()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe("parseDrawioFileContent", () => {
|
||||||
|
it("passes a plain-XML mxfile through unchanged", () => {
|
||||||
|
const r = parseDrawioFileContent(PLAIN_MXFILE)
|
||||||
|
expect(r).toEqual({ ok: true, xml: PLAIN_MXFILE })
|
||||||
|
})
|
||||||
|
|
||||||
|
it("wraps a bare mxGraphModel into a one-page mxfile", () => {
|
||||||
|
const r = parseDrawioFileContent(MODEL_XML)
|
||||||
|
expect(r.ok).toBe(true)
|
||||||
|
if (r.ok) {
|
||||||
|
expect(r.xml).toContain("<mxfile")
|
||||||
|
expect(r.xml).toContain('value="Hello"')
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
it("decompresses a compressed mxfile into plain XML pages", () => {
|
||||||
|
const r = parseDrawioFileContent(COMPRESSED_MXFILE)
|
||||||
|
expect(r.ok).toBe(true)
|
||||||
|
if (r.ok) {
|
||||||
|
expect(r.xml).toContain("<mxGraphModel")
|
||||||
|
expect(r.xml).toContain('value="Hello"')
|
||||||
|
// The compressed blob must be gone.
|
||||||
|
expect(r.xml).not.toContain(drawioCompress(MODEL_XML))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
it("decompresses only the compressed pages of a mixed file", () => {
|
||||||
|
const mixed = `<mxfile><diagram id="a" name="Plain">${MODEL_XML}</diagram><diagram id="b" name="Squeezed">${drawioCompress(MODEL_XML)}</diagram></mxfile>`
|
||||||
|
const r = parseDrawioFileContent(mixed)
|
||||||
|
expect(r.ok).toBe(true)
|
||||||
|
if (r.ok) {
|
||||||
|
const doc = new DOMParser().parseFromString(r.xml, "text/xml")
|
||||||
|
const diagrams = Array.from(
|
||||||
|
doc.querySelectorAll("diagram"),
|
||||||
|
) as Element[]
|
||||||
|
expect(diagrams).toHaveLength(2)
|
||||||
|
for (const d of diagrams) {
|
||||||
|
expect(d.querySelector("mxGraphModel")).not.toBeNull()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
it("keeps empty pages as-is", () => {
|
||||||
|
const withEmpty = `<mxfile><diagram id="a" name="Page-1">${MODEL_XML}</diagram><diagram id="b" name="Empty"></diagram></mxfile>`
|
||||||
|
const r = parseDrawioFileContent(withEmpty)
|
||||||
|
expect(r).toEqual({ ok: true, xml: withEmpty })
|
||||||
|
})
|
||||||
|
|
||||||
|
it("rejects empty files", () => {
|
||||||
|
const r = parseDrawioFileContent(" ")
|
||||||
|
expect(r.ok).toBe(false)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("rejects non-drawio content", () => {
|
||||||
|
const r = parseDrawioFileContent("<svg><rect/></svg>")
|
||||||
|
expect(r.ok).toBe(false)
|
||||||
|
if (!r.ok) expect(r.error).toContain("Not a draw.io file")
|
||||||
|
})
|
||||||
|
|
||||||
|
it("rejects a page whose content is neither XML nor compressed", () => {
|
||||||
|
const bad = `<mxfile><diagram id="a" name="Broken">!!! not a diagram !!!</diagram></mxfile>`
|
||||||
|
const r = parseDrawioFileContent(bad)
|
||||||
|
expect(r.ok).toBe(false)
|
||||||
|
if (!r.ok) expect(r.error).toContain('"Broken"')
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -31,6 +31,7 @@ const tsxBin = path.resolve(
|
|||||||
const EXPECTED_TOOLS = [
|
const EXPECTED_TOOLS = [
|
||||||
"start_session",
|
"start_session",
|
||||||
"create_new_diagram",
|
"create_new_diagram",
|
||||||
|
"load_diagram",
|
||||||
"edit_diagram",
|
"edit_diagram",
|
||||||
"get_diagram",
|
"get_diagram",
|
||||||
"export_diagram",
|
"export_diagram",
|
||||||
|
|||||||
Reference in New Issue
Block a user