Add native-style menus, search and presentation controls, typography resources, and Xmind import/export styling support. Bundle canvas styles for preview and production, show marquee borders and selection fills, and align editor density with the project UI. Include interaction regression coverage and client comparison notes.
196 lines
8.1 KiB
TypeScript
196 lines
8.1 KiB
TypeScript
import { clickXmindAction } from "./editor-actions";
|
|
import { expect, test } from "@playwright/test";
|
|
import JSZip from "jszip";
|
|
|
|
const pixel =
|
|
"iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAAAO0lEQVR4AezXwQkAMAgDwND9d27rBD4FOdB3wv1yktzJrwI/f+4UIECAAAECBAgQIECAAAEC+wW63f0AAAD//+3aM8YAAAAGSURBVAMABkQgIT13JVIAAAAASUVORK5CYII=";
|
|
|
|
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 expect(page.getByLabel("主题备注")).toHaveValue(
|
|
"第一行说明\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 expect(page.getByLabel("主题备注")).toHaveValue(
|
|
"更新后的多行备注\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 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 expect(page.getByLabel("主题备注")).toHaveValue(
|
|
"更新后的多行备注\n保留换行"
|
|
);
|
|
await canvas.getByText("链接入口", { exact: true }).click();
|
|
await page.getByRole("button", { name: "定位目标主题", exact: true }).click();
|
|
await expect(canvas.getByText("目标主题", { exact: true })).toBeVisible();
|
|
await expect(page.getByLabel("主题备注")).toHaveValue("目标的备注");
|
|
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 expect(page.getByLabel("主题备注")).toHaveValue("旧版备注");
|
|
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();
|
|
});
|