ai-chatbot/tests/flow-board/topic-details.spec.ts
134 23ced0b760 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 17:38:14 +08:00

197 lines
8.4 KiB
TypeScript

import { clickXmindAction } from "./editor-actions";
import { expect, test } from "@playwright/test";
import JSZip from "jszip";
const pixel =
"iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAAAO0lEQVR4AezXwQkAMAgDwND9d27rBD4FOdB3wv1yktzJrwI/f+4UIECAAAECBAgQIECAAAEC+wW63f0AAAD//+3aM8YAAAAGSURBVAMABkQgIT13JVIAAAAASUVORK5CYII=";
async function expectNote(page: import("@playwright/test").Page, text: string) {
await expect.poll(() => page.getByLabel("主题备注").locator("p").allTextContents().then(lines => lines.join("\n"))).toBe(text);
}
test("topic content imports, edits, undoes, survives sheet changes and follows internal links", async ({
page,
}) => {
const errors: string[] = [];
page.on("pageerror", (error) => errors.push(error.message));
await page.goto("/dev/flow-board-preview", { waitUntil: "commit" });
const zip = new JSZip();
zip.file(
"content.json",
JSON.stringify([
{
title: "产品工作表",
rootTopic: {
id: "root",
title: "产品说明",
notes: { plain: { content: "第一行说明\n第二行说明" } },
labels: ["产品", "待审核"],
href: "https://example.com/docs",
image: { src: "xap:resources/pixel.png", width: 48 },
children: {
attached: [
{
id: "branch",
title: "折叠分支",
branch: "folded",
children: {
attached: [
{
id: "target",
title: "目标主题",
notes: { plain: { content: "目标的备注" } },
},
],
},
},
{ id: "link", title: "链接入口", href: "xmind:#target" },
{
id: "attachment",
title: "附加文件",
href: "xap:attachments/spec.txt",
},
],
},
},
},
{ title: "空白工作表", rootTopic: { id: "empty", title: "独立中心" } },
])
);
zip.file("resources/pixel.png", pixel, { base64: true });
zip.file("attachments/spec.txt", "产品需求附件");
await page.locator("input[accept*=xmind]").setInputFiles({
name: "主题内容.xmind",
mimeType: "application/octet-stream",
buffer: await zip.generateAsync({ type: "nodebuffer" }),
});
const canvas = page.locator(".drawnix");
await canvas.getByText("产品说明", { exact: true }).click();
await clickXmindAction(page, "备注");
await page.getByRole("button", {name:"主题详情",exact:true}).click();
await expectNote(page, "第一行说明\n第二行说明");
await expect(
page.getByRole("button", { name: "移除标签 产品", exact: true })
).toBeVisible();
await expect(page.getByLabel("主题外部链接")).toHaveValue(
"https://example.com/docs"
);
await expect(
canvas.locator('img[src^="data:image/png"], image[href^="data:image/png"]')
).toHaveCount(1);
await page.screenshot({
path: test.info().outputPath("topic-content-panel.png"),
});
await page.getByLabel("主题备注").fill("更新后的多行备注\n保留换行");
await page.getByLabel("新增主题标签").fill("完成");
await page.getByRole("button", { name: "添加", exact: true }).click();
await expect(
page.getByRole("button", { name: "移除标签 完成", exact: true })
).toBeVisible();
await clickXmindAction(page, "撤销");
await expect(
page.getByRole("button", { name: "移除标签 完成", exact: true })
).toHaveCount(0);
await expectNote(page, "更新后的多行备注\n保留换行");
await clickXmindAction(page, "重做");
await expect(
page.getByRole("button", { name: "移除标签 完成", exact: true })
).toBeVisible();
await page.getByLabel("主题外部链接").fill("javascript:alert(1)");
await page.getByLabel("主题外部链接").blur();
await expect(page.getByLabel("主题外部链接")).toHaveAttribute(
"aria-invalid",
"true"
);
await page.getByRole("button", { name: "关闭主题内容", exact: true }).click();
await clickXmindAction(page, "备注");
await page.getByRole("button", {name:"主题详情",exact:true}).click();
await expect(page.getByLabel("主题外部链接")).toHaveValue(
"https://example.com/docs"
);
await page.getByRole("button", { name: "空白工作表", exact: true }).click();
await page.getByRole("button", { name: "产品工作表", exact: true }).click();
await canvas.getByText("产品说明", { exact: true }).click();
await expectNote(page, "更新后的多行备注\n保留换行");
await canvas.getByText("链接入口", { exact: true }).click();
await page.getByRole("button", { name: "定位目标主题", exact: true }).click();
await expect(canvas.getByText("目标主题", { exact: true })).toBeVisible();
await expectNote(page, "目标的备注");
await canvas.getByText("附加文件", { exact: true }).click();
const downloadPromise = page.waitForEvent("download");
await page.getByRole("link", { name: "下载 spec.txt", exact: true }).click();
const download = await downloadPromise;
expect(download.suggestedFilename()).toBe("spec.txt");
const stream = await download.createReadStream();
const chunks = [];
for await (const chunk of stream!) chunks.push(chunk);
expect(Buffer.concat(chunks).toString()).toBe("产品需求附件");
await page
.getByLabel("上传主题附件")
.setInputFiles({
name: "新版需求.txt",
mimeType: "text/plain",
buffer: Buffer.from("替换后的附件"),
});
await expect(
page.getByRole("link", { name: "下载 新版需求.txt", exact: true })
).toHaveAttribute(
"href",
`data:application/octet-stream;base64,${Buffer.from("替换后的附件").toString("base64")}`
);
await clickXmindAction(page, "撤销");
await expect(
page.getByRole("link", { name: "下载 spec.txt", exact: true })
).toBeVisible();
await page.getByRole("button", { name: "移除附件", exact: true }).click();
await expect(
page.getByRole("link", { name: "下载 spec.txt", exact: true })
).toHaveCount(0);
await clickXmindAction(page, "撤销");
await expect(
page.getByRole("link", { name: "下载 spec.txt", exact: true })
).toBeVisible();
expect(errors).toEqual([]);
});
test("legacy XML imports plain notes, separate labels and embedded pictures", async ({
page,
}) => {
await page.goto("/dev/flow-board-preview", { waitUntil: "commit" });
const zip = new JSZip();
zip.file(
"content.xml",
`<?xml version="1.0"?><xmap-content xmlns="urn:xmind:xmap:xmlns:content:2.0" xmlns:xhtml="http://www.w3.org/1999/xhtml" xmlns:xlink="http://www.w3.org/1999/xlink"><sheet id="sheet"><title>旧版内容</title><topic id="root" xlink:href="https://example.com/legacy"><title>旧版资料</title><notes><plain>旧版备注</plain><html>不重复合并的富文本备注</html></notes><labels><label>计划</label><label>资料</label></labels><xhtml:img src="xap:resources/image.png" width="48"/><children><topics type="attached"><topic id="missing"><title>缺失图片仍保留主题</title><xhtml:img src="xap:resources/missing.png"/></topic></topics></children></topic></sheet></xmap-content>`
);
zip.file("resources/image.png", pixel, { base64: true });
await page
.locator("input[accept*=xmind]")
.setInputFiles({
name: "旧版内容.xmind",
mimeType: "application/octet-stream",
buffer: await zip.generateAsync({ type: "nodebuffer" }),
});
await expect(
page.getByText("部分图片缺失、损坏或格式暂不支持", { exact: true })
).toBeVisible();
const canvas = page.locator(".drawnix");
await canvas.getByText("旧版资料", { exact: true }).click();
await clickXmindAction(page, "备注");
await page.getByRole("button", {name:"主题详情",exact:true}).click();
await expectNote(page, "旧版备注");
await expect(
page.getByRole("button", { name: "移除标签 计划", exact: true })
).toBeVisible();
await expect(
page.getByRole("button", { name: "移除标签 资料", exact: true })
).toBeVisible();
await expect(page.getByLabel("主题外部链接")).toHaveValue(
"https://example.com/legacy"
);
await expect(
canvas.locator('img[src^="data:image/png"], image[href^="data:image/png"]')
).toHaveCount(1);
await expect(
canvas.getByText("缺失图片仍保留主题", { exact: true })
).toBeVisible();
});