Files
FreeCADWasmDev/tools/part-box-selection-browser-cases.mjs

146 lines
11 KiB
JavaScript

import assert from "node:assert/strict";
import fs from "node:fs";
import path from "node:path";
import { playwright } from "./part-primitive-business-cases.mjs";
export async function runBoxSelectionBrowserCases(root) {
const directory = path.join(root, "docs/current/part-box-selection-browser", new Date().toISOString().replace(/[:.]/g, "-"));
fs.mkdirSync(directory, { recursive: true });
const browser = await playwright().chromium.launch({ executablePath: process.env.FREECAD_CHROME ?? "/usr/bin/google-chrome",
headless: true, args: ["--no-sandbox", "--disable-dev-shm-usage"] });
const files = [], cases = [];
try {
const context = await browser.newContext({ viewport: { width: 1440, height: 900 }, deviceScaleFactor: 1 });
const page = await context.newPage();
const errors = []; page.on("pageerror", error => errors.push(String(error)));
const document = "BoxSelectionBrowser";
const origin = process.env.FREECAD_BROWSER_ORIGIN ?? "http://127.0.0.1:5180";
await page.goto(`${origin}/workbench.html?module=Part&command=part.document&document=${document}`);
const instrument = async restore => page.evaluate(async ({ restore, document }) => {
const moduleUrl = performance.getEntriesByType("resource").find(entry => new URL(entry.name).pathname === "/src/module-router.ts")?.name;
if (!moduleUrl) throw new Error("Mounted router module was not loaded");
const { WebModuleRouter } = await import(moduleUrl);
const request = WebModuleRouter.prototype.request;
window.__boxRequests = [];
WebModuleRouter.prototype.request = async function(module, command, payload, id) {
if (restore && command === "part.document" && payload.action === "open") {
const { readDocumentFile } = await import("/src/storage.ts");
payload = { ...payload, bytes: await readDocumentFile("BoxSelectionBrowser.FCStd") };
}
const response = await request.call(this, module, command, payload, id);
if (id === document || payload.document === document) window.__boxRequests.push({ command, payload: { ...payload, bytes: undefined }, response });
return response;
};
}, { restore, document });
await instrument(false);
const invoke = async args => {
await page.locator("#payload-editor").fill(JSON.stringify({ schema: "freecad-web-part/document/v1", module: "Part", document, ...args }));
await page.evaluate(() => { if (window.__freecadWorkbenchState) window.__freecadWorkbenchState.response = null; });
await page.getByRole("button", { name: "执行选中命令", exact: true }).click();
await page.waitForFunction(() => !!window.__freecadWorkbenchState?.response && !document.querySelector("button.run")?.disabled);
const response = await page.evaluate(() => window.__freecadWorkbenchState.response);
assert.equal(response.kind, "result", JSON.stringify(response)); return response;
};
for (const args of [{ action: "create" }, { action: "setBox", object: "Left", dimensions: [2, 2, 2] },
{ action: "setBox", object: "Right", dimensions: [2, 2, 2] }, { action: "setPosition", object: "Right", position: [6, 0, 0] }]) await invoke(args);
const pickCommand = () => page.locator(".matrix-list button").filter({ has: page.locator("span", { hasText: /^Part_BoxSelection$/ }) }).click();
await pickCommand();
const task = page.getByRole("region", { name: "Part face box selection", exact: true });
await task.getByRole("button", { name: "加载文档", exact: true }).click();
const viewport = task.getByLabel("Native selection viewport", { exact: true });
await viewport.locator("canvas").waitFor();
const drag = async (region, expectedObjects) => {
await viewport.scrollIntoViewIfNeeded();
const rect = await viewport.boundingBox();
const before = await page.evaluate(() => window.__boxRequests.filter(item => item.payload.command === "Std_BoxElementSelection").length);
await page.mouse.move(rect.x + rect.width * region[0], rect.y + rect.height * region[1]); await page.mouse.down();
await page.mouse.move(rect.x + rect.width * region[2], rect.y + rect.height * region[3], { steps: 6 }); await page.mouse.up();
try {
await page.waitForFunction(count => window.__boxRequests.filter(item => item.payload.command === "Std_BoxElementSelection").length > count, before, { timeout: 10000 });
} catch (error) {
const diagnosis = await page.evaluate(() => ({ requests: window.__boxRequests, state: window.__freecadWorkbenchState,
alerts: [...document.querySelectorAll('[role="alert"]')].map(e => e.textContent), output: document.querySelector('[aria-label="Native selected faces"]')?.textContent }));
fs.writeFileSync(path.join(directory, "drag-diagnostic.json"), JSON.stringify(diagnosis, null, 2));
await page.screenshot({ path: path.join(directory, "drag-failure.png"), fullPage: true });
throw error;
}
const last = await page.evaluate(() => window.__boxRequests.filter(item => item.payload.command === "Std_BoxElementSelection").at(-1));
assert.equal(last.response.kind, "result", JSON.stringify(last));
assert.deepEqual(last.response.result.selection.map(item => item.object).sort(), expectedObjects.slice().sort());
assert.equal(last.payload.owner, "DocumentProbe"); assert.equal(last.payload.parameters.elementType, "Face");
const actual = last.response.result.selection;
const countText = actual.map(item => `${item.object}: ${item.subelements.join(", ")}`).join("; ");
await assertEventuallyOutput(countText);
cases.push({ input: { drag: region, request: last.payload }, actual,
assertions: [{ name: "Real pointer drag passes the rendered camera to the native face selector and updates mounted selection", passed: true }] });
return actual;
};
const assertEventuallyOutput = async text => page.waitForFunction(text => {
const output = document.querySelector('[aria-label="Part face box selection"] [aria-label="Native selected faces"]');
return output?.textContent === text;
}, text);
const left = await drag([0.02, 0.02, 0.5, 0.98], ["Left"]); assert.equal(left[0].subelements.length, 6);
await task.getByLabel("Selection mode", { exact: true }).selectOption("add");
await drag([0.5, 0.02, 0.98, 0.98], ["Left", "Right"]);
await task.getByLabel("Selection mode", { exact: true }).selectOption("remove");
await drag([0.02, 0.02, 0.5, 0.98], ["Right"]);
await task.getByLabel("Selection mode", { exact: true }).selectOption("replace");
await task.getByLabel("Fully contained", { exact: true }).check();
await drag([0.98, 0.98, 0.02, 0.02], ["Left", "Right"]);
await task.getByLabel("Fully contained", { exact: true }).uncheck();
const beforeCancel = await page.evaluate(() => window.__boxRequests.length);
const cancelRect = await viewport.boundingBox();
await page.mouse.move(cancelRect.x + 10, cancelRect.y + 10); await page.mouse.down();
await page.mouse.move(cancelRect.x + 100, cancelRect.y + 100); await page.keyboard.press("Escape"); await page.mouse.up();
assert.equal(await page.evaluate(() => window.__boxRequests.length), beforeCancel);
cases.push({ input: { operation: "escape-during-drag" }, actual: { nativeCalls: 0 }, assertions: [{ name: "Cancelling pointer selection preserves native selection without a kernel request", passed: true }] });
await drag([0.02, 0.02, 0.5, 0.98], ["Left"]);
await task.getByRole("button", { name: "隐藏所选对象", exact: true }).click();
await page.waitForFunction(() => window.__boxRequests.at(-1)?.payload.command === "Std_HideSelection");
await drag([0.02, 0.02, 0.98, 0.98], ["Right"]);
await task.getByRole("button", { name: "显示所有对象", exact: true }).click();
await page.waitForFunction(() => window.__boxRequests.at(-1)?.payload.command === "Std_ShowObjects");
await drag([0.02, 0.02, 0.98, 0.98], ["Left", "Right"]);
await page.setViewportSize({ width: 390, height: 844 });
await drag([0.02, 0.02, 0.5, 0.98], ["Left"]);
const capture = async name => {
const file = path.join(directory, name); const png = await viewport.locator("canvas").screenshot({ path: file }); files.push(file);
const pixels = await page.evaluate(async base64 => {
const image = new Image(); image.src = `data:image/png;base64,${base64}`; await image.decode();
const canvas = document.createElement("canvas"); canvas.width = image.width; canvas.height = image.height;
const ctx = canvas.getContext("2d"); ctx.drawImage(image, 0, 0); const values = ctx.getImageData(0, 0, canvas.width, canvas.height).data;
const colors = new Set(); for (let i = 0; i < values.length; i += 4) colors.add(`${values[i]},${values[i+1]},${values[i+2]}`);
return { width: canvas.width, height: canvas.height, colors: colors.size };
}, png.toString("base64"));
assert.ok(pixels.width > 100 && pixels.height > 100 && pixels.colors > 8, JSON.stringify(pixels)); return pixels;
};
const mobile = await capture("mobile-geometry.png");
const mobileFile = path.join(directory, "mobile.png"); await page.screenshot({ path: mobileFile, fullPage: true }); files.push(mobileFile);
const layout = await task.evaluate(element => ({ width: element.clientWidth, scroll: element.scrollWidth }));
assert.ok(layout.scroll <= layout.width + 1, JSON.stringify(layout));
await page.setViewportSize({ width: 1440, height: 900 }); await drag([0.02, 0.02, 0.98, 0.98], ["Left", "Right"]);
const desktop = await capture("desktop-geometry.png");
const desktopFile = path.join(directory, "desktop.png"); await page.screenshot({ path: desktopFile, fullPage: true }); files.push(desktopFile);
await page.locator("aside:nth-child(2) > button.command").filter({ has: page.locator("span", { hasText: /^part.document$/ }) }).click();
await invoke({ action: "save", path: "/tmp/BoxSelectionBrowser.FCStd" });
const saved = await page.evaluate(async () => {
const { writeDocumentFile } = await import("/src/storage.ts");
const data = window.__freecadWorkbenchState.response.result.bytes;
await writeDocumentFile("BoxSelectionBrowser.FCStd", data); return data.byteLength;
}); assert.ok(saved > 0);
await page.reload(); await instrument(true);
await page.locator("aside:nth-child(2) > button.command").filter({ has: page.locator("span", { hasText: /^part.document$/ }) }).click();
await invoke({ action: "open", path: "/tmp/BoxSelectionBrowser.FCStd" });
await pickCommand(); await task.getByRole("button", { name: "加载文档", exact: true }).click();
await assertEventuallyOutput("Left: Face1, Face2, Face3, Face4, Face5, Face6; Right: Face1, Face2, Face3, Face4, Face5, Face6");
await drag([0.02, 0.02, 0.5, 0.98], ["Left"]);
assert.deepEqual(errors, []);
cases.push({ input: { actions: ["resize", "save-OPFS", "reload", "open-FCStd", "select-again"] }, actual: { mobile, desktop, layout, fileBytes: saved },
assertions: [{ name: "Selection geometry is nonblank and correctly projected after resizing; native selection persists through FCStd/OPFS and reload", passed: true }] });
fs.writeFileSync(path.join(directory, "result.json"), JSON.stringify({ status: "passed", cases }, null, 2));
} catch (error) {
fs.writeFileSync(path.join(directory, "failure.json"), JSON.stringify({ error: String(error.stack ?? error), cases }, null, 2)); throw error;
} finally { await browser.close(); }
return { cases, files };
}