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.
96 lines
4.8 KiB
TypeScript
96 lines
4.8 KiB
TypeScript
import { expect, test, type Page } from "@playwright/test";
|
|
import JSZip from "jszip";
|
|
import { clickXmindAction } from "./editor-actions";
|
|
|
|
async function openMap(page: Page) {
|
|
await page.goto("/dev/flow-board-preview", { waitUntil: "commit" });
|
|
const zip = new JSZip();
|
|
zip.file("content.json", JSON.stringify([{ title: "框选验证", rootTopic: {
|
|
id: "root", title: "项目", children: { attached: [
|
|
{ id: "first", title: "产品" }, { id: "second", title: "设计" },
|
|
{ id: "third", title: "开发" }, { id: "fourth", title: "上线" },
|
|
] },
|
|
} }]));
|
|
await page.locator("input[accept*=xmind]").setInputFiles({
|
|
name: "selection.xmind", mimeType: "application/octet-stream",
|
|
buffer: await zip.generateAsync({ type: "nodebuffer" }),
|
|
});
|
|
await expect(page.locator(".drawnix").getByText("产品", { exact: true })).toBeVisible();
|
|
await page.evaluate(() => document.fonts.ready);
|
|
}
|
|
|
|
async function marqueeBranches(page: Page) {
|
|
const canvas = page.locator(".drawnix");
|
|
const first = await canvas.getByText("产品", { exact: true }).boundingBox();
|
|
const second = await canvas.getByText("设计", { exact: true }).boundingBox();
|
|
if (!first || !second) throw new Error("Missing branch geometry");
|
|
const start = { x: Math.min(first.x, second.x) - 35, y: Math.min(first.y, second.y) - 25 };
|
|
const end = { x: Math.max(first.x + first.width, second.x + second.width) + 35, y: Math.max(first.y + first.height, second.y + second.height) + 25 };
|
|
await page.mouse.move(start.x, start.y);
|
|
await page.mouse.down();
|
|
await page.mouse.move(end.x, end.y, { steps: 20 });
|
|
const overlay = canvas.locator(".board-active-svg");
|
|
await expect(overlay).toHaveCSS("position", "absolute");
|
|
await expect(overlay).toHaveCSS("pointer-events", "none");
|
|
const frame = overlay.locator('.active-host-g > g > path[stroke="#6698FF"]');
|
|
await expect(frame).toHaveCount(1);
|
|
await expect(frame).toHaveCSS("stroke", "rgb(56, 124, 255)");
|
|
await expect(frame).toHaveCSS("stroke-width", "2px");
|
|
await expect.poll(async () => {
|
|
const box = await frame.boundingBox();
|
|
if (!box) return Number.POSITIVE_INFINITY;
|
|
// SVG paint bounds may include the selection outline's padding.
|
|
return Math.max(Math.abs(box.x - start.x), Math.abs(box.y - start.y),
|
|
Math.abs(box.x + box.width - end.x), Math.abs(box.y + box.height - end.y));
|
|
}).toBeLessThanOrEqual(5);
|
|
const fill = overlay.locator('.active-host-g > g > path[fill="#6698FF25"]');
|
|
await expect(fill).toHaveCSS("fill", "rgba(56, 124, 255, 0.12)");
|
|
await page.mouse.up();
|
|
await expect(page.locator(".xmind-topic-count")).toHaveText("主题: 2 / 5");
|
|
await expect(fill).toHaveCount(0);
|
|
await expect(overlay.locator('.selection-rectangle')).not.toHaveCount(0);
|
|
}
|
|
|
|
test("blank-canvas marquee selects two branches and batch deletion can be undone", async ({ page }) => {
|
|
await openMap(page);
|
|
await expect(page.getByRole("button", { name: "选择工具", exact: true })).toHaveAttribute("aria-pressed", "true");
|
|
await marqueeBranches(page);
|
|
await page.keyboard.press("Backspace");
|
|
const canvas = page.locator(".drawnix");
|
|
await expect(canvas.getByText("产品", { exact: true })).toHaveCount(0);
|
|
await expect(canvas.getByText("设计", { exact: true })).toHaveCount(0);
|
|
await expect(canvas.getByText("开发", { exact: true })).toBeVisible();
|
|
await clickXmindAction(page, "撤销");
|
|
await expect(canvas.getByText("产品", { exact: true })).toBeVisible();
|
|
await expect(canvas.getByText("设计", { exact: true })).toBeVisible();
|
|
});
|
|
|
|
test("panning and selection shortcuts stay in sync and do not consume text input", async ({ page }) => {
|
|
await openMap(page);
|
|
const select = page.getByRole("button", { name: "选择工具", exact: true });
|
|
const pan = page.getByRole("button", { name: "拖动画布", exact: true });
|
|
await page.keyboard.press("h");
|
|
await expect(pan).toHaveAttribute("aria-pressed", "true");
|
|
const topic = page.locator(".drawnix").getByText("产品", { exact: true });
|
|
const before = await topic.boundingBox();
|
|
await page.mouse.move(160, 200);
|
|
await page.mouse.down();
|
|
await page.mouse.move(210, 240, { steps: 12 });
|
|
await page.mouse.up();
|
|
await expect.poll(async () => (await topic.boundingBox())?.x).not.toBe(before?.x);
|
|
await page.keyboard.press("v");
|
|
await expect(select).toHaveAttribute("aria-pressed", "true");
|
|
await marqueeBranches(page);
|
|
await pan.click();
|
|
await expect(pan).toHaveAttribute("aria-pressed", "true");
|
|
await select.click();
|
|
await marqueeBranches(page);
|
|
const title = page.getByLabel("白板标题", { exact: true });
|
|
await title.fill("");
|
|
await title.pressSequentially("hv");
|
|
await expect(title).toHaveValue("hv");
|
|
await expect(select).toHaveAttribute("aria-pressed", "true");
|
|
await page.getByRole("button", { name: "新增画布", exact: true }).click();
|
|
await expect(select).toHaveAttribute("aria-pressed", "true");
|
|
});
|