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.
228 lines
8.2 KiB
TypeScript
228 lines
8.2 KiB
TypeScript
import { clickXmindAction, openXmindStructures } from "./editor-actions";
|
|
import { expect, type Page, test } from "@playwright/test";
|
|
import JSZip from "jszip";
|
|
|
|
async function upload(page: Page, rootTopic: object, name: string) {
|
|
const zip = new JSZip();
|
|
zip.file("content.json", JSON.stringify([{ title: name, rootTopic }]));
|
|
await page.locator("input[accept*=xmind]").setInputFiles({
|
|
name: `${name}.xmind`,
|
|
mimeType: "application/octet-stream",
|
|
buffer: await zip.generateAsync({ type: "nodebuffer" }),
|
|
});
|
|
}
|
|
|
|
test("side-title tree table renders a spanning left header and preserves its native structure", async ({
|
|
page,
|
|
}) => {
|
|
const errors: string[] = [];
|
|
page.on("pageerror", (error) => errors.push(error.message));
|
|
await page.goto("/dev/flow-board-preview", { waitUntil: "commit" });
|
|
await upload(
|
|
page,
|
|
{
|
|
id: "root",
|
|
title: "发布协调",
|
|
structureClass: "org.xmind.ui.treetable",
|
|
style: { properties: { "svg:fill": "#000000FF" } },
|
|
children: {
|
|
attached: [
|
|
{
|
|
id: "plan",
|
|
title: "规划",
|
|
style: { properties: { "svg:fill": "#F7C85FFF" } },
|
|
children: {
|
|
attached: [
|
|
{ id: "scope", title: "范围确认" },
|
|
{ id: "date", title: "发布日期" },
|
|
],
|
|
},
|
|
},
|
|
{ id: "ship", title: "交付" },
|
|
],
|
|
},
|
|
},
|
|
"侧标题树形表格"
|
|
);
|
|
|
|
const canvas = page.locator(".drawnix");
|
|
const table = canvas.locator('[data-special-table="treeTableSide"]');
|
|
await expect(table).toBeVisible();
|
|
await expect(table.locator("[data-special-table-cell]")).toHaveCount(5);
|
|
await expect(
|
|
table.locator('[data-special-table-cell][data-depth="0"]')
|
|
).toHaveAttribute("fill", "#000000");
|
|
await clickXmindAction(page, "适应画布");
|
|
const box = async (title: string) =>
|
|
(await canvas.getByText(title, { exact: true }).boundingBox())!;
|
|
const root = await box("发布协调");
|
|
const plan = await box("规划");
|
|
const scope = await box("范围确认");
|
|
const date = await box("发布日期");
|
|
expect(plan.x).toBeGreaterThan(root.x + root.width);
|
|
expect(scope.x).toBeGreaterThan(plan.x + plan.width);
|
|
expect(date.y).toBeGreaterThan(scope.y);
|
|
await canvas.getByText("交付", { exact: true }).click();
|
|
await page.getByRole("button", { name: "子主题", exact: true }).click();
|
|
await page.keyboard.insertText("上线复盘");
|
|
await page
|
|
.locator(".flow-board-canvas-shell")
|
|
.click({ position: { x: 50, y: 100 } });
|
|
await expect(canvas.getByText("上线复盘", { exact: true })).toBeVisible();
|
|
await expect(table.locator("[data-special-table-cell]")).toHaveCount(6);
|
|
await page.screenshot({
|
|
path: test.info().outputPath("side-tree-table-preview.png"),
|
|
animations: "disabled",
|
|
});
|
|
|
|
await canvas.getByText("发布协调", { exact: true }).click();
|
|
await openXmindStructures(page);
|
|
await expect(
|
|
page.getByRole("button", { name: "树形表格(侧标题)", exact: true })
|
|
).toHaveAttribute("aria-pressed", "true");
|
|
const downloadPromise = page.waitForEvent("download");
|
|
await page.getByRole("button", { name: "导出 XMind", exact: true }).click();
|
|
const stream = await (await downloadPromise).createReadStream();
|
|
const chunks: Buffer[] = [];
|
|
for await (const chunk of stream!) chunks.push(chunk);
|
|
const exported = await JSZip.loadAsync(Buffer.concat(chunks));
|
|
const content = JSON.parse(
|
|
await exported.file("content.json")!.async("string")
|
|
);
|
|
expect(content[0].rootTopic.structureClass).toBe(
|
|
"org.xmind.ui.treetable"
|
|
);
|
|
expect(errors).toEqual([]);
|
|
});
|
|
|
|
test("spreadsheet imports XMind columns, stacks labelled entries and round-trips the column model", async ({
|
|
page,
|
|
}) => {
|
|
const errors: string[] = [];
|
|
page.on("pageerror", (error) => errors.push(error.message));
|
|
await page.goto("/dev/flow-board-preview", { waitUntil: "commit" });
|
|
const columns = ["Monday", "Tuesday", "Friday"];
|
|
await upload(
|
|
page,
|
|
{
|
|
id: "root",
|
|
title: "Mood Tracker",
|
|
structureClass: "org.xmind.ui.spreadsheet",
|
|
style: { properties: { "svg:fill": "#1F2766FF" } },
|
|
extensions: [
|
|
{
|
|
provider: "org.xmind.ui.spreadsheet",
|
|
content: [
|
|
{
|
|
name: "columns",
|
|
content: columns.map((column) => ({
|
|
name: "column",
|
|
content: column,
|
|
})),
|
|
},
|
|
],
|
|
},
|
|
],
|
|
children: {
|
|
attached: [
|
|
{
|
|
id: "happy",
|
|
title: "Happy",
|
|
style: { properties: { "svg:fill": "#FF9D8EFF" } },
|
|
children: {
|
|
attached: [
|
|
{ id: "walk", title: "Morning walk", labels: ["Monday"] },
|
|
{ id: "coffee", title: "Coffee", labels: ["Monday"] },
|
|
{ id: "music", title: "Music", labels: ["Friday"] },
|
|
],
|
|
},
|
|
},
|
|
{
|
|
id: "calm",
|
|
title: "Calm",
|
|
children: {
|
|
attached: [
|
|
{ id: "read", title: "Read", labels: ["Tuesday"] },
|
|
],
|
|
},
|
|
},
|
|
],
|
|
},
|
|
},
|
|
"矩阵"
|
|
);
|
|
|
|
const canvas = page.locator(".drawnix");
|
|
const table = canvas.locator('[data-special-table="spreadsheet"]');
|
|
await expect(table).toBeVisible();
|
|
await expect(table.locator("[data-special-matrix-column]")).toHaveCount(3);
|
|
await expect(table.locator("[data-special-matrix-cell]")).toHaveCount(6);
|
|
await expect(table.locator("[data-special-matrix-entry]")).toHaveCount(4);
|
|
for (const column of columns)
|
|
await expect(
|
|
table.locator(`[data-special-matrix-column-label="${column}"]`)
|
|
).toHaveText(column);
|
|
await clickXmindAction(page, "适应画布");
|
|
const box = async (title: string) =>
|
|
(await canvas.getByText(title, { exact: true }).boundingBox())!;
|
|
const happy = await box("Happy");
|
|
const walk = await box("Morning walk");
|
|
const coffee = await box("Coffee");
|
|
const music = await box("Music");
|
|
const read = await box("Read");
|
|
expect(Math.abs(walk.x + walk.width / 2 - (coffee.x + coffee.width / 2))).toBeLessThan(1);
|
|
expect(coffee.y).toBeGreaterThan(walk.y);
|
|
expect(music.x).toBeGreaterThan(walk.x);
|
|
expect(read.x).toBeGreaterThan(walk.x);
|
|
expect(read.y).toBeGreaterThan(happy.y);
|
|
await canvas.getByText("Coffee", { exact: true }).dblclick();
|
|
await page.keyboard.press("ControlOrMeta+A");
|
|
await page.keyboard.insertText("Team coffee");
|
|
await page
|
|
.locator(".flow-board-canvas-shell")
|
|
.click({ position: { x: 50, y: 100 } });
|
|
await expect(canvas.getByText("Team coffee", { exact: true })).toBeVisible();
|
|
|
|
await canvas.getByText("Mood Tracker", { exact: true }).click();
|
|
await openXmindStructures(page);
|
|
await expect(
|
|
page.getByRole("complementary", { name: "格式面板" }).getByLabel("矩阵", { exact: true })
|
|
).toHaveAttribute("aria-pressed", "true");
|
|
const downloadPromise = page.waitForEvent("download");
|
|
await page.getByRole("button", { name: "导出 XMind", exact: true }).click();
|
|
const stream = await (await downloadPromise).createReadStream();
|
|
const chunks: Buffer[] = [];
|
|
for await (const chunk of stream!) chunks.push(chunk);
|
|
const buffer = Buffer.concat(chunks);
|
|
const exported = await JSZip.loadAsync(buffer);
|
|
const content = JSON.parse(
|
|
await exported.file("content.json")!.async("string")
|
|
);
|
|
expect(content[0].rootTopic.structureClass).toBe(
|
|
"org.xmind.ui.spreadsheet"
|
|
);
|
|
expect(
|
|
content[0].rootTopic.extensions[0].content[0].content.map(
|
|
(column: { content: string }) => column.content
|
|
)
|
|
).toEqual(columns);
|
|
expect(
|
|
content[0].rootTopic.children.attached[0].children.attached.find(
|
|
(topic: { title: string }) => topic.title === "Team coffee"
|
|
).labels
|
|
).toEqual(["Monday"]);
|
|
await page.locator("input[accept*=xmind]").setInputFiles({
|
|
name: "矩阵往返.xmind",
|
|
mimeType: "application/octet-stream",
|
|
buffer,
|
|
});
|
|
await expect(table).toBeVisible();
|
|
await expect(table.locator("[data-special-matrix-column]")).toHaveCount(3);
|
|
await clickXmindAction(page, "适应画布");
|
|
await page.screenshot({
|
|
path: test.info().outputPath("spreadsheet-preview.png"),
|
|
animations: "disabled",
|
|
});
|
|
expect(errors).toEqual([]);
|
|
});
|