/** * 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 &quot; etc. const mxfile = `\n${model}\n` const svg = drawioSvg(mxfile) expect(svg).toContain("&quot;") expect(svg).toContain("&#xa;") 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") }) })