140 lines
6.6 KiB
JavaScript
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;
|