/**
* Unit tests for load_diagram's file parsing (load-diagram.ts).
*
* A .drawio file stores each page's 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 whose every page is plain XML.
*/
import { deflateRawSync } from "node:zlib"
import { DOMParser } from "linkedom"
import { beforeAll, describe, expect, it } from "vitest"
import { installDomPolyfill } from "../src/dom.ts"
// Install the DOM polyfills exactly as index.ts does at runtime.
beforeAll(() => {
installDomPolyfill()
})
import {
decompressPageContent,
extractSvgEmbeddedXml,
parseDrawioFileContent,
} from "../src/load-diagram.ts"
const MODEL_XML = ``
/** 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 = `${MODEL_XML}`
const COMPRESSED_MXFILE = `${drawioCompress(MODEL_XML)}`
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,
hadCompressedPages: false,
})
})
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(" {
const r = parseDrawioFileContent(
MODEL_XML.replace("", ""),
)
expect(r.ok).toBe(false)
})
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(" {
const mixed = `${MODEL_XML}${drawioCompress(MODEL_XML)}`
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 = `${MODEL_XML}`
const r = parseDrawioFileContent(withEmpty)
expect(r).toEqual({
ok: true,
xml: withEmpty,
hadCompressedPages: false,
})
})
it("rejects empty files", () => {
const r = parseDrawioFileContent(" ")
expect(r.ok).toBe(false)
})
it("rejects non-drawio content", () => {
const r = parseDrawioFileContent("")
expect(r.ok).toBe(false)
if (!r.ok) expect(r.error).toContain("Not a draw.io file")
})
it("rejects an SVG without an embedded diagram", () => {
const r = parseDrawioFileContent("")
expect(r.ok).toBe(false)
if (!r.ok) expect(r.error).toContain("embedded diagram")
})
it("rejects a page whose content is neither XML nor compressed", () => {
const bad = `!!! not a diagram !!!`
const r = parseDrawioFileContent(bad)
expect(r.ok).toBe(false)
if (!r.ok) expect(r.error).toContain('"Broken"')
})
})
describe("hadCompressedPages", () => {
it("is false for a plain-XML mxfile", () => {
const r = parseDrawioFileContent(PLAIN_MXFILE)
expect(r.ok).toBe(true)
if (r.ok) expect(r.hadCompressedPages).toBe(false)
})
it("is false for a bare mxGraphModel", () => {
const r = parseDrawioFileContent(MODEL_XML)
expect(r.ok).toBe(true)
if (r.ok) expect(r.hadCompressedPages).toBe(false)
})
it("is true for a fully compressed mxfile", () => {
const r = parseDrawioFileContent(COMPRESSED_MXFILE)
expect(r.ok).toBe(true)
if (r.ok) expect(r.hadCompressedPages).toBe(true)
})
it("is true for a mixed plain/compressed file", () => {
const mixed = `${MODEL_XML}${drawioCompress(MODEL_XML)}`
const r = parseDrawioFileContent(mixed)
expect(r.ok).toBe(true)
if (r.ok) expect(r.hadCompressedPages).toBe(true)
})
})
/**
* Escape text for an XML attribute the way draw.io/Chromium serialise the
* diagram into the SVG root's content attribute.
*/
function escapeAttr(text: string): string {
return text
.replace(/&/g, "&")
.replace(//g, ">")
.replace(/"/g, """)
.replace(/\n/g, "
")
}
/** Build a .drawio.svg (Editable SVG) with the given mxfile embedded. */
function drawioSvg(mxfileXml: string): string {
return ``
}
const SVG_PROLOG = `
`
describe("extractSvgEmbeddedXml", () => {
it("returns undefined for non-SVG text", () => {
expect(extractSvgEmbeddedXml(PLAIN_MXFILE)).toBeUndefined()
expect(extractSvgEmbeddedXml("hello")).toBeUndefined()
})
it("returns an empty string for an SVG without content", () => {
expect(extractSvgEmbeddedXml("")).toBe("")
})
it("returns the decoded content attribute", () => {
expect(extractSvgEmbeddedXml(drawioSvg(PLAIN_MXFILE))).toBe(
PLAIN_MXFILE,
)
})
})
describe("parseDrawioFileContent with .drawio.svg", () => {
it("loads a compressed one-page mxfile embedded in an SVG", () => {
const r = parseDrawioFileContent(drawioSvg(COMPRESSED_MXFILE))
expect(r.ok).toBe(true)
if (r.ok) {
expect(r.xml).toContain(" {
const r = parseDrawioFileContent(
SVG_PROLOG + drawioSvg(COMPRESSED_MXFILE),
)
expect(r.ok).toBe(true)
if (r.ok) expect(r.xml).toContain('value="Hello"')
})
it("round-trips " and
in an uncompressed embedded label", () => {
const model = MODEL_XML.replace(
'value="Hello"',
'value="say "hi"
next"',
)
// A pretty-printed file: the newline between tags becomes
in
// the SVG attribute, the label's entities become " etc.
const mxfile = `\n${model}\n`
const svg = drawioSvg(mxfile)
expect(svg).toContain(""")
expect(svg).toContain("
")
expect(svg).toContain(">
<diagram")
const r = parseDrawioFileContent(svg)
expect(r.ok).toBe(true)
if (r.ok) {
expect(r.xml).toContain('value="say "hi"
next"')
const doc = new DOMParser().parseFromString(r.xml, "text/xml")
const cell = doc.querySelector('mxCell[id="box1"]') as Element
expect(cell.getAttributeNode("value")?.value).toBe('say "hi"\nnext')
}
})
it("loads a two-page embedded mxfile as two pages", () => {
const twoPages = `${drawioCompress(MODEL_XML)}${drawioCompress(MODEL_XML.replace("Hello", "World"))}`
const r = parseDrawioFileContent(drawioSvg(twoPages))
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()
}
expect(r.xml).toContain('value="World"')
}
})
it("rejects an SVG whose content is not draw.io XML", () => {
const r = parseDrawioFileContent(
'',
)
expect(r.ok).toBe(false)
if (!r.ok) expect(r.error).toContain("not draw.io XML")
})
})