146 lines
11 KiB
JavaScript
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 };
|
|
}
|