Add native task metadata, working-day calendar settings, dependency scheduling and rich note editing. Support complete-sheet PNG export with resolution and transparency controls, preserve XMind extensions and update browser regressions.
169 lines
5.2 KiB
TypeScript
169 lines
5.2 KiB
TypeScript
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();
|
|
});
|