/** * Tests for XML syntax checking, autoFixXml and the XML serializer. * * linkedom (the DOM used in Node) parses leniently and never reports syntax * errors, so validation relies on the strict check in dom.ts. autoFixXml * runs on the whole document whenever any check fails, so its steps must * leave valid parts of the document untouched. */ import { beforeAll, describe, expect, it } from "vitest" import { getXmlSyntaxError, installDomPolyfill } from "../src/dom.js" beforeAll(() => { installDomPolyfill() }) import { addPageToDoc, parseMxfile, serializeMxfile } from "../src/pages.js" import { validateAndFixXml } from "../src/xml-validation.js" /** Bare model with the root cells plus the given cells. */ const model = (cells: string) => `${cells}` // A bare & makes the first validation fail, which triggers autoFixXml on // the whole document. const BROKEN_CELL = `` describe("getXmlSyntaxError", () => { it("accepts well-formed XML", () => { expect(getXmlSyntaxError(model(""))).toBeNull() }) it.each([ ["duplicate attribute", ``], ["unquoted attribute", ``], ["missing space between attributes", ``], ["bare ampersand", ``], ["unclosed tag", ``], ["plain text", `hello`], ])("reports %s", (_name, xml) => { expect(getXmlSyntaxError(xml)).toMatch(/^\d+:\d+: /) }) }) describe("validateAndFixXml", () => { it("rejects a duplicate style attribute", () => { const r = validateAndFixXml( model( ``, ), ) expect(r.valid).toBe(false) expect(r.error).toContain("duplicate attribute: style") }) it("rejects an unquoted attribute value", () => { const r = validateAndFixXml( model(``), ) expect(r.valid).toBe(false) }) it("keeps style values intact while fixing another cell", () => { const cells = `` const r = validateAndFixXml(model(cells + BROKEN_CELL)) expect(r.valid).toBe(true) expect(r.fixed).toContain('style="shape=cylinder3;whiteSpace=wrap;"') expect(r.fixed).toContain('style="edgeStyle=orthogonalEdgeStyle;"') expect(r.fixed).toContain('value="R&D"') }) it("keeps " inside rich-text labels", () => { const rich = `` const r = validateAndFixXml(model(rich + BROKEN_CELL)) expect(r.valid).toBe(true) expect(r.fixed).toContain( 'value="<font style="color: red;">Hi</font>"', ) expect(getXmlSyntaxError(r.fixed ?? "")).toBeNull() }) it("keeps UserObject and object wrappers", () => { const wrapped = `` const r = validateAndFixXml(model(wrapped + BROKEN_CELL)) expect(r.valid).toBe(true) expect(r.fixed).toContain(' { const xml = [ "", "", '', '', BROKEN_CELL, '', "", "", ].join("\n") const r = validateAndFixXml(xml) expect(r.valid).toBe(true) expect(r.fixes).toEqual(["Escaped unescaped & characters"]) }) it("leaves cells split over two lines alone while fixing another cell", () => { const xml = [ "", '', '', ' ', '', ' ', BROKEN_CELL, "", ].join("\n") const r = validateAndFixXml(xml) expect(r.valid).toBe(true) expect(r.fixes).toEqual(["Escaped unescaped & characters"]) }) it("renames duplicate short ids without touching the attribute name", () => { const r = validateAndFixXml( model( ``, ), ) expect(r.valid).toBe(true) expect(r.fixed).toContain('id="d_dup1"') expect(r.fixed).toContain('id="i_dup1"') }) it("adds a missing space between attributes", () => { const r = validateAndFixXml( model( ``, ), ) expect(r.valid).toBe(true) expect(r.fixed).toContain(' { const r = validateAndFixXml( model( ``, ), ) expect(r.valid).toBe(true) expect(r.fixed).toContain('value="Hello"') }) }) describe("XML serializer and strict parsing in page helpers", () => { it("keeps line breaks and tabs in attribute values", () => { const xml = `` const out = serializeMxfile(parseMxfile(xml) as Document) expect(out).toContain('value="Multi-Head Attention x"') expect(out).not.toMatch(/value="[^"]*\n/) }) it("escapes special characters in attributes and text", () => { const xml = `a < b` const out = serializeMxfile(parseMxfile(xml) as Document) expect(out).toBe(xml) }) it("parseMxfile returns null for malformed XML", () => { expect( parseMxfile( ``, ), ).toBeNull() }) it("addPageToDoc rejects malformed page XML", () => { const doc = parseMxfile( `${model("")}`, ) as Document expect(() => addPageToDoc(doc, { xml: model(``), }), ).toThrow() }) })