import { expect, test } from "@playwright/test"; import JSZip from "jszip"; import { exportXmind } from "@/lib/flow-board/xmind-export"; const topic = (id: string, title: string, children: any[] = []): any => ({ id, type: "mind_child", data: { topic: { children: [{ text: title }] } }, children, }); const root = (id: string, title: string, children: any[] = []): any => ({ ...topic(id, title, children), type: "mindmap", layout: "right", points: [[0, 0]], }); const read = async (result: Awaited>) => JSZip.loadAsync(await result.blob.arrayBuffer()); test("XMind export uses current active content, fresh IDs and valid cross-topic references", async () => { const a = topic("a", "研发"); const b = topic("b", "上线"); const r = root("r", "最新中心", [ a, b, { ...topic("sum", "第一阶段"), start: 0, end: 1 }, ]); a.xmind = { topicLinkId: "b", notes: { plain: { content: "计划\n第二行" } }, labels: ["工作"], }; a.data.topic.children[0] = { text: "研发", bold: true, "font-size": 24 }; const elements = [ r, { id: "boundary", type: "geometry", mindAnnotation: { kind: "boundary", topicIds: ["a", "b"], padding: 18 }, }, { id: "rel", type: "arrow-line", texts: [{ text: { children: [{ text: "前置" }] } }], mindAnnotation: { kind: "relationship", sourceId: "a", targetId: "b" }, }, ]; const snapshot = { activeSheetId: "one", children: elements, sheets: [ { id: "one", title: "计划", children: [root("r", "旧数据")] }, { id: "two", title: "归档", children: [root("r", "第二中心")] }, ], }; const before = JSON.stringify(snapshot); const result = await exportXmind(snapshot, "计划"); const zip = await read(result); const sheets = JSON.parse(await zip.file("content.json")!.async("string")); expect(sheets).toHaveLength(2); const out = sheets[0].rootTopic; expect(out.title).toBe("最新中心"); expect(out.children.attached[0].href).toBe( `xmind:#${out.children.attached[1].id}` ); expect(out.children.attached[0].notes.plain.content).toBe("计划\n第二行"); expect(out.children.attached[0].style.properties["fo:font-size"]).toBe("24"); expect(out.boundaries[0].range).toBe("(0,1)"); expect(out.summaries[0].topicId).toBe(out.children.summary[0].id); expect(sheets[0].relationships[0]).toMatchObject({ end1Id: out.children.attached[0].id, end2Id: out.children.attached[1].id, title: "前置", }); expect(sheets[1].rootTopic.id).not.toBe(out.id); expect(JSON.stringify(snapshot)).toBe(before); expect(result.warnings).toEqual([]); }); test("XMind attachment bytes and manifest entries are portable", async () => { const r = root("root", "附件"); r.xmind = { attachment: { name: "需求.txt", url: "data:application/octet-stream;base64,5YaF5a65", mimeType: "application/octet-stream", }, }; const zip = await read(await exportXmind({ children: [r] }, "附件")); const sheets = JSON.parse(await zip.file("content.json")!.async("string")); const path = sheets[0].rootTopic.href.replace(/^xap:/, ""); expect(await zip.file(path)!.async("string")).toBe("内容"); expect( JSON.parse(await zip.file("manifest.json")!.async("string"))["file-entries"] ).toHaveProperty([path]); expect( JSON.parse(await zip.file("metadata.json")!.async("string")) .dataStructureVersion ).toBe("2"); }); test("unsupported board elements warn and empty mind sheets never export misleading workbooks", async () => { const result = await exportXmind( { children: [root("r", "中心"), { id: "text", type: "text" }] }, "测试" ); expect(result.warnings[0]).toContain("自由画布"); await expect(exportXmind({ children: [] }, "空白")).rejects.toThrow( "没有可导出" ); });