import { beforeEach, describe, expect, it } from "vitest" import { formatSelectionContext } from "@/lib/chat-helpers" import { EMPTY_SUMMARY } from "@/lib/diagram-diff" import { withPageDefaults } from "@/lib/drawio/drawio-config" import enDict from "@/lib/i18n/dictionaries/en.json" import jaDict from "@/lib/i18n/dictionaries/ja.json" import zhDict from "@/lib/i18n/dictionaries/zh.json" import zhHantDict from "@/lib/i18n/dictionaries/zh-Hant.json" import { describeChanges, describeTotals } from "@/lib/version-text" import { MAX_VERSIONS, useVersionsStore, versionsFromLegacyHistory, } from "@/stores/versions-store" describe("withPageDefaults", () => { it("adds page and grid when the model has none", () => { expect(withPageDefaults("")).toBe( '', ) }) it("keeps values the diagram already sets", () => { const xml = '' expect(withPageDefaults(xml)).toBe(xml) }) it("reads attributes written with spaces around =", () => { const xml = '' expect(withPageDefaults(xml)).toBe(xml) }) it("handles self-closing models and several pages", () => { const xml = '' expect(withPageDefaults(xml)).toBe( '', ) }) }) describe("describeChanges", () => { const en = { shapesAddedOne: "added 1 shape", shapesAddedOther: "added {count} shapes", edgesChangedOne: "changed 1 connector", edgesChangedOther: "changed {count} connectors", noChanges: "No visible changes", listSeparator: ", ", } it("uses singular and plural forms and sentence case", () => { expect( describeChanges( { ...EMPTY_SUMMARY, shapesAdded: 2, edgesChanged: 1 }, en, ), ).toBe("Added 2 shapes, changed 1 connector") }) it("says so when nothing changed", () => { expect(describeChanges({ ...EMPTY_SUMMARY }, en)).toBe( "No visible changes", ) }) it("uses the language's list separator", () => { const zh = { shapesAddedOther: "新增 {count} 个图形", edgesChangedOne: "改了 1 条连线", listSeparator: ",", } expect( describeChanges( { ...EMPTY_SUMMARY, shapesAdded: 3, edgesChanged: 1 }, zh, ), ).toBe("新增 3 个图形,改了 1 条连线") }) }) describe("formatSelectionContext", () => { it("lists selected ids with their labels", () => { const text = formatSelectionContext([ { id: "4", label: "EC2" }, { id: "cf", label: "" }, ]) expect(text).toContain('- id="4" (EC2)') expect(text).toContain('- id="cf"') expect(text).not.toContain('id="cf" (') }) it("returns nothing for missing or malformed input", () => { expect(formatSelectionContext(undefined)).toBe("") expect(formatSelectionContext("4")).toBe("") expect(formatSelectionContext([{ label: "no id" }])).toBe("") }) it("strips quotes and newlines and limits length and count", () => { const many = Array.from({ length: 80 }, (_, i) => ({ id: `c${i}`, label: `line\n"quoted"${"x".repeat(200)}`, })) const text = formatSelectionContext(many) const lines = text.split("\n").filter((l) => l.startsWith("- id=")) expect(lines).toHaveLength(50) // The model is told the list is cut short expect(text).toContain("- and 30 more selected shapes not listed") expect(text).not.toContain('"quoted"') expect(lines[0].length).toBeLessThan(130) }) }) describe("describeTotals", () => { it("uses singular and plural forms from the dictionaries", () => { const dicts = [enDict, zhDict, jaDict, zhHantDict].map( (d) => d.versions as Record, ) const one = { ...EMPTY_SUMMARY, shapesAdded: 1, edgesAdded: 0 } const many = { ...EMPTY_SUMMARY, shapesAdded: 3, edgesAdded: 1 } expect(dicts.map((t) => describeTotals(one, t))).toEqual([ "1 shape, 0 connectors", "共 1 个图形、0 条连线", "図形 1 個、コネクタ 0 本", "共 1 個圖形、0 條連線", ]) expect(describeTotals(many, dicts[0])).toBe("3 shapes, 1 connector") }) }) describe("versions store", () => { beforeEach(() => useVersionsStore.getState().clear()) const add = (n: number) => useVersionsStore.getState().addVersion({ xml: ``, beforeXml: ``, turnIndex: n, summary: { ...EMPTY_SUMMARY }, }) it("keeps the before-XML only on the newest version", () => { add(0) add(2) const [first, second] = useVersionsStore.getState().versions expect(first.beforeXml).toBeUndefined() expect(second.beforeXml).toBe('') }) it("keeps at most MAX_VERSIONS versions, each with its number", () => { for (let i = 0; i < MAX_VERSIONS + 5; i++) add(i) const { versions } = useVersionsStore.getState() expect(versions).toHaveLength(MAX_VERSIONS) expect(versions[0].turnIndex).toBe(5) // v6 stays v6 when v1 to v5 are dropped expect(versions[0].number).toBe(6) expect(versions.at(-1)?.number).toBe(MAX_VERSIONS + 5) }) it("numbers on from the last version kept, and numbers saved ones", () => { add(0) add(2) add(4) useVersionsStore.getState().removeFromTurn(4) add(4) expect( useVersionsStore.getState().versions.map((v) => v.number), ).toEqual([1, 2, 3]) const { versions } = useVersionsStore.getState() // Saved before versions had numbers useVersionsStore .getState() .setVersions(versions.map(({ number, ...v }) => v as any)) expect( useVersionsStore.getState().versions.map((v) => v.number), ).toEqual([1, 2, 3]) }) it("drops versions from a turn that is re-run, none left on the canvas", () => { add(0) add(2) add(4) useVersionsStore.getState().removeFromTurn(2) const state = useVersionsStore.getState() expect(state.versions.map((v) => v.turnIndex)).toEqual([0]) expect(state.onCanvasVersionId).toBeNull() }) it("hand edits clear both version flags", () => { const id = add(0) expect(useVersionsStore.getState().onCanvasVersionId).toBe(id) useVersionsStore.getState().clearCanvasFlags() let state = useVersionsStore.getState() expect(state.onCanvasVersionId).toBeNull() expect(state.versions[0].onCanvas).toBeUndefined() useVersionsStore.getState().setUndone(id) useVersionsStore.getState().clearCanvasFlags() state = useVersionsStore.getState() expect(state.undoneVersionId).toBeNull() expect(state.versions[0].undone).toBeUndefined() }) it("converts the old history list", () => { const versions = versionsFromLegacyHistory([ { svg: "data:image/svg+xml;base64,AA", xml: "" }, ]) expect(versions).toHaveLength(1) expect(versions[0]).toMatchObject({ xml: "", turnIndex: -1 }) expect(versionsFromLegacyHistory(undefined)).toEqual([]) }) })