Files
FreeCADWasmDev/tools/run_web_offline_functional.mjs
wangdequan 9a7c4f0335 Save FreeCAD Web workspace sources and user validation evidence
IFC native binding remains incomplete; preserve accurate test results.
2026-09-15 22:01:58 +08:00

140 lines
6.6 KiB
JavaScript

import fs from "node:fs";
import path from "node:path";
import { fileURLToPath } from "node:url";
const toolsRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)));
const projectRoot = path.resolve(toolsRoot, "..");
const docsRoot = path.join(projectRoot, "docs");
const origin = "http://127.0.0.1:4174";
const sourceRevision = JSON.parse(fs.readFileSync(path.join(docsRoot, "freecad-web-offline-package-manifest.json"), "utf8")).sourceRevision;
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(`${message.error.code}: ${message.error.message}`));
else pending.resolve(message.result);
return;
}
for (const listener of this.listeners.get(message.method) ?? []) listener(message.params);
});
}
async send(method, params = {}) {
await this.ready;
const id = this.nextId++;
return new Promise((resolve, reject) => {
this.pending.set(id, { resolve, reject });
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();
}
}
const targets = await fetch("http://127.0.0.1:9223/json/list").then((response) => response.json());
const target = targets.find((item) => item.type === "page");
if (!target?.webSocketDebuggerUrl) throw new Error("no Chrome CDP page target found on port 9223");
const cdp = new CdpSession(target.webSocketDebuggerUrl);
const networkRequests = [];
const exceptions = [];
cdp.on("Network.requestWillBeSent", (event) => networkRequests.push({ url: event.request.url, method: event.request.method, type: event.type }));
cdp.on("Runtime.exceptionThrown", (event) => exceptions.push(event.exceptionDetails?.text ?? "runtime exception"));
await cdp.send("Runtime.enable");
await cdp.send("Network.enable");
await cdp.send("Page.enable");
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;
}
async function waitFor(label, predicate, timeoutMs = 30000) {
const deadline = Date.now() + timeoutMs;
let last;
while (Date.now() < deadline) {
last = await evaluate(`(() => { const status = document.querySelector('#status'); return { status: status?.textContent ?? '', level: 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 }; })()`);
if (predicate(last)) return last;
await new Promise((resolve) => setTimeout(resolve, 250));
}
throw new Error(`${label} timed out: ${JSON.stringify(last)}`);
}
async function navigate(pathname) {
networkRequests.length = 0;
exceptions.length = 0;
await cdp.send("Page.navigate", { url: `${origin}${pathname}` });
await waitFor("page startup", (state) => state.status !== "" && !state.status.includes("初始化"), 30000).catch(async (error) => {
const state = await evaluate("document.readyState");
throw new Error(`${error.message}; readyState=${state}`);
});
}
const cases = [];
await navigate("/?sample=1");
const importState = await waitFor("sample import", (state) => state.status.startsWith("已加载 "));
cases.push({
id: "sample-import",
status: "pass",
state: importState,
networkRequests: networkRequests.map((request) => request.url),
externalRequests: networkRequests.filter((request) => !request.url.startsWith(origin)).map((request) => request.url),
exceptions: [...exceptions],
});
await evaluate("document.querySelector('#reset-button').click()");
const resetState = await waitFor("reset", (state) => state.status === "已清空");
cases.push({ id: "reset", status: "pass", state: resetState, externalRequests: networkRequests.filter((request) => !request.url.startsWith(origin)).map((request) => request.url) });
await navigate("/?storage=1");
const storageState = await waitFor("OPFS smoke", (state) => state.status.includes("OPFS smoke passed"));
cases.push({ id: "opfs", status: "pass", state: storageState, externalRequests: networkRequests.filter((request) => !request.url.startsWith(origin)).map((request) => request.url), exceptions: [...exceptions] });
await navigate("/?cache=1");
const cacheState = await waitFor("mesh cache smoke", (state) => state.status === "网格缓存 smoke passed");
cases.push({ id: "mesh-cache", status: "pass", state: cacheState, externalRequests: networkRequests.filter((request) => !request.url.startsWith(origin)).map((request) => request.url), exceptions: [...exceptions] });
const result = {
schema: "freecad-web-offline-functional/v1",
sourceRevision,
package: "FreeCAD/web/offline-package",
origin,
browser: "Google Chrome 151.0.7922.137 headless",
cases,
summary: {
caseCount: cases.length,
passedCases: cases.filter((item) => item.status === "pass").length,
externalRequestCount: cases.reduce((total, item) => total + (item.externalRequests?.length ?? 0), 0),
exceptionCount: cases.reduce((total, item) => total + (item.exceptions?.length ?? 0), 0),
result: cases.every((item) => item.status === "pass" && item.externalRequests?.length === 0 && (item.exceptions?.length ?? 0) === 0)
? "pass_offline_core_functional_replay"
: "fail_offline_core_functional_replay",
},
};
fs.writeFileSync(path.join(docsRoot, "freecad-web-offline-functional.json"), `${JSON.stringify(result, null, 2)}\n`);
console.log(`offline cases passed: ${result.summary.passedCases}/${result.summary.caseCount}`);
console.log(`external requests: ${result.summary.externalRequestCount}; exceptions: ${result.summary.exceptionCount}`);
cdp.close();
if (result.summary.result.startsWith("fail")) process.exitCode = 1;