Handle clipboard images while editing or selecting a topic and route Insert image through the same topic attachment behavior. Read image data locally in preview mode and retain the production upload path. Add four browser regression cases for real clipboard paste, undo/redo, export resources, canvas images, and text input isolation. Record validation in the client comparison notes.
121 lines
6.4 KiB
TypeScript
121 lines
6.4 KiB
TypeScript
import { expect, test, type Page } from "@playwright/test";
|
|
import JSZip from "jszip";
|
|
import { clickXmindAction } from "./editor-actions";
|
|
|
|
const pixel = "iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAAAO0lEQVR4AezXwQkAMAgDwND9d27rBD4FOdB3wv1yktzJrwI/f+4UIECAAAECBAgQIECAAAEC+wW63f0AAAD//+3aM8YAAAAGSURBVAMABkQgIT13JVIAAAAASUVORK5CYII=";
|
|
const picture = { name: "clipboard.png", mimeType: "image/png", buffer: Buffer.from(pixel, "base64") };
|
|
|
|
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: "开发计划" },
|
|
] },
|
|
} }]));
|
|
await page.locator("input[accept*=xmind]").setInputFiles({
|
|
name: "paste.xmind", mimeType: "application/octet-stream",
|
|
buffer: await zip.generateAsync({ type: "nodebuffer" }),
|
|
});
|
|
await expect(page.locator(".drawnix").getByText("设计方案", { exact: true })).toBeVisible();
|
|
}
|
|
|
|
async function writeClipboardImage(page: Page) {
|
|
await page.evaluate(async (base64) => {
|
|
const blob = await (await fetch(`data:image/png;base64,${base64}`)).blob();
|
|
await navigator.clipboard.write([new ClipboardItem({ "image/png": blob })]);
|
|
}, pixel);
|
|
}
|
|
|
|
const canvasImages = (page: Page) => page.locator('.drawnix img[src^="data:image/png"], .drawnix image[href^="data:image/png"]');
|
|
|
|
async function exportedSheets(page: Page) {
|
|
const pending = page.waitForEvent("download");
|
|
await page.getByRole("button", { name: "导出 XMind", exact: true }).click();
|
|
const download = await pending;
|
|
const stream = await download.createReadStream();
|
|
const chunks: Buffer[] = [];
|
|
for await (const chunk of stream!) chunks.push(chunk);
|
|
const zip = await JSZip.loadAsync(Buffer.concat(chunks));
|
|
return { zip, sheets: JSON.parse(await zip.file("content.json")!.async("string")) };
|
|
}
|
|
|
|
test("real clipboard image paste while editing attaches to the topic, survives export, and undoes", async ({ page, context }) => {
|
|
await context.grantPermissions(["clipboard-read", "clipboard-write"]);
|
|
const uploads: string[] = [];
|
|
page.on("request", request => { if (/\/api\/files\/upload/.test(request.url())) uploads.push(request.url()); });
|
|
const errors: string[] = [];
|
|
page.on("pageerror", error => errors.push(error.message));
|
|
await openMap(page);
|
|
await page.locator(".drawnix").getByText("设计方案", { exact: true }).dblclick();
|
|
await expect(page.locator('.drawnix [data-slate-editor="true"][contenteditable="true"]')).toBeFocused();
|
|
await writeClipboardImage(page);
|
|
await page.keyboard.press("ControlOrMeta+v");
|
|
await expect(canvasImages(page)).toHaveCount(1);
|
|
await expect(page.locator(".drawnix").getByText("设计方案", { exact: true })).toBeVisible();
|
|
await page.keyboard.press("Escape");
|
|
const { zip, sheets } = await exportedSheets(page);
|
|
const topic = sheets[0].rootTopic.children.attached.find((item: { title: string }) => item.title === "设计方案");
|
|
expect(topic.image.width).toBe(32);
|
|
expect(topic.image.height).toBe(32);
|
|
expect(topic.image.src).toMatch(/^xap:resources\//);
|
|
expect(await zip.file(topic.image.src.replace("xap:", ""))!.async("uint8array")).not.toHaveLength(0);
|
|
expect(sheets[0].rootTopic.children.attached.find((item: { title: string }) => item.title === "开发计划").image).toBeUndefined();
|
|
await clickXmindAction(page, "撤销");
|
|
await expect(canvasImages(page)).toHaveCount(0);
|
|
await clickXmindAction(page, "重做");
|
|
await expect(canvasImages(page)).toHaveCount(1);
|
|
await page.screenshot({ path: "test-results/flow-board-ui/image-paste-editing.png" });
|
|
expect(uploads).toEqual([]);
|
|
expect(errors).toEqual([]);
|
|
});
|
|
|
|
test("selected topic accepts clipboard and Insert image, replacing its single picture", async ({ page, context }) => {
|
|
await context.grantPermissions(["clipboard-read", "clipboard-write"]);
|
|
await openMap(page);
|
|
await page.locator(".drawnix").getByText("设计方案", { exact: true }).click();
|
|
await writeClipboardImage(page);
|
|
await page.keyboard.press("ControlOrMeta+v");
|
|
await expect(canvasImages(page)).toHaveCount(1);
|
|
await clickXmindAction(page, "图片");
|
|
await page.locator('input[data-flow-board-shell-control="true"][accept="image/jpeg,image/png,image/webp"]').setInputFiles(picture);
|
|
await expect(canvasImages(page)).toHaveCount(1);
|
|
const { sheets } = await exportedSheets(page);
|
|
expect(sheets[0].rootTopic.children.attached.find((item: { title: string }) => item.title === "设计方案").image).toBeDefined();
|
|
});
|
|
|
|
test("blank canvas still pastes an independent image and text editing still pastes text", async ({ page, context }) => {
|
|
await context.grantPermissions(["clipboard-read", "clipboard-write"]);
|
|
await openMap(page);
|
|
await page.mouse.click(120, 170);
|
|
await writeClipboardImage(page);
|
|
await page.keyboard.press("ControlOrMeta+v");
|
|
await expect(canvasImages(page)).toHaveCount(1);
|
|
await page.locator(".drawnix").getByText("设计方案", { exact: true }).dblclick();
|
|
await page.evaluate(() => navigator.clipboard.writeText("新的方案"));
|
|
await page.keyboard.press("ControlOrMeta+a");
|
|
await page.keyboard.press("ControlOrMeta+v");
|
|
await expect(page.locator(".drawnix").getByText("新的方案", { exact: true })).toBeVisible();
|
|
await page.keyboard.press("Escape");
|
|
const { sheets } = await exportedSheets(page);
|
|
expect(sheets[0].rootTopic.children.attached.every((topic: { image?: unknown }) => !topic.image)).toBe(true);
|
|
});
|
|
|
|
test("image paste in title and notes is not intercepted by the canvas", async ({ page }) => {
|
|
await openMap(page);
|
|
await page.locator(".drawnix").getByText("设计方案", { exact: true }).click();
|
|
await clickXmindAction(page, "备注");
|
|
for (const target of [page.getByLabel("白板标题", { exact: true }), page.getByLabel("主题备注", { exact: true })]) {
|
|
const prevented = await target.evaluate((element, base64) => {
|
|
const clipboardData = new DataTransfer();
|
|
const bytes = Uint8Array.from(atob(base64), char => char.charCodeAt(0));
|
|
clipboardData.items.add(new File([bytes], "image.png", { type: "image/png" }));
|
|
const event = new ClipboardEvent("paste", { clipboardData, bubbles: true, cancelable: true });
|
|
element.dispatchEvent(event);
|
|
return event.defaultPrevented;
|
|
}, pixel);
|
|
expect(prevented).toBe(false);
|
|
}
|
|
await expect(canvasImages(page)).toHaveCount(0);
|
|
});
|