import pako from "pako" import { describe, expect, it } from "vitest" import { diffDiagrams, EMPTY_SUMMARY, isSameDocument } from "@/lib/diagram-diff" const doc = (cells: string) => `${cells}` const box = (id: string, value = id) => `` const edge = (id: string, source: string, target: string) => `` const pageOf = (name: string, cells: string, attrs = "") => `${cells}` const file = (...pages: string[]) => `${pages.join("")}` const packedPage = (name: string, cells: string) => { const model = `${cells}` const packed = Buffer.from( pako.deflateRaw(encodeURIComponent(model)), ).toString("base64") return `${packed}` } describe("diffDiagrams", () => { it("counts added shapes and connectors on an empty canvas", () => { const { summary, touchedIds } = diffDiagrams( "", doc(box("a") + box("b") + edge("e", "a", "b")), ) expect(summary.shapesAdded).toBe(2) expect(summary.edgesAdded).toBe(1) expect(touchedIds.sort()).toEqual(["a", "b", "e"]) }) it("separates added, changed and removed cells", () => { const before = doc(box("a") + box("b") + edge("e", "a", "b")) const after = doc(box("a", "renamed") + box("c") + edge("e", "a", "c")) const { summary, touchedIds } = diffDiagrams(before, after) expect(summary).toEqual({ shapesAdded: 1, shapesRemoved: 1, shapesChanged: 1, edgesAdded: 0, edgesRemoved: 0, edgesChanged: 1, }) expect(touchedIds.sort()).toEqual(["a", "c", "e"]) }) it("ignores how the same cell is written", () => { // As the model writes it, and as draw.io saves it (the style as is) const model = `` const saved = `` expect(diffDiagrams(model, saved).summary).toEqual(EMPTY_SUMMARY) }) it("reports nothing for identical diagrams", () => { const xml = doc(box("a")) const { summary, touchedIds } = diffDiagrams(xml, xml) expect(summary).toEqual(EMPTY_SUMMARY) expect(touchedIds).toEqual([]) }) it("ignores values draw.io drops when it saves", () => { // The model writes defaults and long numbers; draw.io leaves them out const model = doc( ``, ) const saved = doc( ``, ) expect(diffDiagrams(model, saved).summary).toEqual(EMPTY_SUMMARY) expect(isSameDocument(model, saved)).toBe(true) }) it("sees any change to a style, its order included", () => { const styled = (style: string) => doc( ``, ) const same = (a: string, b: string) => isSameDocument(styled(a), styled(b)) expect( same("fillColor=#f00;rounded=1", "fillColor=#f00;rounded=1"), ).toBe(true) // draw.io applies named styles and keys in order expect( same( "fillColor=#f00;defaultVertex", "defaultVertex;fillColor=#f00", ), ).toBe(false) // A leading ";" leaves the default style out expect(same("fillColor=#f00", ";fillColor=#f00")).toBe(false) expect( diffDiagrams(styled("fillColor=#f00"), styled(";fillColor=#f00")) .summary.shapesChanged, ).toBe(1) }) it("tells a label with = in it from a separate attribute", () => { const wrapped = (attrs: string) => doc( ``, ) const a = wrapped('label="server region=us-east-1"') const b = wrapped('label="server" region="us-east-1"') expect(isSameDocument(a, b)).toBe(false) expect(diffDiagrams(a, b).summary.shapesChanged).toBe(1) }) it("reads only a cell's style as a style, not data named style", () => { const wrapped = (style: string) => doc( ``, ) expect(isSameDocument(wrapped("alpha;"), wrapped("alpha"))).toBe(false) }) it("does not count layers as shapes", () => { const before = doc('') const after = doc(box("r")) expect(diffDiagrams(before, after)).toEqual({ summary: { ...EMPTY_SUMMARY, shapesAdded: 1 }, touchedIds: ["r"], fromScratch: true, }) }) it("ignores empty style entries, as draw.io does", () => { const styled = (style: string) => doc( ``, ) const same = (a: string, b: string) => isSameDocument(styled(a), styled(b)) expect(same("rounded=1;html=1;", "rounded=1;html=1")).toBe(true) expect(same("rounded=1;;html=1", "rounded=1;html=1")).toBe(true) expect( diffDiagrams( styled("rounded=1;html=1;"), styled("rounded=1;html=1"), ).summary, ).toEqual(EMPTY_SUMMARY) // A leading ";" still leaves the default style out expect(same(";fillColor=#f00", "fillColor=#f00")).toBe(false) }) it("reads the id from wrappers", () => { const wrapped = `` const { summary, touchedIds } = diffDiagrams(doc(""), doc(wrapped)) expect(summary.shapesAdded).toBe(1) expect(touchedIds).toEqual(["u1"]) }) it("only looks at the first page", () => { const twoPages = file(pageOf("p1", box("a")), pageOf("p2", box("z"))) expect(diffDiagrams("", twoPages).summary.shapesAdded).toBe(1) }) it("reads compressed pages", () => { const xml = file(packedPage("p1", box("a") + box("b"))) expect(diffDiagrams("", xml).summary.shapesAdded).toBe(2) expect(diffDiagrams(xml, xml).fromScratch).toBe(false) }) it("treats unparsable XML as empty", () => { expect( diffDiagrams("", doc(box("a"))).fromScratch, ).toBe(true) }) }) describe("isSameDocument", () => { const a = file(pageOf("One", box("a")), pageOf("Two", box("b"))) it("is true for the same pages written differently", () => { const b = file( pageOf("One", box("a"), ' dx="300" dy="120" grid="1"'), packedPage("Two", box("b")), ) expect(isSameDocument(a, b)).toBe(true) }) it("sees a change on the second page", () => { const b = file(pageOf("One", box("a")), pageOf("Two", box("b", "x"))) expect(diffDiagrams(a, b).summary).toEqual(diffDiagrams(a, a).summary) expect(isSameDocument(a, b)).toBe(false) }) it("sees added pages, renamed pages and changed page settings", () => { expect(isSameDocument(a, file(pageOf("One", box("a"))))).toBe(false) expect( isSameDocument( a, file(pageOf("One", box("a")), pageOf("Renamed", box("b"))), ), ).toBe(false) expect( isSameDocument( file(pageOf("One", box("a"), ' background="#ffffff"')), file(pageOf("One", box("a"), ' background="#000000"')), ), ).toBe(false) }) it("compares a setting written on one side with draw.io's default", () => { const withAttrs = (attrs: string) => file(pageOf("One", box("a"), attrs)) const plain = withAttrs("") // Defaults, as draw.io fills them in expect(isSameDocument(plain, withAttrs(' math="0" grid="1"'))).toBe( true, ) // A page without a paper size keeps the canvas's, whatever it is expect( isSameDocument( plain, withAttrs(' pageWidth="1654" pageHeight="2339"'), ), ).toBe(true) // Real changes expect(isSameDocument(plain, withAttrs(' math="1"'))).toBe(false) expect(isSameDocument(plain, withAttrs(' shadow="1"'))).toBe(false) expect( isSameDocument( withAttrs(' pageWidth="850" pageHeight="1100"'), withAttrs(' pageWidth="1654" pageHeight="2339"'), ), ).toBe(false) expect( isSameDocument( plain, withAttrs( ' backgroundImage="{"src":"x.png"}"', ), ), ).toBe(false) }) it("sees a background set on one side", () => { expect( isSameDocument( file(pageOf("One", box("a"))), file(pageOf("One", box("a"), ' background="#000000"')), ), ).toBe(false) }) it("sees shapes brought to front or sent back", () => { expect( isSameDocument( file(pageOf("One", box("a") + box("b"))), file(pageOf("One", box("b") + box("a"))), ), ).toBe(false) }) it("sees a hidden layer", () => { const hidden = file(pageOf("One", box("a"))).replace( '', '', ) expect(isSameDocument(file(pageOf("One", box("a"))), hidden)).toBe( false, ) }) it("does not care where children of other parents are written", () => { const group = (inner: string) => `${inner}` const child = `` expect( isSameDocument( file(pageOf("One", group(child) + box("a"))), file(pageOf("One", group("") + box("a") + child)), ), ).toBe(true) }) it("compares file variables, in any order; none is the same as {}", () => { const withVars = (vars: string | null) => `${pageOf("One", box("a"))}` const team = (value: string) => `{"team":"${value}","x":"1"}` expect( isSameDocument(withVars(team("Old")), withVars(team("New"))), ).toBe(false) expect( isSameDocument( withVars(team("Old")), withVars( "{"x":"1","team":"Old"}", ), ), ).toBe(true) expect(isSameDocument(withVars(null), withVars(team("Old")))).toBe( false, ) expect(isSameDocument(withVars(null), withVars("{}"))).toBe(true) }) it("compares a wrapper's own data as written, geometry as numbers", () => { const wrapped = (x: string, geometryX: string) => file( pageOf( "One", ``, ), ) expect(isSameDocument(wrapped("01", "40"), wrapped("1", "40"))).toBe( false, ) expect(isSameDocument(wrapped("1", "40.0"), wrapped("1", "40"))).toBe( true, ) }) it("reads numeric page settings as numbers", () => { const page = (attrs: string) => file(pageOf("One", box("a"), attrs)) expect( isSameDocument( page(' gridSize="10.0" pageScale="1.0"'), page(' gridSize="10" pageScale="1"'), ), ).toBe(true) // Written on one side only: the default, also as 10.0 expect(isSameDocument(page(' gridSize="10.0"'), page(""))).toBe(true) expect( isSameDocument(page(' gridSize="20"'), page(' gridSize="10"')), ).toBe(false) }) })