ai-chatbot/tests/flow-board/notes-gantt.spec.ts
134 c3f5903a86 feat(flow-board): add rich notes, Gantt tasks and PNG export
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.
2026-10-06 18:06:29 +08:00

280 lines
11 KiB
TypeScript

import { expect, test, type Page } from "@playwright/test";
import JSZip from "jszip";
import { clickXmindAction, clickXmindSheet } from "./editor-actions";
const DAY = 86400000;
async function loadPlan(page: Page) {
await page.goto("/dev/flow-board-preview", { waitUntil: "commit" });
const zip = new JSZip();
const task = (start: string, duration: number, progress = 0) => [
{
provider: "org.xmind.ui.task",
content: {
start: new Date(`${start}T00:00:00`).getTime(),
due: new Date(`${start}T23:59:59.999`).getTime() + (duration - 1) * DAY,
duration: duration * DAY,
progress,
assignees: ["小明"],
},
},
];
zip.file(
"content.json",
JSON.stringify([
{
id: "sheet",
title: "项目计划",
rootTopic: {
id: "root",
title: "项目",
structureClass: "org.xmind.ui.logic.right",
children: {
attached: [
{
id: "a",
title: "设计",
notes: { plain: { content: "第一行\n第二行" } },
extensions: task("2026-10-05", 2),
},
{
id: "b",
title: "开发",
extensions: [
{
provider: "org.xmind.ui.task",
content: {
...task("2026-10-07", 3)[0].content,
dependencies: [{ id: "a", type: "FS", lag: 0 }],
},
},
],
},
{ id: "c", title: "验收" },
],
},
},
},
{ title: "其他工作表", rootTopic: { id: "other", title: "其他" } },
])
);
await page
.locator("input[accept*=xmind]")
.setInputFiles({
name: "项目计划.xmind",
mimeType: "application/octet-stream",
buffer: await zip.generateAsync({ type: "nodebuffer" }),
});
await expect(
page.locator(".drawnix").getByText("设计", { exact: true })
).toBeVisible();
await page.evaluate(() => document.fonts.ready);
}
async function exported(page: Page) {
const promise = page.waitForEvent("download");
await clickXmindAction(page, "导出 XMind");
const d = await promise,
stream = await d.createReadStream(),
chunks: Buffer[] = [];
for await (const c of stream!) chunks.push(c);
const buffer = Buffer.concat(chunks);
await d.saveAs(test.info().outputPath("edited-plan.xmind"));
const zip = await JSZip.loadAsync(buffer);
return {
buffer,
sheets: JSON.parse(await zip.file("content.json")!.async("string")),
};
}
test("rich notes format selection, lists and safe links, undo, export and reimport", async ({
page,
}) => {
const errors: string[] = [];
page.on("pageerror", (e) => errors.push(e.message));
await loadPlan(page);
await page
.getByRole("button", { name: "查看笔记 设计", exact: true })
.click();
const note = page.getByLabel("主题备注");
await expect(note).toContainText("第一行");
await note.fill("交付说明");
await note.press("ControlOrMeta+a");
await page.getByRole("button", { name: "笔记加粗", exact: true }).click();
await expect(note.locator('span[style*="font-weight: 700"]')).toContainText(
"交付说明"
);
await page.getByRole("button", { name: "笔记有序列表", exact: true }).click();
await expect(note.locator('[data-note-list="number"]')).toHaveCount(1);
await page.getByRole("button", { name: "笔记链接", exact: true }).click();
await page.getByLabel("笔记链接地址").fill("https://example.com/spec");
await page.getByRole("button", { name: "确定", exact: true }).click();
await expect(note.locator("a")).toHaveAttribute(
"href",
"https://example.com/spec"
);
await clickXmindAction(page, "撤销");
await expect(note.locator("a")).toHaveCount(0);
await clickXmindAction(page, "重做");
await expect(note.locator("a")).toHaveCount(1);
await page.screenshot({ path: test.info().outputPath("rich-notes.png") });
await page.getByRole("button", { name: "关闭笔记", exact: true }).click();
const out = await exported(page);
const native = out.sheets[0].rootTopic.children.attached[0].notes;
expect(native.plain.content).toBe("交付说明");
expect(native.realHTML.content).toContain("<ol>");
expect(native.realHTML.content).toContain("<strong>");
expect(
native.html.content.paragraphs[0].spans[0].style.properties[
"fo:font-weight"
]
).toBe("bold");
await page
.locator("input[accept*=xmind]")
.setInputFiles({
name: "富文本往返.xmind",
mimeType: "application/octet-stream",
buffer: out.buffer,
});
await page.locator(".drawnix").getByText("设计", { exact: true }).click();
await clickXmindAction(page, "备注");
await expect(page.getByLabel("主题备注").locator("a")).toHaveAttribute(
"href",
"https://example.com/spec"
);
expect(errors).toEqual([]);
});
test("task dates, dependencies, Gantt move/resize, undo, filters and native file roundtrip", async ({
page,
}) => {
const errors: string[] = [];
page.on("pageerror", (e) => errors.push(e.message));
await loadPlan(page);
await page.locator(".drawnix").getByText("验收", { exact: true }).click();
await clickXmindAction(page, "任务");
const editor = page.getByRole("dialog", { name: "任务信息", exact: true });
await page.getByLabel("任务开始日期").fill("2026-10-10");
await page.getByLabel("任务工期").fill("2");
await expect(page.getByLabel("任务截止日期")).toHaveValue("2026-10-11");
await page.getByLabel("任务进度", { exact: true }).fill("50");
await page.getByLabel("任务负责人").fill("小明");
await page
.getByLabel("前置任务", { exact: true })
.selectOption({ label: "开发" });
await editor.getByRole("button", { name: "添加", exact: true }).click();
await editor.getByRole("button", { name: "保存任务", exact: true }).click();
await expect(editor).toHaveCount(0);
await clickXmindAction(page, "甘特图");
const gantt = page.getByRole("dialog", { name: "甘特图", exact: true });
await expect(gantt.locator(".xmind-gantt-bar")).toHaveCount(3);
await expect(gantt.locator("[data-gantt-dependency]")).toHaveCount(2);
const row = (name: string) =>
gantt
.locator(".xmind-gantt-row")
.filter({ has: page.getByRole("button", { name, exact: true }) });
await expect(row("验收")).toContainText("2026-10-10");
const design = gantt.getByRole("button", {
name: "任务时间条 设计",
exact: true,
});
await design.focus();
await design.press("ArrowRight");
await expect(row("设计")).toContainText("2026-10-06");
await expect(row("开发")).toContainText("2026-10-08");
await design.press("ControlOrMeta+z");
await expect(row("设计")).toContainText("2026-10-05");
await expect(row("开发")).toContainText("2026-10-07");
const box = await design.boundingBox();
expect(box).not.toBeNull();
await page.mouse.move(box!.x + box!.width / 2, box!.y + box!.height / 2);
await page.mouse.down();
await page.mouse.move(
box!.x + box!.width / 2 + 68,
box!.y + box!.height / 2,
{ steps: 5 }
);
await page.mouse.up();
await expect(row("设计")).toContainText("2026-10-07");
await expect(row("开发")).toContainText("2026-10-09");
await expect(row("验收")).toContainText("2026-10-12");
const resized = await design.boundingBox();
await page.mouse.move(resized!.x + resized!.width - 2, resized!.y + 10);
await page.mouse.down();
await page.mouse.move(resized!.x + resized!.width + 32, resized!.y + 10, {
steps: 4,
});
await page.mouse.up();
await expect(row("设计")).toContainText("3 天");
await page.getByLabel("搜索甘特图任务").fill("验收");
await expect(gantt.locator(".xmind-gantt-row")).toHaveCount(1);
await page.getByRole("button", { name: "清除筛选", exact: true }).click();
await expect(gantt.locator(".xmind-gantt-row")).toHaveCount(3);
await page.screenshot({ path: test.info().outputPath("gantt-tasks.png") });
await page.getByRole("button", { name: "关闭甘特图", exact: true }).click();
const out = await exported(page);
const topics = out.sheets[0].rootTopic.children.attached;
expect(topics[1].extensions[0].content.dependencies[0].id).toBe(topics[0].id);
await page
.locator("input[accept*=xmind]")
.setInputFiles({
name: "任务往返.xmind",
mimeType: "application/octet-stream",
buffer: out.buffer,
});
await clickXmindAction(page, "甘特图");
await expect(gantt.locator(".xmind-gantt-bar")).toHaveCount(3);
await expect(gantt.locator("[data-gantt-dependency]")).toHaveCount(2);
expect(errors).toEqual([]);
});
test("task validation, circular dependencies, removal, CSV export and sheet isolation", async ({
page,
}) => {
await loadPlan(page);
await page
.getByRole("button", { name: "编辑任务 设计", exact: true })
.click();
const editor = page.getByRole("dialog", { name: "任务信息", exact: true });
await page.getByLabel("任务截止日期").fill("2026-10-01");
await editor.getByRole("button", { name: "保存任务", exact: true }).click();
await expect(editor.getByRole("alert")).toContainText("不能早于");
await page.getByLabel("任务截止日期").fill("2026-10-06");
await page
.getByLabel("前置任务", { exact: true })
.selectOption({ label: "开发" });
await editor.getByRole("button", { name: "添加", exact: true }).click();
await expect(editor.getByRole("alert")).toContainText("循环");
await editor
.getByRole("button", { name: "关闭任务信息", exact: true })
.click();
await clickXmindAction(page, "甘特图");
const gantt = page.getByRole("dialog", { name: "甘特图", exact: true });
const promise = page.waitForEvent("download");
await page
.getByRole("button", { name: "导出甘特图 CSV", exact: true })
.click();
const download = await promise;
expect(download.suggestedFilename()).toContain("甘特图.csv");
const stream = await download.createReadStream();
const chunks: Buffer[] = [];
for await (const c of stream!) chunks.push(c);
expect(Buffer.concat(chunks).toString()).toContain(
'"设计","2026-10-05","2026-10-06","2"'
);
await gantt.getByRole("button", { name: "设计", exact: true }).click();
await editor.getByRole("button", { name: "移除任务", exact: true }).click();
await expect(gantt.locator(".xmind-gantt-bar")).toHaveCount(1);
await expect(gantt.locator("[data-gantt-dependency]")).toHaveCount(0);
await gantt
.getByRole("button", { name: "任务时间条 开发", exact: true })
.press("ControlOrMeta+z");
await expect(gantt.locator(".xmind-gantt-bar")).toHaveCount(2);
await expect(gantt.locator("[data-gantt-dependency]")).toHaveCount(1);
await page.getByRole("button", { name: "关闭甘特图", exact: true }).click();
await clickXmindSheet(page, "其他工作表");
await clickXmindAction(page, "甘特图");
await expect(gantt.getByText("没有任务", { exact: true })).toBeVisible();
await page.getByRole("button", { name: "关闭甘特图", exact: true }).click();
await clickXmindSheet(page, "项目计划");
await clickXmindAction(page, "甘特图");
await expect(gantt.locator(".xmind-gantt-bar")).toHaveCount(2);
});