import { expect, test } from "@playwright/test"; import type { PlaitElement } from "@plait/core"; import { getNoteDocument, serializeNoteDocument, } from "@/lib/flow-board/topic-notes"; import { addDays, dateIndex, DAY, getTopicTask, patchTopicTask, scheduleTaskChanges, validateTopicTask, type TopicTask, } from "@/lib/flow-board/topic-tasks"; import { exportXmind } from "@/lib/flow-board/xmind-export"; import { patchTopicDetails } from "@/lib/flow-board/topic-details"; import JSZip from "jszip"; const task = (start = "2026-10-05", duration = 2): TopicTask => ({ start, due: addDays(start, duration - 1), duration, progress: 25, priority: 2, assignees: ["小明"], dependencies: [], }); const topic = (id: string, t: TopicTask): PlaitElement => ({ id, type: "mindmap", children: [], data: { topic: { children: [{ text: id }] } }, xmind: patchTopicTask({ id }, t), }); test("rich notes retain marks, nested links and lists through serialization, and reject executable links", async () => { const source = { id: "notes", xmind: { notes: { plain: { content: "粗体链接\n步骤" }, html: { content: { paragraphs: [ { spans: [ { text: "粗体", style: { properties: { "fo:font-weight": "bold" } }, }, { href: "https://example.com", spans: [ { text: "链接", style: { properties: { "fo:font-style": "italic", "fo:text-decoration": "underline", }, }, }, ], }, ], }, { style: { properties: { "fo:text-bullet": "number" } }, spans: [{ text: "步骤", href: "javascript:alert(1)" }], }, ], }, }, }, labels: ["保留"], }, }; const document = getNoteDocument(source); expect(document[0].children[0].bold).toBe(true); expect(document[0].children[1]).toMatchObject({ href: "https://example.com/", italic: true, underline: true, }); expect(document[1].list).toBe("number"); expect(document[1].children[0].href).toBeUndefined(); const metadata = patchTopicDetails(source, { notesDocument: document }); expect(metadata.labels).toEqual(["保留"]); expect( serializeNoteDocument(getNoteDocument({ id: "notes", xmind: metadata })) ).toEqual(metadata.notes); const result = await exportXmind( { children: [ { id: "notes", type: "mindmap", xmind: metadata, data: { topic: { children: [{ text: "测试" }] } }, }, ], }, "笔记" ); const zip = await JSZip.loadAsync(await result.blob.arrayBuffer()); expect( JSON.parse(await zip.file("content.json")!.async("string"))[0].rootTopic .notes ).toEqual(metadata.notes); }); test("native task extensions export dates, fractional progress, millisecond durations and remapped dependencies", async () => { const a = topic("a", task()), b = topic("b", { ...task("2026-10-07", 3), dependencies: [{ id: "a", type: "FS", lag: DAY }], }); const root = { id: "root", type: "mindmap", data: { topic: { children: [{ text: "计划" }] } }, children: [a, b], }; expect(getTopicTask(a)).toEqual(task()); const result = await exportXmind({ children: [root] }, "计划"); const zip = await JSZip.loadAsync(await result.blob.arrayBuffer()), content = JSON.parse(await zip.file("content.json")!.async("string")); const [one, two] = content[0].rootTopic.children.attached; expect(one.extensions[0].provider).toBe("org.xmind.ui.task"); expect(one.extensions[0].content).toMatchObject({ progress: 0.25, duration: 2 * DAY, assignees: ["小明"], }); expect(two.extensions[0].content.dependencies).toEqual([ { id: one.id, type: "FS", lag: DAY }, ]); }); test("task scheduling propagates dependencies, detects cycles and invalid dates, and rolls across leap days", () => { const a = topic("a", task()), b = topic("b", { ...task("2026-10-07"), dependencies: [{ id: "a", type: "FS", lag: 0 }], }), c = topic("c", { ...task("2026-10-09"), dependencies: [{ id: "b", type: "FS", lag: 0 }], }); const changes = scheduleTaskChanges([a, b, c], "a", task("2026-10-08", 3)); expect(changes.get("b")?.start).toBe("2026-10-11"); expect(changes.get("c")?.start).toBe("2026-10-13"); expect( validateTopicTask( "a", { ...task(), dependencies: [{ id: "c", type: "FS", lag: 0 }] }, [a, b, c] ) ).toContain("循环"); expect( validateTopicTask("a", { ...task(), due: "2026-10-01" }, [a, b, c]) ).toContain("早于"); expect(Number.isNaN(dateIndex("2026-02-30"))).toBe(true); expect(addDays("2028-02-28", 1)).toBe("2028-02-29"); const independent = patchTopicTask(a, null); expect(getTopicTask({ ...a, xmind: independent })).toBeNull(); });