Files
FreeCADWasmDev/tools/part-section-cut-empty-sources-cases.mjs

119 lines
9.1 KiB
JavaScript

import assert from "node:assert/strict";
import fs from "node:fs";
import path from "node:path";
import { createHash } from "node:crypto";
import { playwright } from "./part-primitive-business-cases.mjs";
export async function runSectionCutEmptySourcesCases(ids) {
const capabilityId = "Part:desktop:Part_SectionCut";
if (!ids.includes(capabilityId)) return [];
const directory = path.resolve("docs/current/part-section-cut-empty-sources", new Date().toISOString().replace(/[:.]/g, "-"));
fs.mkdirSync(directory, { recursive: true });
const result = { capabilityId, status: "failed", cases: [], evidenceFiles: {},
deferredCases: ["runLegacySectionCutCases"],
remaining: ["Intersecting-object BooleanFragments mode", "Automatic cut-face coloring and color controls", "Cut and fragment transparency"] };
const record = (input, actual, name) => result.cases.push({ input, actual, assertions: [{ name, passed: true }] });
const session = state => state.objects.find(item => item.sectionCutSession)?.sectionCutSession;
const objects = state => Object.fromEntries(state.objects.map(item => [item.name, item]));
const signature = state => ({ objects: objects(state), undoCount: state.undoCount, redoCount: state.redoCount });
let browser;
try {
for (const file of ["tools/part-section-cut-empty-sources-cases.mjs", "tools/part-section-cut-business-cases.mjs",
"web/src/part-section-cut-task.tsx", "web/src/part-desktop-adapter.ts", "web/src/part-document-facade.ts",
"web/src/workbench-shell.tsx", "freecad-source/src/WebAssembly/FreeCADCoreProbe/PartSectionCutDocument.h",
...["js", "wasm", "worker.js"].map(ext => `web/public/kernel/FreeCADWebDocumentProbe.${ext}`)])
result.evidenceFiles[file] = createHash("sha256").update(fs.readFileSync(file)).digest("hex");
browser = await playwright().chromium.launch({ executablePath: process.env.FREECAD_CHROME ?? "/usr/bin/google-chrome",
headless: true, args: ["--no-sandbox", "--disable-dev-shm-usage"] });
const page = await browser.newPage({ viewport: { width: 1440, height: 900 } });
const errors = []; page.on("pageerror", error => errors.push(String(error)));
const documentId = "SectionCutEmptySources";
await page.goto(`${process.env.FREECAD_BROWSER_ORIGIN ?? "http://127.0.0.1:5180"}/workbench.html?module=Part&command=part.document&document=${documentId}`);
const run = async args => {
await page.locator("aside > button.command").filter({ has: page.locator("span", { hasText: /^part.document$/ }) }).click();
await page.locator("#payload-editor").fill(JSON.stringify({ schema: "freecad-web-part/document/v1", module: "Part",
document: documentId, builderInfo: true, includeMesh: true, ...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 reply = await page.evaluate(() => window.__freecadWorkbenchState.response);
assert.equal(reply.kind, "result", JSON.stringify(reply)); return reply.result;
};
const snapshot = () => run({ action: "snapshot" });
await run({ action: "create" });
const other = await run({ action: "create", document: "SectionCutEmptyOther" });
await snapshot();
await page.locator(".matrix-list button").filter({ has: page.locator("span", { hasText: /^Part_SectionCut$/ }) }).click();
const panel = page.getByRole("region", { name: "Section cutting", exact: true });
const ready = () => page.waitForFunction(() => {
const fieldset = document.querySelector('section[aria-label="Section cutting"] fieldset');
return fieldset && !fieldset.disabled;
});
const click = async name => { await panel.getByRole("button", { name, exact: true }).click(); await ready(); };
const apply = panel.getByRole("button", { name: "Apply section cut", exact: true });
await ready(); assert.equal(await apply.isDisabled(), true);
assert.equal(await panel.getByRole("button", { name: "Refresh cut sources", exact: true }).isDisabled(), true);
await run({ action: "setBox", object: "Box", dimensions: [10, 10, 10] });
await click("Refresh section cut"); assert.equal(await apply.isEnabled(), true);
await panel.getByRole("checkbox", { name: "Cut X", exact: true }).check();
await panel.getByRole("spinbutton", { name: "Cut X position", exact: true }).fill("5");
await click("Apply section cut");
const cut = await snapshot();
assert.deepEqual(session(cut).sources, ["Box"]);
assert.ok(Math.abs(cut.objects.find(item => item.name === session(cut).result).volume - 500) < 1e-6);
record({ document: documentId, initialObjects: [], dimensions: [10, 10, 10], planeX: 5 }, { volume: 500 },
"An empty document disables Apply; refreshing a visible native shape enables a real cut");
const empty = await run({ action: "remove", object: "Box" });
assert.deepEqual(session(empty).sources, []); assert.equal(empty.objects.length, 1);
await click("Refresh section cut"); assert.equal(await apply.isDisabled(), true);
assert.equal(await panel.getByRole("spinbutton", { name: "Cut X position", exact: true }).inputValue(), "5");
assert.deepEqual(signature(await snapshot()), signature(empty), "Reading the empty session must not change history");
await click("Section cut undo"); assert.equal(await apply.isEnabled(), true);
const restored = await snapshot(); assert.deepEqual(objects(restored), objects(cut));
await click("Section cut redo"); assert.equal(await apply.isDisabled(), true);
assert.deepEqual(objects(await snapshot()), objects(empty));
record({ operation: "remove last source, refresh, undo, redo" }, { sources: [], objectCount: 1, preservedPlaneX: 5 },
"The session-only document disables Apply and preserves planes; native history restores both geometry and button state");
await run({ action: "save", path: "/tmp/SectionCutEmptySources.FCStd" });
await run({ action: "close" });
const reopened = await run({ action: "open", path: "/tmp/SectionCutEmptySources.FCStd" });
assert.deepEqual(objects(reopened), objects(empty));
await click("Refresh section cut"); assert.equal(await apply.isDisabled(), true);
for (const [name, width, height] of [["desktop", 1440, 900], ["mobile", 390, 844]]) {
await page.setViewportSize({ width, height }); await panel.scrollIntoViewIfNeeded();
assert.equal(await apply.isDisabled(), true);
assert.equal(await panel.getByText("Add a visible shape, then refresh cut sources to continue.").isVisible(), true);
assert.equal(await page.evaluate(() => document.documentElement.scrollWidth <= innerWidth), true);
const file = path.join(directory, `${name}.png`); await page.screenshot({ path: file, fullPage: true });
result.evidenceFiles[path.relative(process.cwd(), file)] = createHash("sha256").update(fs.readFileSync(file)).digest("hex");
}
record({ operation: "save-close-open empty session", viewports: [1440, 390] }, { sources: session(reopened).sources },
"FCStd restores the empty native session; desktop and mobile show a disabled Apply and recovery instructions");
await run({ action: "setBox", object: "Replacement", dimensions: [8, 10, 10] });
await click("Refresh section cut"); assert.equal(await apply.isDisabled(), true, "New shapes must be collected into the existing session first");
await click("Refresh cut sources"); assert.equal(await apply.isEnabled(), true);
const collected = await snapshot();
assert.deepEqual(session(collected).sources, ["Replacement"]);
const replacementCut = collected.objects.find(item => item.name === session(collected).result);
assert.ok(Math.abs(replacementCut.volume - 500) < 1e-6); assert.equal(replacementCut.topology.valid, true);
assert.ok(replacementCut.mesh.triangleCount > 0);
assert.deepEqual(session(collected).axes, session(empty).axes);
await click("Section cut undo"); assert.equal(await apply.isDisabled(), true);
assert.deepEqual(session(await snapshot()).sources, []);
await click("Section cut redo"); assert.equal(await apply.isEnabled(), true);
assert.deepEqual(objects(await snapshot()), objects(collected));
assert.deepEqual(signature(await run({ action: "snapshot", document: "SectionCutEmptyOther" })), signature(other));
assert.equal(await panel.getByRole("status").count(), 0); assert.deepEqual(errors, []);
record({ operation: "add replacement, read, collect sources, undo, redo", dimensions: [8, 10, 10] },
{ sources: ["Replacement"], volume: replacementCut.volume, triangles: replacementCut.mesh.triangleCount },
"Source refresh recovers an empty session using preserved planes, reversible native geometry and current-document isolation");
result.status = "not_implemented";
} catch (error) { result.error = String(error.stack ?? error); process.stderr.write(result.error + "\n"); }
finally { await browser?.close(); }
fs.writeFileSync(path.join(directory, "result.json"), JSON.stringify(result, null, 2) + "\n");
return [result];
}