Add native task metadata, working-day calendar settings, dependency scheduling and rich note editing. Support complete-sheet PNG export with resolution and transparency controls, preserve XMind extensions and update browser regressions.
127 lines
6.7 KiB
TypeScript
127 lines
6.7 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();
|
|
await page.evaluate(() => document.fonts.ready);
|
|
}
|
|
|
|
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);
|
|
const chooserPending = page.waitForEvent("filechooser");
|
|
await clickXmindAction(page, "图片");
|
|
await (await chooserPending).setFiles(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 });
|
|
// Inspect before the rich-text editor's own bubbling handler.
|
|
// The canvas capture handler must leave clipboard ownership to the field.
|
|
let interceptedByCanvas = false;
|
|
element.addEventListener("paste", (paste) => { interceptedByCanvas = paste.defaultPrevented; }, {capture:true,once:true});
|
|
element.dispatchEvent(event);
|
|
return interceptedByCanvas;
|
|
}, pixel);
|
|
expect(prevented).toBe(false);
|
|
}
|
|
await expect(canvasImages(page)).toHaveCount(0);
|
|
});
|