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.
197 lines
8.4 KiB
TypeScript
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();
|
|
});
|