ai-chatbot/tests/flow-board/xmind-import.spec.ts
134 79253245f3 fix(flow-board): align Xmind editor UI and restore marquee feedback
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.
2026-10-04 13:32:54 +08:00

75 lines
5.9 KiB
TypeScript

import { openXmindStructures } from "./editor-actions";
import { expect, type Page, test } from "@playwright/test";
import JSZip from "jszip";
async function upload(page: Page, files: Record<string, string>, name = "测试导图.xmind") {
const zip = new JSZip();
for (const [path, content] of Object.entries(files)) zip.file(path, content);
await page.locator('input[type=file][accept*=xmind]').setInputFiles({
name, mimeType: "application/octet-stream", buffer: await zip.generateAsync({ type: "nodebuffer" }),
});
}
test("modern XMind preserves sheets, free topics, summaries, boundaries, relationships and style", async ({ page }) => {
const errors: string[] = [];
page.on("pageerror", (error) => errors.push(error.message));
await page.goto("/dev/flow-board-preview", { waitUntil: "commit" });
const root = { id: "root", title: "新品项目", structureClass: "org.xmind.ui.logic.right", children: {
attached: [{ id: "research", title: "市场调研", style: { properties: { "svg:fill": "#fff4cc", "fo:font-size": 22, "fo:font-weight": "bold" } }, children: { attached: [{ id: "a", title: "用户访谈" }, { id: "b", title: "竞品分析" }] } }, { id: "design", title: "产品设计" }],
summary: [{ id: "summary", title: "启动阶段" }],
detached: [{ id: "free", title: "待讨论", position: { x: 450, y: 300 } }],
}, summaries: [{ range: "(0,1)", topicId: "summary" }], boundaries: [{ range: "(0,1)" }] };
await upload(page, { "content.json": JSON.stringify([
{ title: "产品计划", rootTopic: root, relationships: [{ end1Id: "research", end2Id: "design", title: "输入设计" }] },
{ title: "上线清单", rootTopic: { id: "root2", title: "正式上线", structureClass: "org.xmind.ui.org-chart.down", children: { attached: [{ id: "qa", title: "质量验收" }] } } },
]) });
const canvas = page.locator(".drawnix");
for (const label of ["新品项目", "用户访谈", "竞品分析", "启动阶段", "待讨论", "输入设计"]) await expect(canvas.getByText(label, { exact: true })).toBeVisible();
await expect(page.locator("[plait-geometry=true]")).toHaveCount(1);
await expect(page.locator("[plait-arrow-line=true]")).toHaveCount(1);
await expect(canvas.getByText("市场调研", { exact: true })).toHaveCSS("font-size", "22px");
await canvas.getByText("市场调研", { exact: true }).click();
await expect(page.getByLabel("节点字号")).toHaveValue("22");
await openXmindStructures(page);
await expect(page.getByRole("button", { name: "向右逻辑", exact: true })).toHaveAttribute("aria-pressed", "true");
await page.getByRole("button", { name: "上线清单", exact: true }).click();
await expect(canvas.getByText("质量验收", { exact: true })).toBeVisible();
await expect(canvas.getByText("新品项目", { exact: true })).toHaveCount(0);
await page.getByRole("button", { name: "产品计划", exact: true }).click();
await expect(canvas.getByText("输入设计", { exact: true })).toBeVisible();
await canvas.getByText("产品设计", { exact: true }).dblclick();
await page.keyboard.press("ControlOrMeta+A");
await page.keyboard.insertText("设计已更新");
await page.locator('.flow-board-canvas-shell').click({ position: { x: 100, y: 100 } });
await expect(canvas.getByText("设计已更新", { exact: true })).toBeVisible();
expect(errors).toEqual([]);
});
test("legacy XML XMind retains separate sheets and folded branches", async ({ page }) => {
await page.goto("/dev/flow-board-preview", { waitUntil: "commit" });
await upload(page, { "content.xml": `<?xml version="1.0"?><xmap-content xmlns="urn:xmind:xmap:xmlns:content:2.0">
<sheet id="s1"><title>旧版计划</title><topic id="r1" structure-class="org.xmind.ui.logic.right"><title>旧版中心</title><children><topics type="attached"><topic id="c1" branch="folded" style-id="yellow"><title>折叠分支</title><children><topics type="attached"><topic id="c2"><title>内部任务</title></topic></topics></children></topic></topics></children></topic></sheet>
<sheet id="s2"><title>旧版归档</title><topic id="r2"><title>归档中心</title></topic></sheet></xmap-content>`,
"styles.xml": `<xmap-styles xmlns="urn:xmind:xmap:xmlns:style:2.0" xmlns:svg="http://www.w3.org/2000/svg"><styles><style id="yellow"><topic-properties svg:fill="#fff4cc" /></style></styles></xmap-styles>`,
});
const canvas = page.locator(".drawnix");
await expect(canvas.getByText("折叠分支", { exact: true })).toBeVisible();
await expect(canvas.getByText("内部任务", { exact: true })).toHaveCount(0);
await canvas.getByText("折叠分支", { exact: true }).click();
await page.keyboard.press("ControlOrMeta+/");
await expect(canvas.getByText("内部任务", { exact: true })).toBeVisible();
await page.getByRole("button", { name: "旧版归档", exact: true }).click();
await expect(canvas.getByText("归档中心", { exact: true })).toBeVisible();
});
test("invalid import preserves the existing workbook and gives a readable error", async ({ page }) => {
await page.goto("/dev/flow-board-preview", { waitUntil: "commit" });
await upload(page, { "content.json": JSON.stringify([{ title: "保留画布", rootTopic: { id: "root", title: "已有内容" } }]) });
await expect(page.locator(".drawnix").getByText("已有内容", { exact: true })).toBeVisible();
await upload(page, { "content.json": "broken json" }, "损坏文件.xmind");
await expect(page.getByText("XMind 内容解析失败,请在 XMind 中重新保存后再导入", { exact: true })).toBeVisible();
await expect(page.locator(".drawnix").getByText("已有内容", { exact: true })).toBeVisible();
await page.locator('input[type=file][accept*=xmind]').setInputFiles({ name: "伪装文件.xmind", mimeType: "application/octet-stream", buffer: Buffer.from("not a zip") });
await expect(page.getByText("无法打开 XMind 文件,请确认文件完整且未加密", { exact: true })).toBeVisible();
});