Files
next-ai-draw-io/tests/unit/redesign-helpers.test.ts
T
dayuan.jiang d87c3c0bab Merge origin/main (#984, the canvas shell) into the cleanup branch
Conflicts resolved in 16 files. Where main's new shell, classic page and
tests depend on a wire format or a signature the cleanup had changed, main
wins; where main only extended code the cleanup had restructured, main's
change is carried over into the new structure:

- MCP HTTP server: the poll response keeps main's separate export fields
  (exportFormat, exportXml, exportOptions, exportId with a random id)
  instead of the cleanup's pendingExport object; the classic preview page
  is main's version
- MCP index: start_session keeps the cleanup's structure and gains main's
  BROWSER=none, previewUrl and same-origin note; get_selection is written
  in the registerTool style; create_new_diagram keeps the cleanup's
  helpers with main's comment
- history: addHistory takes main's source parameter and returns the index
  again (main's signature)
- new-diagram: the cleanup's prepareNewModel split plus main's page ids
  for diagrams without one
- diagram context: one ExportResult type (main's, with optional fields)
  for both the tagged exports and main's requestExport; the cleanup's
  single export resolver stays
- versions store and version-text test: main's VersionItem base type and
  describeChangeSummary rename, without the legacy history conversion the
  cleanup removed
- dictionaries: main's shell section added; openFileHint stays removed
  (unused on main too)
- tests: the server wiring test keeps the SDK client and gains main's
  session cases; the HTTP server test uses main's export fields; the DOM
  polyfill comes from tests/setup.ts
- package.json: main's postcss kept, the cleanup's removals kept; lockfile
  regenerated
2026-10-12 11:59:51 +09:00

191 lines
7.1 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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 { describeChangeSummary, describeTotals } from "@/lib/version-text"
import { MAX_VERSIONS, useVersionsStore } from "@/stores/versions-store"
describe("withPageDefaults", () => {
it("adds page and grid when the model has none", () => {
expect(withPageDefaults("<mxGraphModel><root/></mxGraphModel>")).toBe(
'<mxGraphModel page="1" grid="1" gridSize="10"><root/></mxGraphModel>',
)
})
it("keeps values the diagram already sets", () => {
const xml = '<mxGraphModel dx="10" page="0" grid="0" gridSize="5">'
expect(withPageDefaults(xml)).toBe(xml)
})
it("reads attributes written with spaces around =", () => {
const xml = '<mxGraphModel page = "0" grid ="0" gridSize= "5">'
expect(withPageDefaults(xml)).toBe(xml)
})
it("handles self-closing models and several pages", () => {
const xml =
'<mxfile><diagram><mxGraphModel/></diagram><diagram><mxGraphModel page="0"></mxGraphModel></diagram></mxfile>'
expect(withPageDefaults(xml)).toBe(
'<mxfile><diagram><mxGraphModel page="1" grid="1" gridSize="10"/></diagram><diagram><mxGraphModel page="0" grid="1" gridSize="10"></mxGraphModel></diagram></mxfile>',
)
})
})
describe("describeChangeSummary", () => {
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(
describeChangeSummary(
{ ...EMPTY_SUMMARY, shapesAdded: 2, edgesChanged: 1 },
en,
),
).toBe("Added 2 shapes, changed 1 connector")
})
it("says so when nothing changed", () => {
expect(describeChangeSummary({ ...EMPTY_SUMMARY }, en)).toBe(
"No visible changes",
)
})
it("uses the language's list separator", () => {
const zh = {
shapesAddedOther: "新增 {count} 个图形",
edgesChangedOne: "改了 1 条连线",
listSeparator: ",",
}
expect(
describeChangeSummary(
{ ...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<string, string>,
)
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: `<x n="${n}"/>`,
beforeXml: `<before n="${n}"/>`,
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('<before n="2"/>')
})
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", () => {
add(0)
add(2)
add(4)
useVersionsStore.getState().removeFromTurn(4)
add(4)
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()
})
})