528 lines
58 KiB
JavaScript
528 lines
58 KiB
JavaScript
#!/usr/bin/env node
|
|
|
|
import crypto from "node:crypto";
|
|
import fs from "node:fs";
|
|
import http from "node:http";
|
|
import os from "node:os";
|
|
import path from "node:path";
|
|
import { fileURLToPath } from "node:url";
|
|
import { spawn } from "node:child_process";
|
|
|
|
const toolsRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)));
|
|
const projectRoot = path.resolve(toolsRoot, "..");
|
|
const docsRoot = path.join(projectRoot, "docs");
|
|
const distRoot = path.join(projectRoot, "web", "dist");
|
|
const scenario = process.env.P6_CHROME_SCENARIO ?? "baseline";
|
|
const preloadModules = (process.env.P6_CHROME_PRELOAD_MODULES ?? "").split(",").map((value) => value.trim()).filter(Boolean);
|
|
const preloadManifest = process.env.P6_CHROME_MODULE_MANIFEST ?? "/modules/manifest.json";
|
|
const preloadConcurrency = Math.max(1, Math.min(2, Number(process.env.P6_CHROME_PRELOAD_CONCURRENCY ?? 2)));
|
|
const preloadKinds = (process.env.P6_CHROME_PRELOAD_ASSETS ?? "wasm,data").split(",").map((value) => value.trim()).filter(Boolean);
|
|
const isOpenScadScenario = scenario === "openscad" || scenario === "openscad-python";
|
|
const isPartScenario = scenario === "part" || scenario === "part-io" || scenario === "part-inspect" || scenario === "part-python" || scenario === "part-save";
|
|
const isPartDesignScenario = scenario === "partdesign";
|
|
const isPartDesignInspectScenario = scenario === "partdesign-inspect";
|
|
const isPartDesignPythonScenario = scenario === "partdesign-python";
|
|
const isPointsScenario = scenario === "points" || scenario === "points-inspect" || scenario === "points-python";
|
|
const isPointsInspectScenario = scenario === "points-inspect";
|
|
const isPointsPythonScenario = scenario === "points-python";
|
|
const isPointsSaveScenario = scenario === "points-save";
|
|
const isSandboxScenario = scenario === "sandbox";
|
|
const isSandboxInspectScenario = scenario === "sandbox-inspect";
|
|
const isSandboxPythonScenario = scenario === "sandbox-python";
|
|
const isSandboxSaveScenario = scenario === "sandbox-save";
|
|
const isSketcherScenario = scenario === "sketcher";
|
|
const isSketcherInspectScenario = scenario === "sketcher-inspect";
|
|
const isSketcherPythonScenario = scenario === "sketcher-python";
|
|
const isSpreadsheetScenario = scenario === "spreadsheet";
|
|
const isSpreadsheetInspectScenario = scenario === "spreadsheet-inspect";
|
|
const isSpreadsheetPythonScenario = scenario === "spreadsheet-python";
|
|
const isSpreadsheetSaveScenario = scenario === "spreadsheet-save";
|
|
const isSurfaceScenario = scenario === "surface";
|
|
const isSurfacePythonScenario = scenario === "surface-python";
|
|
const isSurfaceSaveScenario = scenario === "surface-save";
|
|
const isStartScenario = scenario === "start";
|
|
const isStartInspectScenario = scenario === "start-inspect";
|
|
const isStartPythonScenario = scenario === "start-python";
|
|
const isTemplatePyModScenario = scenario === "templatepymod";
|
|
const isTemplatePyModPythonScenario = scenario === "templatepymod-python";
|
|
const isTuxScenario = scenario === "tux";
|
|
const isWebScenario = scenario === "web";
|
|
const isWebPersistenceScenario = scenario === "web-action-03";
|
|
const isTestScenario = scenario === "test";
|
|
const isTechDrawScenario = scenario === "techdraw";
|
|
const isTechDrawNativeScenario = scenario === "techdraw-native";
|
|
const isTechDrawInspectScenario = scenario === "techdraw-inspect";
|
|
const isTechDrawPythonScenario = scenario === "techdraw-python";
|
|
const isTechDrawSaveScenario = scenario === "techdraw-save";
|
|
const isCoreScenario = scenario === "core";
|
|
const isDataPropertyScenario = scenario === "data-property";
|
|
const isDocumentScenario = scenario === "document";
|
|
const isWorkbenchScenario = scenario === "workbench";
|
|
const isViewScenario = scenario === "view";
|
|
const isWebLikeScenario = isWebScenario || isWebPersistenceScenario;
|
|
const isActionScenario = isOpenScadScenario || isPartScenario || isPartDesignScenario || isPartDesignInspectScenario || isPartDesignPythonScenario || isPointsScenario || isPointsSaveScenario || isSandboxScenario || isSandboxInspectScenario || isSandboxPythonScenario || isSandboxSaveScenario || isSketcherScenario || isSketcherInspectScenario || isSketcherPythonScenario || isSpreadsheetScenario || isSpreadsheetInspectScenario || isSpreadsheetPythonScenario || isSpreadsheetSaveScenario || isSurfaceScenario || isSurfacePythonScenario || isSurfaceSaveScenario || isStartScenario || isStartInspectScenario || isStartPythonScenario || isTemplatePyModScenario || isTemplatePyModPythonScenario || isTuxScenario || isWebLikeScenario || isTestScenario || isTechDrawScenario || isTechDrawNativeScenario || isTechDrawInspectScenario || isTechDrawPythonScenario || isTechDrawSaveScenario || isCoreScenario || isDataPropertyScenario || isDocumentScenario || isWorkbenchScenario || isViewScenario;
|
|
const scenarioStem = process.env.P6_CHROME_OUTPUT_STEM ?? (scenario === "view" ? "view-interaction-chrome" : scenario === "workbench" ? "workbench-command-chrome" : scenario === "document" ? "document-contract-chrome" : scenario === "data-property" ? "data-property-family-chrome" : scenario === "core" ? "core-shell-chrome" : scenario === "techdraw-save" ? "techdraw-action-05-native-chrome" : scenario === "techdraw-python" ? "techdraw-action-04-native-chrome" : scenario === "techdraw-inspect" ? "techdraw-action-03-native-chrome" : scenario === "techdraw-native" ? "techdraw-action-02-native-chrome" : scenario === "techdraw" ? "techdraw-chrome" : scenario === "test" ? "test-chrome" : scenario === "web-action-03" ? "web-action-03-chrome" : scenario === "web" ? "web-chrome" : scenario === "tux" ? "tux-chrome" : scenario === "templatepymod-python" ? "templatepymod-action-02-chrome" : scenario === "templatepymod" ? "templatepymod-chrome" : scenario === "start-python" ? "start-action-03-chrome" : scenario === "start-inspect" ? "start-action-02-chrome" : scenario === "start" ? "start-chrome" : scenario === "surface-save" ? "surface-action-04-chrome" : scenario === "surface-python" ? "surface-action-03-chrome" : scenario === "surface" ? "surface-chrome" : scenario === "spreadsheet-save" ? "spreadsheet-action-04-chrome" : scenario === "spreadsheet-python" ? "spreadsheet-action-03-chrome" : scenario === "spreadsheet-inspect" ? "spreadsheet-action-02-chrome" : scenario === "spreadsheet" ? "spreadsheet-chrome" : scenario === "sketcher-python" ? "sketcher-action-03-chrome" : scenario === "sketcher-inspect" ? "sketcher-action-02-chrome" : scenario === "sketcher" ? "sketcher-chrome" : scenario === "sandbox-save" ? "sandbox-action-04-chrome" : scenario === "sandbox-python" ? "sandbox-action-03-chrome" : scenario === "sandbox-inspect" ? "sandbox-action-02-chrome" : scenario === "sandbox" ? "sandbox-chrome" : scenario === "points-python" ? "points-python-chrome" : scenario === "points-inspect" ? "points-inspect-chrome" : scenario === "points-save" ? "points-action-04-chrome" : scenario === "openscad-python" ? "openscad-python-chrome" : scenario === "openscad" ? "openscad-chrome" : scenario === "points" ? "points-chrome" : scenario === "partdesign" ? "partdesign-chrome" : scenario === "partdesign-inspect" ? "partdesign-action-02-chrome" : scenario === "partdesign-python" ? "partdesign-action-03-chrome" : scenario === "part-save" ? "part-save-chrome" : scenario === "part-python" ? "part-python-chrome" : scenario === "part-inspect" ? "part-inspect-chrome" : scenario === "part-io" ? "part-io-chrome" : scenario === "part" ? "part-chrome" : "chrome-harness");
|
|
const outputPath = path.join(docsRoot, "p6", `${scenarioStem}.json`);
|
|
const schemaPath = path.join(docsRoot, "p6", "chrome-harness.schema.json");
|
|
const defaultLogPath = path.join(docsRoot,
|
|
scenario === "view" ? "freecad-p6-view-interaction-chrome.log" :
|
|
scenario === "workbench" ? "freecad-p6-workbench-command-chrome.log" :
|
|
scenario === "document" ? "freecad-p6-document-contract-chrome.log" :
|
|
scenario === "data-property" ? "freecad-p6-data-property-family-chrome.log" :
|
|
scenario === "core" ? "freecad-p6-core-shell-chrome.log" :
|
|
scenario === "openscad" ? "freecad-p6-openscad-f-chrome.log" :
|
|
scenario === "points-python" ? "freecad-p6-points-action-03-f-chrome.log" : scenario === "points-inspect" ? "freecad-p6-points-action-02-f-chrome.log" : scenario === "points" ? "freecad-p6-points-f-chrome.log" :
|
|
scenario === "partdesign" ? "freecad-p6-partdesign-f-chrome.log" : scenario === "partdesign-inspect" ? "freecad-p6-partdesign-action-02-f-chrome.log" : scenario === "partdesign-python" ? "freecad-p6-partdesign-action-03-f-chrome.log" : scenario === "points-save" ? "freecad-p6-points-action-04-f-chrome.log" :
|
|
scenario === "part-save" ? "freecad-p6-part-action-05-f-chrome.log" :
|
|
scenario === "part-python" ? "freecad-p6-part-action-04-f-chrome.log" :
|
|
scenario === "part-inspect" ? "freecad-p6-part-action-03-f-chrome.log" :
|
|
scenario === "part-io" ? "freecad-p6-part-action-02-f-chrome.log" :
|
|
scenario === "part" ? "freecad-p6-part-f-chrome.log" :
|
|
scenario === "sandbox" ? "freecad-p6-sandbox-f-chrome.log" :
|
|
scenario === "sandbox-inspect" ? "freecad-p6-sandbox-action-02-f-chrome.log" :
|
|
scenario === "sandbox-python" ? "freecad-p6-sandbox-action-03-f-chrome.log" :
|
|
scenario === "sandbox-save" ? "freecad-p6-sandbox-action-04-f-chrome.log" :
|
|
scenario === "sketcher" ? "freecad-p6-sketcher-f-chrome.log" :
|
|
scenario === "sketcher-inspect" ? "freecad-p6-sketcher-action-02-f-chrome.log" :
|
|
scenario === "sketcher-python" ? "freecad-p6-sketcher-action-03-f-chrome.log" :
|
|
scenario === "spreadsheet" ? "freecad-p6-spreadsheet-f-chrome.log" :
|
|
scenario === "spreadsheet-inspect" ? "freecad-p6-spreadsheet-action-02-f-chrome.log" :
|
|
scenario === "spreadsheet-python" ? "freecad-p6-spreadsheet-action-03-f-chrome.log" :
|
|
scenario === "spreadsheet-save" ? "freecad-p6-spreadsheet-action-04-f-chrome.log" :
|
|
scenario === "surface" ? "freecad-p6-surface-f-chrome.log" :
|
|
scenario === "surface-python" ? "freecad-p6-surface-action-03-f-chrome.log" :
|
|
scenario === "surface-save" ? "freecad-p6-surface-action-04-f-chrome.log" :
|
|
scenario === "start" ? "freecad-p6-start-f-chrome.log" :
|
|
scenario === "start-inspect" ? "freecad-p6-start-action-02-f-chrome.log" :
|
|
scenario === "start-python" ? "freecad-p6-start-action-03-f-chrome.log" :
|
|
scenario === "templatepymod" ? "freecad-p6-templatepymod-f-chrome.log" :
|
|
scenario === "tux" ? "freecad-p6-tux-f-chrome.log" :
|
|
scenario === "web" ? "freecad-p6-web-f-chrome.log" :
|
|
scenario === "templatepymod-python" ? "freecad-p6-templatepymod-action-02-f-chrome.log" :
|
|
scenario === "web-action-03" ? "freecad-p6-web-action-03-f-chrome.log" :
|
|
scenario === "test" ? "freecad-p6-test-f-chrome.log" :
|
|
scenario === "techdraw" ? "freecad-p6-techdraw-frontend-chrome.log" :
|
|
scenario === "techdraw-native" ? "freecad-p6-techdraw-action-02-native-f-chrome.log" :
|
|
scenario === "techdraw-inspect" ? "freecad-p6-techdraw-action-03-native-f-chrome.log" :
|
|
scenario === "techdraw-python" ? "freecad-p6-techdraw-action-04-native-f-chrome.log" :
|
|
scenario === "techdraw-save" ? "freecad-p6-techdraw-action-05-native-f-chrome.log" :
|
|
"freecad-p6-003-chrome-harness.log");
|
|
const logPath = process.env.P6_CHROME_LOG_PATH ? path.resolve(projectRoot, process.env.P6_CHROME_LOG_PATH) : defaultLogPath;
|
|
const screenshotPath = path.join(docsRoot, "p6", `${scenarioStem}.png`);
|
|
const viewport = { width: 1440, height: 900, deviceScaleFactor: 1 };
|
|
const timeoutMs = 30000;
|
|
|
|
function sha256(value) {
|
|
return crypto.createHash("sha256").update(value).digest("hex");
|
|
}
|
|
|
|
function findChrome() {
|
|
return [
|
|
"/usr/bin/google-chrome",
|
|
"/usr/bin/google-chrome-stable",
|
|
"/usr/bin/chromium",
|
|
"/usr/bin/chromium-browser",
|
|
"/opt/google/chrome/google-chrome",
|
|
].find((candidate) => fs.existsSync(candidate)) ?? null;
|
|
}
|
|
|
|
function contentType(filePath) {
|
|
return {
|
|
".html": "text/html; charset=utf-8",
|
|
".js": "text/javascript; charset=utf-8",
|
|
".wasm": "application/wasm",
|
|
".step": "application/step",
|
|
".json": "application/json; charset=utf-8",
|
|
".css": "text/css; charset=utf-8",
|
|
}[path.extname(filePath)] ?? "application/octet-stream";
|
|
}
|
|
|
|
function withPreload(url) {
|
|
if (!preloadModules.length) return url;
|
|
const value = new URL(url);
|
|
value.searchParams.set("moduleManifest", preloadManifest);
|
|
value.searchParams.set("preloadModules", preloadModules.join(","));
|
|
value.searchParams.set("preloadConcurrency", String(preloadConcurrency));
|
|
value.searchParams.set("preloadKinds", preloadKinds.join(","));
|
|
return value.href;
|
|
}
|
|
|
|
async function listen(server) {
|
|
await new Promise((resolve, reject) => {
|
|
server.once("error", reject);
|
|
server.listen(0, "127.0.0.1", resolve);
|
|
});
|
|
return server.address().port;
|
|
}
|
|
|
|
class CdpSession {
|
|
constructor(url) {
|
|
this.socket = new WebSocket(url);
|
|
this.nextId = 1;
|
|
this.pending = new Map();
|
|
this.listeners = new Map();
|
|
this.ready = new Promise((resolve, reject) => {
|
|
this.socket.addEventListener("open", resolve, { once: true });
|
|
this.socket.addEventListener("error", reject, { once: true });
|
|
});
|
|
this.socket.addEventListener("message", (event) => {
|
|
const message = JSON.parse(event.data);
|
|
if (message.id !== undefined) {
|
|
const pending = this.pending.get(message.id);
|
|
if (!pending) return;
|
|
this.pending.delete(message.id);
|
|
if (message.error) pending.reject(new Error(`${pending.method}: ${message.error.code}: ${message.error.message}`));
|
|
else pending.resolve(message.result);
|
|
return;
|
|
}
|
|
for (const listener of this.listeners.get(message.method) ?? []) listener(message.params);
|
|
});
|
|
}
|
|
|
|
// Keep the method name on rejected protocol calls for deterministic recovery logs.
|
|
|
|
async send(method, params = {}) {
|
|
await this.ready;
|
|
const id = this.nextId++;
|
|
return new Promise((resolve, reject) => {
|
|
this.pending.set(id, { resolve, reject, method });
|
|
this.socket.send(JSON.stringify({ id, method, params }));
|
|
});
|
|
}
|
|
|
|
on(method, listener) {
|
|
const listeners = this.listeners.get(method) ?? [];
|
|
listeners.push(listener);
|
|
this.listeners.set(method, listeners);
|
|
}
|
|
|
|
close() {
|
|
this.socket.close();
|
|
}
|
|
}
|
|
|
|
async function waitFor(label, predicate, evaluate, timeout = timeoutMs) {
|
|
const deadline = Date.now() + timeout;
|
|
let last;
|
|
while (Date.now() < deadline) {
|
|
last = await evaluate();
|
|
if (predicate(last)) return last;
|
|
await new Promise((resolve) => setTimeout(resolve, 200));
|
|
}
|
|
throw new Error(`${label} timed out: ${JSON.stringify(last)}`);
|
|
}
|
|
|
|
const log = [];
|
|
const chromePath = findChrome();
|
|
if (!chromePath) {
|
|
const blocked = {
|
|
schema: "freecad-web-p6-chrome-harness/v1",
|
|
result: "blocked_chrome_unavailable",
|
|
environmentError: "Google Chrome executable was not found; no alternate browser was attempted",
|
|
policy: { browser: "Google Chrome only", nonChromeBrowsersAttempted: false },
|
|
};
|
|
fs.mkdirSync(path.dirname(outputPath), { recursive: true });
|
|
fs.writeFileSync(outputPath, `${JSON.stringify(blocked, null, 2)}\n`);
|
|
fs.writeFileSync(logPath, `${blocked.environmentError}\n`);
|
|
console.error(blocked.environmentError);
|
|
process.exitCode = 2;
|
|
} else {
|
|
const entry = isViewScenario ? "index.html" : isWorkbenchScenario ? "workbench.html" : isDocumentScenario ? "document.html" : isDataPropertyScenario ? "core.html" : isCoreScenario ? "core.html" : isSurfaceSaveScenario ? "surface-save.html" : isSurfacePythonScenario ? "surface-python.html" : isSurfaceScenario ? "surface.html" : isSketcherPythonScenario ? "sketcher-python.html" : isSketcherInspectScenario ? "sketcher-inspect.html" : isSketcherScenario ? "sketcher.html" : isSandboxSaveScenario ? "sandbox-save.html" : isSandboxPythonScenario ? "sandbox-python.html" : isSandboxInspectScenario ? "sandbox-inspect.html" : isSandboxScenario ? "sandbox.html" : isOpenScadScenario ? "openscad.html" : isPointsPythonScenario ? "points-python.html" : isPointsInspectScenario ? "points-inspect.html" : isPointsSaveScenario ? "points-save.html" : isPointsScenario ? "points.html" : isPartDesignPythonScenario ? "partdesign-python.html" : isPartDesignInspectScenario ? "partdesign-inspect.html" : isPartDesignScenario ? "partdesign.html" : isPartScenario ? (scenario === "part-io" ? "part-io.html" : scenario === "part-inspect" ? "part-inspect.html" : scenario === "part-python" ? "part-python.html" : scenario === "part-save" ? "part-save.html" : "part.html") : "index.html";
|
|
if (!fs.existsSync(path.join(distRoot, entry))) throw new Error(`${entry} is missing; run npm run build first`);
|
|
const server = http.createServer((request, response) => {
|
|
const requestPath = decodeURIComponent(new URL(request.url ?? "/", "http://127.0.0.1").pathname);
|
|
if (requestPath === "/favicon.ico") {
|
|
response.writeHead(204);
|
|
response.end();
|
|
return;
|
|
}
|
|
const relative = requestPath === "/" ? "index.html" : requestPath.replace(/^\/+/, "");
|
|
const filePath = path.resolve(distRoot, relative);
|
|
if (filePath !== distRoot && !filePath.startsWith(`${distRoot}${path.sep}`)) {
|
|
response.writeHead(403); response.end("forbidden"); return;
|
|
}
|
|
if (!fs.existsSync(filePath) || !fs.statSync(filePath).isFile()) {
|
|
response.writeHead(404); response.end("not found"); return;
|
|
}
|
|
const cacheControl = requestPath.startsWith("/kernel/") || requestPath.startsWith("/modules/")
|
|
? "public, max-age=3600"
|
|
: "no-store";
|
|
response.writeHead(200, {
|
|
"content-type": contentType(filePath),
|
|
"cache-control": cacheControl,
|
|
// FreeCAD WASM uses SharedArrayBuffer; the smoke server must be
|
|
// cross-origin isolated just like the production deployment.
|
|
"cross-origin-opener-policy": "same-origin",
|
|
"cross-origin-embedder-policy": "require-corp",
|
|
});
|
|
fs.createReadStream(filePath).pipe(response);
|
|
});
|
|
const serverPort = await listen(server);
|
|
const origin = `http://127.0.0.1:${serverPort}`;
|
|
const userDataDir = fs.mkdtempSync(path.join(os.tmpdir(), "freecad-p6-chrome-"));
|
|
const chrome = spawn(chromePath, [
|
|
"--headless=new", "--no-sandbox", "--disable-gpu", "--disable-dev-shm-usage",
|
|
"--remote-debugging-port=0", "--remote-allow-origins=*", `--user-data-dir=${userDataDir}`,
|
|
`--window-size=${viewport.width},${viewport.height}`, withPreload(isViewScenario ? `${origin}/?sample=1` : isDataPropertyScenario ? `${origin}/core.html` : isCoreScenario ? `${origin}/core.html` : isSurfacePythonScenario ? `${origin}/surface-python.html` : isSurfaceScenario ? `${origin}/surface.html` : isSpreadsheetSaveScenario ? `${origin}/spreadsheet-save.html` : isSpreadsheetPythonScenario ? `${origin}/spreadsheet-python.html` : isSpreadsheetInspectScenario ? `${origin}/spreadsheet-inspect.html` : isSpreadsheetScenario ? `${origin}/spreadsheet.html` : isSketcherPythonScenario ? `${origin}/sketcher-python.html` : isSketcherInspectScenario ? `${origin}/sketcher-inspect.html` : isSketcherScenario ? `${origin}/sketcher.html` : isSandboxSaveScenario ? `${origin}/sandbox-save.html` : isSandboxPythonScenario ? `${origin}/sandbox-python.html` : isSandboxInspectScenario ? `${origin}/sandbox-inspect.html` : isSandboxScenario ? `${origin}/sandbox.html` : isOpenScadScenario ? `${origin}/openscad.html${scenario === "openscad-python" ? "?action=python-api" : ""}` : isPointsPythonScenario ? `${origin}/points-python.html` : isPointsInspectScenario ? `${origin}/points-inspect.html` : isPointsSaveScenario ? `${origin}/points-save.html` : isPointsScenario ? `${origin}/points.html` : isPartDesignPythonScenario ? `${origin}/partdesign-python.html` : isPartDesignInspectScenario ? `${origin}/partdesign-inspect.html` : isPartDesignScenario ? `${origin}/partdesign.html` : isPartScenario ? (scenario === "part-io" ? `${origin}/part-io.html` : scenario === "part-inspect" ? `${origin}/part-inspect.html` : scenario === "part-python" ? `${origin}/part-python.html` : scenario === "part-save" ? `${origin}/part-save.html` : `${origin}/part.html`) : `${origin}/?sample=1`),
|
|
], { stdio: ["ignore", "pipe", "pipe"] });
|
|
let chromeOutput = "";
|
|
chrome.stdout.on("data", (chunk) => { chromeOutput += chunk.toString(); });
|
|
chrome.stderr.on("data", (chunk) => { chromeOutput += chunk.toString(); });
|
|
|
|
let cdp;
|
|
try {
|
|
let target;
|
|
const deadline = Date.now() + timeoutMs;
|
|
while (!target && Date.now() < deadline) {
|
|
try {
|
|
const match = chromeOutput.match(/DevTools listening on ws:\/\/([^\s]+)/);
|
|
if (match) {
|
|
const port = match[1].split(":")[1].split("/")[0];
|
|
const targets = await fetch(`http://127.0.0.1:${port}/json/list`).then((response) => response.json());
|
|
target = targets.find((item) => item.type === "page");
|
|
}
|
|
} catch {
|
|
// Chrome is still starting.
|
|
}
|
|
if (!target) await new Promise((resolve) => setTimeout(resolve, 100));
|
|
}
|
|
if (!target?.webSocketDebuggerUrl) throw new Error(`Chrome CDP target unavailable: ${chromeOutput.trim()}`);
|
|
cdp = new CdpSession(target.webSocketDebuggerUrl);
|
|
const requests = [];
|
|
const responses = [];
|
|
const consoleEntries = [];
|
|
const exceptions = [];
|
|
cdp.on("Network.requestWillBeSent", (event) => requests.push({ requestId: event.requestId, url: event.request.url, method: event.request.method, type: event.type }));
|
|
cdp.on("Network.responseReceived", (event) => responses.push({ requestId: event.requestId, url: event.response.url, status: event.response.status }));
|
|
cdp.on("Runtime.consoleAPICalled", (event) => consoleEntries.push({ type: event.type, text: event.args?.map((arg) => arg.value ?? arg.description ?? "").join(" ") ?? "" }));
|
|
cdp.on("Runtime.exceptionThrown", (event) => exceptions.push(event.exceptionDetails?.text ?? "runtime exception"));
|
|
cdp.on("Log.entryAdded", (event) => { if (event.entry.level === "error") consoleEntries.push({ type: "log", text: event.entry.text }); });
|
|
await cdp.send("Runtime.enable");
|
|
await cdp.send("Network.enable");
|
|
await cdp.send("Log.enable");
|
|
await cdp.send("Page.enable");
|
|
await cdp.send("Emulation.setDeviceMetricsOverride", { ...viewport, mobile: false });
|
|
if (isSurfaceSaveScenario) await cdp.send("Page.navigate", { url: withPreload(`${origin}/surface-save.html`) });
|
|
if (isStartScenario) await cdp.send("Page.navigate", { url: withPreload(`${origin}/start.html`) });
|
|
if (isStartInspectScenario) await cdp.send("Page.navigate", { url: withPreload(`${origin}/start-inspect.html`) });
|
|
if (isStartPythonScenario) await cdp.send("Page.navigate", { url: withPreload(`${origin}/start-python.html`) });
|
|
if (isTemplatePyModScenario) await cdp.send("Page.navigate", { url: withPreload(`${origin}/templatepymod.html`) });
|
|
if (isTemplatePyModPythonScenario) await cdp.send("Page.navigate", { url: withPreload(`${origin}/templatepymod-python.html`) });
|
|
if (isTuxScenario) await cdp.send("Page.navigate", { url: withPreload(`${origin}/tux.html`) });
|
|
if (isWebScenario) await cdp.send("Page.navigate", { url: withPreload(`${origin}/web.html`) });
|
|
if (isWebPersistenceScenario) await cdp.send("Page.navigate", { url: withPreload(`${origin}/web-persistence.html`) });
|
|
if (isTestScenario) await cdp.send("Page.navigate", { url: withPreload(`${origin}/test.html`) });
|
|
if (isTechDrawScenario) await cdp.send("Page.navigate", { url: withPreload(`${origin}/techdraw.html`) });
|
|
if (isTechDrawNativeScenario) await cdp.send("Page.navigate", { url: withPreload(`${origin}/techdraw-native.html`) });
|
|
if (isTechDrawInspectScenario) await cdp.send("Page.navigate", { url: withPreload(`${origin}/techdraw-inspect.html`) });
|
|
if (isTechDrawPythonScenario) await cdp.send("Page.navigate", { url: withPreload(`${origin}/techdraw-python.html`) });
|
|
if (isTechDrawSaveScenario) await cdp.send("Page.navigate", { url: withPreload(`${origin}/techdraw-save.html`) });
|
|
if (isCoreScenario) await cdp.send("Page.navigate", { url: withPreload(`${origin}/core.html`) });
|
|
if (isDataPropertyScenario) await cdp.send("Page.navigate", { url: withPreload(`${origin}/core.html`) });
|
|
if (isDocumentScenario) await cdp.send("Page.navigate", { url: withPreload(`${origin}/document.html?mode=full`) });
|
|
if (isWorkbenchScenario) await cdp.send("Page.navigate", { url: withPreload(`${origin}/workbench.html`) });
|
|
|
|
async function evaluate(expression) {
|
|
const result = await cdp.send("Runtime.evaluate", { expression, returnByValue: true, awaitPromise: true });
|
|
if (result.exceptionDetails) throw new Error(result.exceptionDetails.text ?? "Runtime.evaluate failed");
|
|
return result.result?.value;
|
|
}
|
|
const modulePreload = preloadModules.length
|
|
? await waitFor("module artifact preload", (value) => value?.phase === "ready" || value?.phase === "error", () => evaluate("window.__freecadModulePreloadState ?? null"), 180000)
|
|
: { phase: "disabled", manifestUrl: null, requestedModules: [], requestedAssets: 0, loadedAssets: 0, failedAssets: [], loadedBytes: 0 };
|
|
if (modulePreload.phase === "error") throw new Error(`module artifact preload failed: ${JSON.stringify(modulePreload.failedAssets)}`);
|
|
async function state() {
|
|
if (isViewScenario) return evaluate("(() => ({status: document.querySelector('#status')?.textContent ?? '', stage: window.__freecadViewState?.stage ?? 'starting', view: window.__freecadViewState ?? null, canvasPixels: (() => { const c = document.querySelector('#viewport canvas'); if (!c) return 0; const ctx = c.getContext('2d'); return ctx ? 0 : c.width * c.height; })(), controls: [...document.querySelectorAll('button,select,input')].filter((item) => !item.disabled).map((item) => item.id)}))()");
|
|
if (isWorkbenchScenario) return evaluate("(() => ({status: document.querySelector('.status')?.textContent ?? '', stage: window.__freecadWorkbenchState?.stage ?? 'ready', response: window.__freecadWorkbenchState?.response ?? null, matrix: window.__freecadWorkbenchState?.matrix ?? null, selected: window.__freecadWorkbenchState?.selected ?? null}))()");
|
|
if (isWebScenario) return evaluate("(() => ({status: document.querySelector('.status')?.textContent ?? '', stage: window.__freecadWebState?.stage ?? 'ready', response: window.__freecadWebState?.response ?? null}))()");
|
|
if (isWebPersistenceScenario) return evaluate("(() => ({status: document.querySelector('.status')?.textContent ?? '', stage: window.__freecadWebPersistenceState?.stage ?? 'ready', response: window.__freecadWebPersistenceState?.response ?? null}))()");
|
|
if (isTestScenario) return evaluate("(() => ({status: document.querySelector('.status')?.textContent ?? '', stage: window.__freecadTestState?.stage ?? 'ready', response: window.__freecadTestState?.response ?? null}))()");
|
|
if (isTechDrawScenario) return evaluate("(() => ({status: document.querySelector('.status')?.textContent ?? '', stage: window.__freecadTechDrawState?.stage ?? 'ready', response: window.__freecadTechDrawState?.response ?? null}))()");
|
|
if (isTechDrawNativeScenario) return evaluate("(() => ({status: document.querySelector('.status')?.textContent ?? '', stage: window.__freecadTechDrawNativeState?.stage ?? 'ready', response: window.__freecadTechDrawNativeState?.response ?? null}))()");
|
|
if (isTechDrawInspectScenario) return evaluate("(() => ({status: document.querySelector('.status')?.textContent ?? '', stage: window.__freecadTechDrawInspectState?.stage ?? 'ready', response: window.__freecadTechDrawInspectState?.response ?? null}))()");
|
|
if (isTechDrawPythonScenario) return evaluate("(() => ({status: document.querySelector('.status')?.textContent ?? '', stage: window.__freecadTechDrawPyState?.stage ?? 'ready', response: window.__freecadTechDrawPyState?.response ?? null}))()");
|
|
if (isTechDrawSaveScenario) return evaluate("(() => ({status: document.querySelector('.status')?.textContent ?? '', stage: window.__freecadTechDrawSaveState?.stage ?? 'ready', response: window.__freecadTechDrawSaveState?.response ?? null}))()");
|
|
if (isCoreScenario) return evaluate("(() => { const s = window.__freecadCoreShellState ?? {}; const error = s.response?.error?.code ?? null; return {status: document.querySelector('.status')?.textContent ?? '', stage: s.stage ?? 'ready', response: s.response ?? null, progress: s.progress ?? [], duplicate: s.duplicate ?? (error === 'duplicate_request' ? error : null), cancel: s.cancel ?? (error === 'cancelled' ? error : null)}; })()");
|
|
if (isDataPropertyScenario) return evaluate("(() => { const s = window.__freecadCoreShellState ?? {}; const error = s.response?.error?.code ?? null; return {status: document.querySelector('.status')?.textContent ?? '', stage: s.stage ?? 'ready', response: s.response ?? null, progress: s.progress ?? [], duplicate: s.duplicate ?? (error === 'duplicate_request' ? error : null), cancel: s.cancel ?? (error === 'cancelled' ? error : null)}; })()");
|
|
if (isDocumentScenario) return evaluate("(() => { const s = window.__freecadCoreShellState ?? {}; const error = s.response?.error?.code ?? null; return {status: document.querySelector('.status')?.textContent ?? '', stage: s.stage ?? 'ready', response: s.response ?? null, progress: s.progress ?? [], duplicate: s.duplicate ?? (error === 'duplicate_request' ? error : null), cancel: s.cancel ?? (error === 'cancelled' ? error : null)}; })()");
|
|
if (isTuxScenario) return evaluate("(() => ({status: document.querySelector('.status')?.textContent ?? '', stage: window.__freecadTuxState?.stage ?? 'ready', response: window.__freecadTuxState?.response ?? null}))()");
|
|
if (isTemplatePyModPythonScenario) return evaluate("(() => ({status: document.querySelector('#status')?.textContent ?? '', level: document.querySelector('#status')?.dataset.level ?? '', stage: window.__freecadTpmPyState?.stage ?? '', requestId: window.__freecadTpmPyState?.requestId ?? null, response: window.__freecadTpmPyState?.response ?? null, api: document.querySelector('#api')?.textContent ?? '', resources: document.querySelector('#resources')?.textContent ?? ''}))()");
|
|
if (isTemplatePyModScenario) return evaluate("(() => ({status: document.querySelector('#status')?.textContent ?? '', level: document.querySelector('#status')?.dataset.level ?? '', stage: window.__freecadTpmState?.stage ?? '', requestId: window.__freecadTpmState?.requestId ?? null, response: window.__freecadTpmState?.response ?? null, lines: document.querySelector('#lines')?.textContent ?? '', pythonOnly: document.querySelector('#pythonOnly')?.textContent ?? ''}))()");
|
|
if (isStartPythonScenario) return evaluate("(() => ({status: document.querySelector('#status')?.textContent ?? '', level: document.querySelector('#status')?.dataset.level ?? '', stage: window.__freecadStartPyState?.stage ?? '', requestId: window.__freecadStartPyState?.requestId ?? null, response: window.__freecadStartPyState?.response ?? null, module: document.querySelector('#module')?.textContent ?? '', api: document.querySelector('#api')?.textContent ?? '', resources: document.querySelector('#resources')?.textContent ?? ''}))()");
|
|
if (isStartInspectScenario) return evaluate("(() => ({status: document.querySelector('#status')?.textContent ?? '', level: document.querySelector('#status')?.dataset.level ?? '', stage: window.__freecadStartInventoryState?.stage ?? '', requestId: window.__freecadStartInventoryState?.requestId ?? null, response: window.__freecadStartInventoryState?.response ?? null, entry: document.querySelector('#entry')?.textContent ?? '', types: document.querySelector('#types')?.textContent ?? '', readOnly: document.querySelector('#readOnly')?.textContent ?? ''}))()");
|
|
if (isStartScenario) return evaluate("(() => ({status: document.querySelector('#status')?.textContent ?? '', level: document.querySelector('#status')?.dataset.level ?? '', stage: window.__freecadStartState?.stage ?? '', requestId: window.__freecadStartState?.requestId ?? null, response: window.__freecadStartState?.response ?? null, module: document.querySelector('#module')?.textContent ?? '', types: document.querySelector('#types')?.textContent ?? '', resources: document.querySelector('#resources')?.textContent ?? ''}))()");
|
|
if (isSurfaceSaveScenario) return evaluate("(() => ({status: document.querySelector('#status')?.textContent ?? '', level: document.querySelector('#status')?.dataset.level ?? '', stage: window.__freecadSurfaceSaveState?.stage ?? '', requestId: window.__freecadSurfaceSaveState?.requestId ?? null, response: window.__freecadSurfaceSaveState?.response ?? null, path: document.querySelector('#path')?.textContent ?? '', objects: document.querySelector('#objects')?.textContent ?? '', round: document.querySelector('#round')?.textContent ?? ''}))()");
|
|
if (isSurfacePythonScenario) return evaluate("(() => ({status: document.querySelector('#status')?.textContent ?? '', level: document.querySelector('#status')?.dataset.level ?? '', stage: window.__freecadSurfacePyState?.stage ?? '', requestId: window.__freecadSurfacePyState?.requestId ?? null, response: window.__freecadSurfacePyState?.response ?? null, count: document.querySelector('#count')?.textContent ?? '', names: document.querySelector('#names')?.textContent ?? ''}))()");
|
|
if (isSurfaceScenario) return evaluate("(() => ({status: document.querySelector('#status')?.textContent ?? '', level: document.querySelector('#status')?.dataset.level ?? '', stage: window.__freecadSurfaceState?.stage ?? '', requestId: window.__freecadSurfaceState?.requestId ?? null, response: window.__freecadSurfaceState?.response ?? null, object: document.querySelector('#object')?.textContent ?? '', objects: document.querySelector('#objects')?.textContent ?? '', recompute: document.querySelector('#recompute')?.textContent ?? ''}))()");
|
|
if (isSpreadsheetSaveScenario) return evaluate("(() => ({status: document.querySelector('#status')?.textContent ?? '', level: document.querySelector('#status')?.dataset.level ?? '', stage: window.__freecadSpreadsheetSaveState?.stage ?? '', requestId: window.__freecadSpreadsheetSaveState?.requestId ?? null, response: window.__freecadSpreadsheetSaveState?.response ?? null, path: document.querySelector('#path')?.textContent ?? '', size: document.querySelector('#size')?.textContent ?? '', round: document.querySelector('#round')?.textContent ?? ''}))()");
|
|
if (isSpreadsheetPythonScenario) return evaluate("(() => ({status: document.querySelector('#status')?.textContent ?? '', level: document.querySelector('#status')?.dataset.level ?? '', stage: window.__freecadSpreadsheetPythonState?.stage ?? '', requestId: window.__freecadSpreadsheetPythonState?.requestId ?? null, response: window.__freecadSpreadsheetPythonState?.response ?? null, entry: document.querySelector('#entry')?.textContent ?? '', count: document.querySelector('#count')?.textContent ?? '', cells: document.querySelector('#cells')?.textContent ?? ''}))()");
|
|
if (isSpreadsheetInspectScenario) return evaluate("(() => ({status: document.querySelector('#status')?.textContent ?? '', level: document.querySelector('#status')?.dataset.level ?? '', stage: window.__freecadSpreadsheetInspectState?.stage ?? '', requestId: window.__freecadSpreadsheetInspectState?.requestId ?? null, response: window.__freecadSpreadsheetInspectState?.response ?? null, entry: document.querySelector('#entry')?.textContent ?? '', types: document.querySelector('#types')?.textContent ?? '', formats: document.querySelector('#formats')?.textContent ?? '', bindings: document.querySelector('#bindings')?.textContent ?? ''}))()");
|
|
if (isSpreadsheetScenario) return evaluate("(() => ({status: document.querySelector('#status')?.textContent ?? '', level: document.querySelector('#status')?.dataset.level ?? '', stage: window.__freecadSpreadsheetState?.stage ?? '', requestId: window.__freecadSpreadsheetState?.requestId ?? null, response: window.__freecadSpreadsheetState?.response ?? null, type: document.querySelector('#type')?.textContent ?? '', a1: document.querySelector('#a1')?.textContent ?? '', b1: document.querySelector('#b1')?.textContent ?? ''}))()");
|
|
if (isSandboxScenario) return evaluate("(() => ({status: document.querySelector('#status')?.textContent ?? '', level: document.querySelector('#status')?.dataset.level ?? '', stage: window.__freecadSandboxState?.stage ?? '', requestId: window.__freecadSandboxState?.requestId ?? null, response: window.__freecadSandboxState?.response ?? null, object: document.querySelector('#object')?.textContent ?? '', integer: document.querySelector('#integer')?.textContent ?? '', resources: document.querySelector('#resources')?.textContent ?? ''}))()");
|
|
if (isSandboxInspectScenario) return evaluate("(() => ({status: document.querySelector('#status')?.textContent ?? '', level: document.querySelector('#status')?.dataset.level ?? '', stage: window.__freecadSandboxInspectState?.stage ?? '', requestId: window.__freecadSandboxInspectState?.requestId ?? null, response: window.__freecadSandboxInspectState?.response ?? null, entry: document.querySelector('#entry')?.textContent ?? '', types: document.querySelector('#types')?.textContent ?? '', python: document.querySelector('#python')?.textContent ?? ''}))()");
|
|
if (isSandboxPythonScenario) return evaluate("(() => ({status: document.querySelector('#status')?.textContent ?? '', level: document.querySelector('#status')?.dataset.level ?? '', stage: window.__freecadSandboxPythonState?.stage ?? '', requestId: window.__freecadSandboxPythonState?.requestId ?? null, response: window.__freecadSandboxPythonState?.response ?? null, entry: document.querySelector('#entry')?.textContent ?? '', count: document.querySelector('#count')?.textContent ?? ''}))()");
|
|
if (isSandboxSaveScenario) return evaluate("(() => ({status: document.querySelector('#status')?.textContent ?? '', level: document.querySelector('#status')?.dataset.level ?? '', stage: window.__freecadSandboxSaveState?.stage ?? '', requestId: window.__freecadSandboxSaveState?.requestId ?? null, response: window.__freecadSandboxSaveState?.response ?? null, object: document.querySelector('#object')?.textContent ?? '', size: document.querySelector('#size')?.textContent ?? '', round: document.querySelector('#round')?.textContent ?? ''}))()");
|
|
if (isSketcherScenario) return evaluate("(() => ({status: document.querySelector('#status')?.textContent ?? '', level: document.querySelector('#status')?.dataset.level ?? '', stage: window.__freecadSketcherState?.stage ?? '', requestId: window.__freecadSketcherState?.requestId ?? null, response: window.__freecadSketcherState?.response ?? null, geometry: document.querySelector('#geometry')?.textContent ?? '', constraints: document.querySelector('#constraints')?.textContent ?? '', dof: document.querySelector('#dof')?.textContent ?? ''}))()");
|
|
if (isSketcherInspectScenario) return evaluate("(() => ({status: document.querySelector('#status')?.textContent ?? '', level: document.querySelector('#status')?.dataset.level ?? '', stage: window.__freecadSketcherInspectState?.stage ?? '', requestId: window.__freecadSketcherInspectState?.requestId ?? null, response: window.__freecadSketcherInspectState?.response ?? null, entry: document.querySelector('#entry')?.textContent ?? '', types: document.querySelector('#types')?.textContent ?? '', object: document.querySelector('#object')?.textContent ?? ''}))()");
|
|
if (isSketcherPythonScenario) return evaluate("(() => ({status: document.querySelector('#status')?.textContent ?? '', level: document.querySelector('#status')?.dataset.level ?? '', stage: window.__freecadSketcherPythonState?.stage ?? '', requestId: window.__freecadSketcherPythonState?.requestId ?? null, response: window.__freecadSketcherPythonState?.response ?? null, entry: document.querySelector('#entry')?.textContent ?? '', count: document.querySelector('#count')?.textContent ?? ''}))()");
|
|
if (isOpenScadScenario) return evaluate("(() => ({status: document.querySelector('#status')?.textContent ?? '', level: document.querySelector('#status-dot')?.dataset.level ?? '', stage: window.__freecadOpenScadState?.stage ?? '', requestId: window.__freecadOpenScadState?.requestId ?? null, response: window.__freecadOpenScadState?.response ?? null, formats: document.querySelector('#formats')?.textContent ?? '', readonly: document.querySelector('#readonly')?.textContent ?? ''}))()");
|
|
if (isPartDesignPythonScenario) return evaluate("(() => ({status: document.querySelector('#status')?.textContent ?? '', level: document.querySelector('#status')?.dataset.level ?? '', stage: window.__freecadPartDesignPythonState?.stage ?? '', requestId: window.__freecadPartDesignPythonState?.requestId ?? null, response: window.__freecadPartDesignPythonState?.response ?? null, entry: document.querySelector('#entry')?.textContent ?? '', exports: document.querySelector('#exports')?.textContent ?? '', bindings: document.querySelector('#bindings')?.textContent ?? ''}))()");
|
|
if (isPartDesignInspectScenario) return evaluate("(() => ({status: document.querySelector('#status')?.textContent ?? '', level: document.querySelector('#status')?.dataset.level ?? '', stage: window.__freecadPartDesignInspectState?.stage ?? '', requestId: window.__freecadPartDesignInspectState?.requestId ?? null, response: window.__freecadPartDesignInspectState?.response ?? null, entry: document.querySelector('#entry')?.textContent ?? '', types: document.querySelector('#types')?.textContent ?? '', formats: document.querySelector('#formats')?.textContent ?? '', bindings: document.querySelector('#bindings')?.textContent ?? ''}))()");
|
|
if (isPartDesignScenario) return evaluate("(() => ({status: document.querySelector('#status')?.textContent ?? '', level: document.querySelector('#status')?.dataset.level ?? '', stage: window.__freecadPartDesignState?.stage ?? '', requestId: window.__freecadPartDesignState?.requestId ?? null, response: window.__freecadPartDesignState?.response ?? null, volume: document.querySelector('#volume')?.textContent ?? '', resources: document.querySelector('#resources')?.textContent ?? ''}))()");
|
|
if (isPointsSaveScenario) return evaluate("(() => ({status: document.querySelector('#status')?.textContent ?? '', level: document.querySelector('#status')?.dataset.level ?? '', stage: window.__freecadPointsSaveState?.stage ?? '', requestId: window.__freecadPointsSaveState?.requestId ?? null, response: window.__freecadPointsSaveState?.response ?? null, path: document.querySelector('#path')?.textContent ?? '', object: document.querySelector('#object')?.textContent ?? '', count: document.querySelector('#count')?.textContent ?? '', size: document.querySelector('#size')?.textContent ?? ''}))()");
|
|
if (isPointsScenario) return evaluate("(() => ({status: document.querySelector('#status')?.textContent ?? '', level: document.querySelector('#status')?.dataset.level ?? '', stage: window.__freecadPointsState?.stage ?? '', requestId: window.__freecadPointsState?.requestId ?? null, response: window.__freecadPointsState?.response ?? null, count: document.querySelector('#count')?.textContent ?? '', bounds: document.querySelector('#bounds')?.textContent ?? ''}))()");
|
|
if (isPartScenario) return evaluate("(() => ({status: document.querySelector('#status')?.textContent ?? '', level: document.querySelector('#status')?.dataset.level ?? '', stage: window.__freecadPartState?.stage ?? '', requestId: window.__freecadPartState?.requestId ?? null, response: window.__freecadPartState?.response ?? null, volume: document.querySelector('#volume')?.textContent ?? '', resources: document.querySelector('#resources')?.textContent ?? ''}))()");
|
|
return evaluate("(() => ({status: document.querySelector('#status')?.textContent ?? '', level: document.querySelector('#status')?.dataset.level ?? '', file: document.querySelector('#file-name')?.textContent ?? '', vertex: document.querySelector('#vertex-count')?.textContent ?? '', triangle: document.querySelector('#triangle-count')?.textContent ?? '', face: document.querySelector('#face-count')?.textContent ?? '', requestId: window.__freecadLastResponseRequestId ?? null}))()");
|
|
}
|
|
async function navigate(url) {
|
|
requests.length = 0; responses.length = 0; consoleEntries.length = 0; exceptions.length = 0;
|
|
await cdp.send("Page.navigate", { url: withPreload(url) });
|
|
await waitFor("kernel startup", (value) => value.status !== "" && !value.status.includes("初始化") && !value.status.includes("等待"), state);
|
|
}
|
|
function caseEvidence(id, value, screenshotHash = null) {
|
|
const externalRequests = requests.filter((request) => !request.url.startsWith(origin)).map((request) => request.url);
|
|
return { id, status: "pass", state: value, requests: [...requests], responses: [...responses], externalRequests, console: [...consoleEntries], exceptions: [...exceptions], screenshotSha256: screenshotHash };
|
|
}
|
|
|
|
const cases = [];
|
|
if (isActionScenario) {
|
|
if (isViewScenario) {
|
|
const readyState = await waitFor("view sample", (value) => value.stage === "ready" && value.view?.objectCount === 1, state);
|
|
cases.push(caseEvidence("view-ready", readyState));
|
|
requests.length = 0; responses.length = 0; consoleEntries.length = 0; exceptions.length = 0;
|
|
await evaluate("document.querySelector('#object-tree-primary').click()");
|
|
cases.push(caseEvidence("view-selection", await waitFor("view selection", (value) => value.view?.selected === true, state)));
|
|
await evaluate("document.querySelector('#object-tree-primary').dispatchEvent(new PointerEvent('pointermove', {bubbles:true}))").catch(() => undefined);
|
|
await evaluate("document.querySelector('#section-button').click()");
|
|
cases.push(caseEvidence("view-section", await waitFor("view section", (value) => value.view?.section === true, state)));
|
|
await evaluate("document.querySelector('#visibility-button').click()");
|
|
cases.push(caseEvidence("view-hidden", await waitFor("view hidden", (value) => value.view?.visible === false, state)));
|
|
await evaluate("document.querySelector('#visibility-button').click(); document.querySelector('#front-button').click(); document.dispatchEvent(new KeyboardEvent('keydown', {key:'f', bubbles:true}))");
|
|
const desktop = await waitFor("view desktop", (value) => value.view?.visible === true && value.view?.section === true, state);
|
|
const desktopScreenshot = await cdp.send("Page.captureScreenshot", { format: "png", fromSurface: true });
|
|
const desktopBytes = Buffer.from(desktopScreenshot.data, "base64");
|
|
const desktopPath = path.join(docsRoot, "p6", "view-interaction-desktop.png");
|
|
fs.writeFileSync(desktopPath, desktopBytes);
|
|
cases.push(caseEvidence("view-desktop", desktop, sha256(desktopBytes)));
|
|
await cdp.send("Emulation.setDeviceMetricsOverride", { width: 390, height: 844, deviceScaleFactor: 1, mobile: true });
|
|
const mobile = await waitFor("view mobile layout", (value) => value.controls.includes("fit-button") && value.controls.includes("section-button"), state);
|
|
const mobileScreenshot = await cdp.send("Page.captureScreenshot", { format: "png", fromSurface: true });
|
|
const mobileBytes = Buffer.from(mobileScreenshot.data, "base64");
|
|
const mobilePath = path.join(docsRoot, "p6", "view-interaction-mobile.png");
|
|
fs.writeFileSync(mobilePath, mobileBytes);
|
|
cases.push(caseEvidence("view-mobile", mobile, sha256(mobileBytes)));
|
|
} else {
|
|
const actionLabel = isWorkbenchScenario ? "Workbench command matrix" : isDataPropertyScenario ? "Data-property family" : isCoreScenario ? "React Core" : isTechDrawSaveScenario ? "TechDraw save/reopen" : isTechDrawPythonScenario ? "TechDraw Python" : isTechDrawInspectScenario ? "TechDraw inventory" : isTechDrawNativeScenario ? "TechDraw native" : isTechDrawScenario ? "TechDraw frontend" : isTestScenario ? "Test inventory" : isWebPersistenceScenario ? "Web save/reopen" : isWebScenario ? "Web" : isTuxScenario ? "Tux" : isTemplatePyModPythonScenario ? "TemplatePyMod Python" : isTemplatePyModScenario ? "TemplatePyMod" : isStartPythonScenario ? "Start Python" : isStartInspectScenario ? "Start inventory" : isStartScenario ? "Start" : isSurfaceSaveScenario ? "Surface save" : isSurfacePythonScenario ? "Surface Python" : isSurfaceScenario ? "Surface" : isSpreadsheetSaveScenario ? "Spreadsheet save" : isSpreadsheetPythonScenario ? "Spreadsheet Python" : isSpreadsheetInspectScenario ? "Spreadsheet inventory" : isSpreadsheetScenario ? "Spreadsheet" : isSketcherPythonScenario ? "Sketcher Python" : isSketcherInspectScenario ? "Sketcher inventory" : isSketcherScenario ? "Sketcher" : isSandboxSaveScenario ? "Sandbox save" : isSandboxPythonScenario ? "Sandbox Python" : isSandboxInspectScenario ? "Sandbox inventory" : isSandboxScenario ? "Sandbox" : isPointsSaveScenario ? "Points save" : isPointsScenario ? "Points" : isPartDesignPythonScenario ? "PartDesign Python" : isPartDesignInspectScenario ? "PartDesign inventory" : isPartDesignScenario ? "PartDesign" : isPartScenario ? "Part" : "OpenSCAD";
|
|
const casePrefix = scenario;
|
|
if (isWorkbenchScenario) await waitFor("Workbench React controls", (value) => value === true, () => evaluate("Boolean(document.querySelector('#validate-command-matrix'))"));
|
|
if (isWebScenario) await waitFor("Web React controls", (value) => value === true, () => evaluate("Boolean(document.querySelector('#run'))"));
|
|
const readyState = await waitFor(`${actionLabel} ready`, (value) => value.stage === "ready", state);
|
|
cases.push(caseEvidence(`${casePrefix}-ready`, readyState));
|
|
if (isTuxScenario) await waitFor("Tux React controls", (value) => value === true, () => evaluate("Boolean(document.querySelector('#run'))"));
|
|
if (isWebPersistenceScenario) await waitFor("Web persistence React controls", (value) => value === true, () => evaluate("Boolean(document.querySelector('#run'))"));
|
|
if (isTestScenario) await waitFor("Test React controls", (value) => value === true, () => evaluate("Boolean(document.querySelector('#run'))"));
|
|
if (isTechDrawScenario) await waitFor("TechDraw React controls", (value) => value === true, () => evaluate("Boolean(document.querySelector('#run'))"));
|
|
if (isTechDrawNativeScenario) await waitFor("TechDraw native React controls", (value) => value === true, () => evaluate("Boolean(document.querySelector('#run'))"));
|
|
if (isTechDrawInspectScenario) await waitFor("TechDraw inspect React controls", (value) => value === true, () => evaluate("Boolean(document.querySelector('#run'))"));
|
|
if (isTechDrawPythonScenario) await waitFor("TechDraw Python React controls", (value) => value === true, () => evaluate("Boolean(document.querySelector('#run'))"));
|
|
if (isTechDrawSaveScenario) await waitFor("TechDraw save React controls", (value) => value === true, () => evaluate("Boolean(document.querySelector('#run'))"));
|
|
if (isCoreScenario) await waitFor("Core React controls", (value) => value === true, () => evaluate("Boolean(document.querySelector('#run'))"));
|
|
if (isDataPropertyScenario) await waitFor("Data-property React controls", (value) => value === true, () => evaluate("Boolean(document.querySelector('#data-property-family'))"));
|
|
if (isDocumentScenario) await waitFor("Document React controls", (value) => value === true, () => evaluate("Boolean(document.querySelector('#run'))"));
|
|
requests.length = 0; responses.length = 0; consoleEntries.length = 0; exceptions.length = 0;
|
|
await evaluate(isWorkbenchScenario ? "document.querySelector('#validate-command-matrix').click()" : isDataPropertyScenario ? "document.querySelector('#data-property-family').click()" : "document.querySelector('#run').click()");
|
|
const actionState = await waitFor(`${actionLabel} action`, (value) => isWorkbenchScenario ? value.stage === "success" && value.matrix?.records === 778 : value.stage === "success", state);
|
|
const screenshot = await cdp.send("Page.captureScreenshot", { format: "png", fromSurface: true });
|
|
const screenshotBytes = Buffer.from(screenshot.data, "base64");
|
|
fs.writeFileSync(screenshotPath, screenshotBytes);
|
|
cases.push(caseEvidence(`${casePrefix}-action`, actionState, sha256(screenshotBytes)));
|
|
if (isWorkbenchScenario) {
|
|
requests.length = 0; responses.length = 0; consoleEntries.length = 0; exceptions.length = 0;
|
|
await evaluate("document.querySelector('#error').click()");
|
|
cases.push(caseEvidence(`${casePrefix}-expected-error`, await waitFor(`${actionLabel} expected error`, (value) => value.stage === "expected-error" && value.response?.error?.code === "invalid_request", state)));
|
|
requests.length = 0; responses.length = 0; consoleEntries.length = 0; exceptions.length = 0;
|
|
await evaluate("document.querySelector('#cancel').click()");
|
|
cases.push(caseEvidence(`${casePrefix}-cancel`, await waitFor(`${actionLabel} cancel`, (value) => value.stage === "cancelled" && value.response?.error?.code === "cancelled", state)));
|
|
}
|
|
if (isCoreScenario) {
|
|
cases.push(caseEvidence(`${casePrefix}-duplicate`, await waitFor(`${actionLabel} duplicate`, (value) => value.duplicate === "duplicate_request", state)));
|
|
}
|
|
if (isDataPropertyScenario) {
|
|
requests.length = 0; responses.length = 0; consoleEntries.length = 0; exceptions.length = 0;
|
|
await evaluate("document.querySelector('#cancel').click()");
|
|
cases.push(caseEvidence(`${casePrefix}-cancel`, await waitFor(`${actionLabel} cancel`, (value) => value.stage === "cancelled", state)));
|
|
}
|
|
if (isDocumentScenario) {
|
|
cases.push(caseEvidence(`${casePrefix}-duplicate`, await waitFor(`${actionLabel} duplicate`, (value) => value.duplicate === "duplicate_request", state)));
|
|
}
|
|
if (!isWorkbenchScenario) {
|
|
requests.length = 0; responses.length = 0; consoleEntries.length = 0; exceptions.length = 0;
|
|
await evaluate("document.querySelector('#error').click()");
|
|
cases.push(caseEvidence(`${casePrefix}-expected-error`, await waitFor(`${actionLabel} expected error`, (value) => value.stage === "expected-error", state)));
|
|
}
|
|
if (isCoreScenario) {
|
|
requests.length = 0; responses.length = 0; consoleEntries.length = 0; exceptions.length = 0;
|
|
await evaluate("document.querySelector('#cancel').click()");
|
|
cases.push(caseEvidence(`${casePrefix}-cancel`, await waitFor(`${actionLabel} cancel`, (value) => value.stage === "cancelled", state)));
|
|
}
|
|
if (isDocumentScenario) {
|
|
requests.length = 0; responses.length = 0; consoleEntries.length = 0; exceptions.length = 0;
|
|
await evaluate("document.querySelector('#cancel').click()");
|
|
cases.push(caseEvidence(`${casePrefix}-cancel`, await waitFor(`${actionLabel} cancel`, (value) => value.stage === "cancelled", state)));
|
|
}
|
|
requests.length = 0; responses.length = 0; consoleEntries.length = 0; exceptions.length = 0;
|
|
await evaluate("document.querySelector('#reset').click()");
|
|
cases.push(caseEvidence(`${casePrefix}-reset`, await waitFor(`${actionLabel} reset`, (value) => value.stage === "reset", state)));
|
|
requests.length = 0; responses.length = 0; consoleEntries.length = 0; exceptions.length = 0;
|
|
await evaluate(isWorkbenchScenario ? "document.querySelector('#validate-command-matrix').click()" : isDataPropertyScenario ? "document.querySelector('#data-property-family').click()" : "document.querySelector('#run').click()");
|
|
cases.push(caseEvidence(`${casePrefix}-repeat`, await waitFor(`${actionLabel} repeat`, (value) => isWorkbenchScenario ? value.stage === "success" : value.stage === "success", state)));
|
|
}
|
|
} else {
|
|
const importState = await waitFor("sample import", (value) => value.status.startsWith("已加载 "), state);
|
|
const screenshot = await cdp.send("Page.captureScreenshot", { format: "png", fromSurface: true });
|
|
const screenshotBytes = Buffer.from(screenshot.data, "base64");
|
|
fs.writeFileSync(screenshotPath, screenshotBytes);
|
|
cases.push(caseEvidence("sample-import", importState, sha256(screenshotBytes)));
|
|
await evaluate("document.querySelector('#reset-button').click()");
|
|
cases.push(caseEvidence("reset", await waitFor("reset", (value) => value.status === "已清空", state)));
|
|
|
|
await navigate(`${origin}/?storage=1`);
|
|
cases.push(caseEvidence("opfs", await waitFor("OPFS smoke", (value) => value.status.includes("OPFS smoke passed"), state)));
|
|
await navigate(`${origin}/?cache=1`);
|
|
cases.push(caseEvidence("mesh-cache", await waitFor("mesh cache smoke", (value) => value.status === "网格缓存 smoke passed", state)));
|
|
|
|
requests.length = 0; responses.length = 0; consoleEntries.length = 0; exceptions.length = 0;
|
|
await cdp.send("Page.navigate", { url: `${origin}/missing-fixture` });
|
|
const missingResponse = await waitFor("expected HTTP error", (value) => value?.status === 404, () => Promise.resolve(responses.find((response) => response.url.endsWith("/missing-fixture"))), 5000);
|
|
cases.push({ id: "expected-error", status: missingResponse.status === 404 ? "pass" : "fail", expected: { status: 404 }, observed: missingResponse, requests: [...requests], responses: [...responses], externalRequests: requests.filter((request) => !request.url.startsWith(origin)).map((request) => request.url), console: [...consoleEntries], exceptions: [...exceptions], screenshotSha256: null });
|
|
}
|
|
|
|
const allExternal = cases.flatMap((item) => item.externalRequests);
|
|
const allExceptions = cases.flatMap((item) => item.exceptions);
|
|
const result = {
|
|
schema: "freecad-web-p6-chrome-harness/v1",
|
|
result: cases.every((item) => item.status === "pass") && allExternal.length === 0 && allExceptions.length === 0 ? "pass" : "fail",
|
|
chrome: { path: chromePath, version: (await new Promise((resolve) => { const version = spawn(chromePath, ["--version"], { stdio: ["ignore", "pipe", "ignore"] }); let output = ""; version.stdout.on("data", (chunk) => { output += chunk; }); version.on("close", () => resolve(output.trim())); })) },
|
|
mode: "headless-new",
|
|
origin,
|
|
viewport,
|
|
timeoutMs,
|
|
networkPolicy: "intercept-and-require-zero-external-requests",
|
|
modulePreload,
|
|
cases,
|
|
evidence: { screenshot: `FreeCAD/docs/p6/${scenarioStem}.png`, screenshotSha256: cases.find((item) => item.screenshotSha256)?.screenshotSha256 ?? null, resultHash: sha256(JSON.stringify(cases)) },
|
|
policy: { browser: "Google Chrome only", nonChromeBrowsersAttempted: false },
|
|
};
|
|
fs.mkdirSync(path.dirname(outputPath), { recursive: true });
|
|
fs.writeFileSync(outputPath, `${JSON.stringify(result, null, 2)}\n`);
|
|
log.push(`origin=${origin}`, `chrome=${result.chrome.version}`, `cases=${cases.length}`, `externalRequests=${allExternal.length}`, `exceptions=${allExceptions.length}`, `result=${result.result}`);
|
|
fs.writeFileSync(logPath, `${log.join("\n")}\n`);
|
|
console.log(JSON.stringify({ result: result.result, cases: cases.length, externalRequests: allExternal.length, exceptions: allExceptions.length }));
|
|
if (result.result !== "pass") process.exitCode = 1;
|
|
} finally {
|
|
cdp?.close();
|
|
chrome.kill("SIGTERM");
|
|
server.close();
|
|
}
|
|
}
|