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"); });