完善五轴 RTCP 仿真与验证资料
@@ -0,0 +1,550 @@
|
||||
import fs from "node:fs/promises";
|
||||
import http from "node:http";
|
||||
import path from "node:path";
|
||||
import { randomUUID } from "node:crypto";
|
||||
import puppeteer from "puppeteer-core";
|
||||
import { PNG } from "pngjs";
|
||||
|
||||
const REPO_ROOT = path.resolve("/home/meswork/cnc_wams");
|
||||
const QA_ROOT = path.join(REPO_ROOT, "qa/web-rtcp-5axis-site-test");
|
||||
const OUTPUT_DIR = path.join(QA_ROOT, "output");
|
||||
const EVIDENCE_SCOPE = "working7-manual-flow-evidence";
|
||||
const SCREENSHOT_DIR = path.join(OUTPUT_DIR, EVIDENCE_SCOPE);
|
||||
const CHROME_PATH = process.env.CHROME_PATH || process.env.CHROMIUM || "/usr/bin/google-chrome";
|
||||
const TARGET_URL = process.env.TARGET_URL || "";
|
||||
const APP_URL = process.env.APP_URL || "/web-rtcp-5axis-sim-plan/app/index.html";
|
||||
const JOB_ID = process.env.JOB_ID || `w7-${new Date().toISOString().replace(/[-:.TZ]/g, "").slice(0, 14)}-${randomUUID().slice(0, 8)}`;
|
||||
const REPORT_ID = process.env.REPORT_ID || `report-${JOB_ID}`;
|
||||
const REPORT_BASENAME = `${EVIDENCE_SCOPE}-report`;
|
||||
|
||||
await fs.mkdir(OUTPUT_DIR, { recursive: true });
|
||||
await fs.mkdir(SCREENSHOT_DIR, { recursive: true });
|
||||
|
||||
let server = null;
|
||||
let targetUrl = TARGET_URL;
|
||||
if (!targetUrl) {
|
||||
server = createStaticServer(REPO_ROOT);
|
||||
await new Promise((resolve) => server.listen(0, "127.0.0.1", resolve));
|
||||
const address = server.address();
|
||||
if (!address || typeof address === "string") throw new Error("failed to start static server");
|
||||
targetUrl = `http://127.0.0.1:${address.port}${APP_URL}`;
|
||||
}
|
||||
|
||||
const browser = await puppeteer.launch({
|
||||
headless: true,
|
||||
executablePath: CHROME_PATH,
|
||||
defaultViewport: { width: 1500, height: 1050, deviceScaleFactor: 1 },
|
||||
ignoreHTTPSErrors: true,
|
||||
args: [
|
||||
"--ignore-certificate-errors",
|
||||
"--disable-gpu",
|
||||
"--enable-webgl",
|
||||
"--use-angle=swiftshader",
|
||||
"--enable-unsafe-swiftshader",
|
||||
"--no-sandbox",
|
||||
],
|
||||
});
|
||||
|
||||
const page = await browser.newPage();
|
||||
const consoleErrors = [];
|
||||
const pageErrors = [];
|
||||
const networkErrors = [];
|
||||
page.on("console", (msg) => {
|
||||
if (msg.type() === "error") consoleErrors.push(msg.text());
|
||||
});
|
||||
page.on("pageerror", (error) => pageErrors.push(error.message));
|
||||
page.on("response", (response) => {
|
||||
if (response.status() >= 400) {
|
||||
networkErrors.push(`${response.status()} ${response.url()}`);
|
||||
}
|
||||
});
|
||||
|
||||
const report = {
|
||||
jobId: JOB_ID,
|
||||
reportId: REPORT_ID,
|
||||
evidenceScope: EVIDENCE_SCOPE,
|
||||
generatedAt: new Date().toISOString(),
|
||||
targetUrl,
|
||||
chromePath: CHROME_PATH,
|
||||
screenshotsDir: SCREENSHOT_DIR,
|
||||
steps: [],
|
||||
checks: [],
|
||||
consoleErrors,
|
||||
pageErrors,
|
||||
networkErrors,
|
||||
};
|
||||
|
||||
try {
|
||||
await page.goto(targetUrl, { waitUntil: "networkidle2", timeout: 60000 });
|
||||
await page.waitForSelector('[data-shell="gmoccapy-5axis"]', { timeout: 15000 });
|
||||
await page.waitForFunction(() => Boolean(window.webRtcp5AxisSimulation?.getState), { timeout: 15000 });
|
||||
await page.waitForFunction(() => document.querySelector("[data-five-axis-canvas]")?.dataset?.threeReady === "true", { timeout: 20000 });
|
||||
await windowReady();
|
||||
await captureStep("01-loaded", "Main shell loaded", "The gmoccapy shell, canvas, diagnostics, and public store API are ready.");
|
||||
|
||||
await click("power");
|
||||
await waitForState((state) => state.machine.powerOn === true && state.machine.taskState === "on", 10000, "power on");
|
||||
await click("HOME");
|
||||
await waitForState((state) => state.machine.allHomed === true && state.machine.mode === "manual", 10000, "home complete");
|
||||
await captureStep("02-powered-homed-manual", "POWER and HOME", "Machine is powered, homed, and in MANUAL mode.");
|
||||
|
||||
await click("mode-auto");
|
||||
await waitForState((state) => state.machine.mode === "auto" && state.machine.powerOn === true && state.machine.allHomed === true, 10000, "auto mode");
|
||||
await captureStep("03-auto-active", "AUTO mode active", "AUTO mode becomes active while power and home state are preserved.");
|
||||
|
||||
await click("mode-manual");
|
||||
await waitForState((state) => state.machine.mode === "manual" && state.machine.powerOn === true, 10000, "manual mode");
|
||||
const beforeJog = await getState();
|
||||
await click("JOG_X_POS");
|
||||
await waitForState((state) => Number(state.axisPose?.x || 0) > Number(beforeJog.axisPose?.x || 0), 10000, "X+ jog changes position");
|
||||
await captureStep("04-manual-jog-x", "Manual X+ jog", "X+ jog changes the X axis position in MANUAL mode.");
|
||||
|
||||
await click("mode-mdi");
|
||||
await waitForState((state) => state.machine.mode === "mdi", 10000, "mdi mode");
|
||||
await setMdiCommand("G90 X12.5 Y-4 Z1.25 F900");
|
||||
await submitMdiCommand();
|
||||
await waitForState((state) => (
|
||||
state.machine.mode === "mdi" &&
|
||||
/G90 X12\.5 Y-4 Z1\.25 F900/.test(state.machine.mdiCommand || state.operatorMessage || "")
|
||||
), 10000, "MDI command accepted");
|
||||
await captureStep("05-mdi-command", "MDI command", "MDI accepts a coordinate command and records the staged/executed command state.");
|
||||
|
||||
await click("mode-manual");
|
||||
await waitForState((state) => state.machine.mode === "manual", 10000, "manual before overrides");
|
||||
await click("rapid-override-up");
|
||||
await waitForState((state) => Number(state.feed.rapidOverride) === 110, 8000, "rapid override up");
|
||||
await click("rapid-override-reset");
|
||||
await waitForState((state) => Number(state.feed.rapidOverride) === 100, 8000, "rapid override reset");
|
||||
await click("feed-override-down");
|
||||
await waitForState((state) => Number(state.feed.feedOverride) === 90, 8000, "feed override down");
|
||||
await click("feed-override-reset");
|
||||
await waitForState((state) => Number(state.feed.feedOverride) === 100, 8000, "feed override reset");
|
||||
await click("ignore-limits");
|
||||
await waitForState((state) => state.gmoccapyGui.ignoreLimits === true, 8000, "ignore limits on");
|
||||
await click("block-delete");
|
||||
await waitForState((state) => state.gmoccapyGui.optionalBlocks === true, 8000, "block delete on");
|
||||
await click("optional-stop");
|
||||
await waitForState((state) => state.gmoccapyGui.optionalStop === true, 8000, "optional stop on");
|
||||
await captureStep("06-overrides-hal", "Overrides and HAL inputs", "Rapid/feed overrides reset to 100 and HAL input toggles are active.");
|
||||
|
||||
await click("spindle-forward");
|
||||
await waitForState((state) => state.spindle.enabled === true && state.spindle.direction === "forward", 8000, "spindle forward");
|
||||
await click("spindle-override-up");
|
||||
await waitForState((state) => Number(state.spindle.override) === 110, 8000, "spindle override up");
|
||||
await click("spindle-override-reset");
|
||||
await waitForState((state) => Number(state.spindle.override) === 100, 8000, "spindle override reset");
|
||||
await click("toggle-flood");
|
||||
await waitForState((state) => state.coolant.flood === true, 8000, "flood toggled on");
|
||||
await click("toggle-mist");
|
||||
await waitForState((state) => state.coolant.mist === true, 8000, "mist toggled on");
|
||||
await click("spindle-stop");
|
||||
await waitForState((state) => state.spindle.enabled === false && state.spindle.direction === "stop", 8000, "spindle stop");
|
||||
await captureStep("07-spindle-coolant", "Spindle and coolant", "Spindle, spindle override, flood, and mist controls obey the powered machine gate.");
|
||||
|
||||
const savedSnapshot = await page.evaluate(() => window.webRtcp5AxisSimulation.saveSession({
|
||||
sessionId: "working7-manual-flow",
|
||||
filename: "working7-session.json",
|
||||
}));
|
||||
await waitForState((state) => state.sessionPersistence.status === "saved", 10000, "session saved");
|
||||
await page.select('[data-action="select-profile"]', "gmoccapy-xyzab");
|
||||
await waitForState((state) => state.machineProfile === "gmoccapy-xyzab", 15000, "profile changed before restore");
|
||||
await page.evaluate(() => window.webRtcp5AxisSimulation.restoreSession({
|
||||
sessionId: "working7-manual-flow",
|
||||
filename: "working7-session.json",
|
||||
}));
|
||||
await waitForState((state) => state.sessionPersistence.status === "restored" && state.machineProfile === "xyzac-trt", 15000, "session restored");
|
||||
const restoredSession = await getState();
|
||||
report.sessionEvidence = {
|
||||
savedPath: savedSnapshot.path,
|
||||
savedStorageMode: savedSnapshot.storageMode,
|
||||
restoredPath: restoredSession.sessionPersistence.path,
|
||||
restoredStorageMode: restoredSession.sessionPersistence.storageMode,
|
||||
};
|
||||
await captureStep("08-session-restored", "Save and restore session", "Saved profile and control state are restored after switching to another profile.");
|
||||
|
||||
await captureStep("09-diagnostics", "Diagnostics", "Task policy, INI, Task/HAL, full boundary, gmoccapy communication, and HAL diagnostics are visible.");
|
||||
addChecks();
|
||||
report.status = report.checks.every((check) => check.pass) && pageErrors.length === 0 ? "PASS" : "FAIL";
|
||||
|
||||
const jsonPath = path.join(OUTPUT_DIR, `${REPORT_BASENAME}.json`);
|
||||
const pdfPath = path.join(OUTPUT_DIR, `${REPORT_BASENAME}.pdf`);
|
||||
report.jsonPath = jsonPath;
|
||||
report.pdfPath = pdfPath;
|
||||
await fs.writeFile(jsonPath, `${JSON.stringify(report, null, 2)}\n`, "utf8");
|
||||
await writePdfReport(pdfPath, report);
|
||||
|
||||
console.log(`working7_manual_flow_status=${report.status}`);
|
||||
console.log(`working7_manual_flow_job_id=${report.jobId}`);
|
||||
console.log(`working7_manual_flow_report_id=${report.reportId}`);
|
||||
console.log(`working7_manual_flow_json=${jsonPath}`);
|
||||
console.log(`working7_manual_flow_pdf=${pdfPath}`);
|
||||
console.log(`working7_manual_flow_screenshots=${SCREENSHOT_DIR}`);
|
||||
if (report.status !== "PASS") process.exitCode = 1;
|
||||
} finally {
|
||||
await page.close().catch(() => {});
|
||||
await browser.close().catch(() => {});
|
||||
if (server) await new Promise((resolve) => server.close(resolve));
|
||||
}
|
||||
|
||||
async function windowReady() {
|
||||
await waitForState((state) => (
|
||||
state.kinematicsRuntimeReadiness?.loaded === true &&
|
||||
state.interpreterRuntimeReadiness?.loaded === true &&
|
||||
state.taskHalRuntimeReadiness?.loaded === true &&
|
||||
state.machineFileStaging?.status === "staged"
|
||||
), 30000, "runtime and machine files ready");
|
||||
}
|
||||
|
||||
async function captureStep(name, title, description) {
|
||||
const screenshotPath = path.join(SCREENSHOT_DIR, `${name}.png`);
|
||||
await page.screenshot({ path: screenshotPath, fullPage: true });
|
||||
const [state, dom, pixelStats] = await Promise.all([
|
||||
getState(),
|
||||
getDomEvidence(),
|
||||
analyzePng(screenshotPath),
|
||||
]);
|
||||
const step = {
|
||||
name,
|
||||
title,
|
||||
description,
|
||||
screenshotPath,
|
||||
pixelStats,
|
||||
state: summarizeState(state),
|
||||
dom,
|
||||
};
|
||||
report.steps.push(step);
|
||||
return step;
|
||||
}
|
||||
|
||||
async function getDomEvidence() {
|
||||
return page.evaluate(() => {
|
||||
const text = (selector) => document.querySelector(selector)?.textContent?.trim() || "";
|
||||
const button = (action) => {
|
||||
const element = document.querySelector(`[data-action="${action}"]`);
|
||||
return {
|
||||
exists: Boolean(element),
|
||||
disabled: Boolean(element?.disabled),
|
||||
active: element?.dataset?.active || null,
|
||||
commandReady: element?.dataset?.commandReady || null,
|
||||
title: element?.getAttribute("title") || "",
|
||||
};
|
||||
};
|
||||
return {
|
||||
regions: window.webRtcp5AxisSimulation.getRegions?.() || null,
|
||||
canvas: { ...(document.querySelector("[data-five-axis-canvas]")?.dataset || {}) },
|
||||
sidebar: {
|
||||
power: button("power"),
|
||||
manual: button("mode-manual"),
|
||||
auto: button("mode-auto"),
|
||||
mdi: button("mode-mdi"),
|
||||
},
|
||||
bottom: {
|
||||
home: button("HOME"),
|
||||
jogXPlus: button("JOG_X_POS"),
|
||||
mdiRun: button("MDI_RUN"),
|
||||
},
|
||||
values: {
|
||||
rapidOverride: text('[data-value="rapid-override"]'),
|
||||
feedOverride: text('[data-value="feed-override"]'),
|
||||
spindleOverride: text('[data-value="spindle-override"]'),
|
||||
halLast: text('[data-value="gmoccapy-hal-last"]'),
|
||||
session: text('[data-session-persistence="status"]'),
|
||||
taskGates: text('[data-linuxcnc-task-policy="gates"]'),
|
||||
taskHal: text('[data-task-hal-runtime="readiness"]'),
|
||||
gmoccapyHal: text('[data-gmoccapy-hal="boundary"]'),
|
||||
operatorMessage: text("[data-operator-message]"),
|
||||
},
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
async function setMdiCommand(command) {
|
||||
await page.$eval('[data-action="mdi-command"]', (input, value) => {
|
||||
input.value = value;
|
||||
input.dispatchEvent(new Event("input", { bubbles: true }));
|
||||
input.dispatchEvent(new Event("change", { bubbles: true }));
|
||||
}, command);
|
||||
await waitForState((state) => state.machine.mdiCommand === command.toUpperCase(), 5000, "MDI input staged");
|
||||
}
|
||||
|
||||
async function submitMdiCommand() {
|
||||
await page.$eval('[data-action="mdi-form"]', (form) => {
|
||||
form.dispatchEvent(new Event("submit", { bubbles: true, cancelable: true }));
|
||||
});
|
||||
}
|
||||
|
||||
async function click(action) {
|
||||
await page.evaluate((selector) => {
|
||||
const element = document.querySelector(selector);
|
||||
if (!element) throw new Error(`missing action ${selector}`);
|
||||
element.click();
|
||||
}, `[data-action="${action}"]`);
|
||||
}
|
||||
|
||||
async function getState() {
|
||||
return page.evaluate(() => JSON.parse(JSON.stringify(window.webRtcp5AxisSimulation.getState())));
|
||||
}
|
||||
|
||||
async function waitForState(predicate, timeoutMs, label) {
|
||||
const started = Date.now();
|
||||
let lastState = null;
|
||||
while (Date.now() - started < timeoutMs) {
|
||||
lastState = await getState();
|
||||
if (predicate(lastState)) return lastState;
|
||||
await wait(50);
|
||||
}
|
||||
throw new Error(`timeout waiting for ${label}: ${JSON.stringify(summarizeState(lastState || {}))}`);
|
||||
}
|
||||
|
||||
function addChecks() {
|
||||
const byName = Object.fromEntries(report.steps.map((step) => [step.name, step]));
|
||||
const loaded = byName["01-loaded"]?.state;
|
||||
const manual = byName["02-powered-homed-manual"]?.state;
|
||||
const auto = byName["03-auto-active"]?.state;
|
||||
const jog = byName["04-manual-jog-x"]?.state;
|
||||
const mdi = byName["05-mdi-command"]?.state;
|
||||
const overrides = byName["06-overrides-hal"]?.state;
|
||||
const spindleCoolant = byName["07-spindle-coolant"]?.state;
|
||||
const restored = byName["08-session-restored"]?.state;
|
||||
const diagnostics = byName["09-diagnostics"]?.dom;
|
||||
|
||||
report.checks.push(
|
||||
check("Main shell regions and canvas are ready", Object.values(byName["01-loaded"]?.dom?.regions || {}).every(Boolean) && byName["01-loaded"]?.dom?.canvas?.threeReady === "true", JSON.stringify({ regions: byName["01-loaded"]?.dom?.regions, canvas: byName["01-loaded"]?.dom?.canvas })),
|
||||
check("POWER and HOME leave MANUAL ready", manual?.machine?.powerOn === true && manual?.machine?.allHomed === true && manual?.machine?.mode === "manual", JSON.stringify(manual?.machine)),
|
||||
check("AUTO activates without losing power/home", auto?.machine?.mode === "auto" && auto?.machine?.powerOn === true && auto?.machine?.allHomed === true, JSON.stringify(auto?.machine)),
|
||||
check("Manual jog changes X", Number(jog?.axisPose?.x || 0) > Number(manual?.axisPose?.x || 0), JSON.stringify({ before: manual?.axisPose, after: jog?.axisPose })),
|
||||
check("MDI command is accepted in MDI mode", mdi?.machine?.mode === "mdi" && /G90 X12\.5 Y-4 Z1\.25 F900/.test(`${mdi?.machine?.mdiCommand || ""} ${mdi?.operatorMessage || ""}`), JSON.stringify({ machine: mdi?.machine, operatorMessage: mdi?.operatorMessage })),
|
||||
check("Overrides reset and HAL toggles are active", overrides?.feed?.rapidOverride === 100 && overrides?.feed?.feedOverride === 100 && overrides?.gmoccapyGui?.ignoreLimits === true && overrides?.gmoccapyGui?.optionalBlocks === true && overrides?.gmoccapyGui?.optionalStop === true, JSON.stringify({ feed: overrides?.feed, gmoccapyGui: overrides?.gmoccapyGui })),
|
||||
check("Spindle and coolant controls update state", spindleCoolant?.spindle?.direction === "stop" && spindleCoolant?.spindle?.override === 100 && spindleCoolant?.coolant?.flood === true && spindleCoolant?.coolant?.mist === true, JSON.stringify({ spindle: spindleCoolant?.spindle, coolant: spindleCoolant?.coolant })),
|
||||
check("Session restore returns to saved profile", restored?.machineProfile === "xyzac-trt" && restored?.sessionPersistence?.status === "restored", JSON.stringify({ machineProfile: restored?.machineProfile, sessionPersistence: restored?.sessionPersistence, evidence: report.sessionEvidence })),
|
||||
check("Diagnostics expose Task/HAL and gmoccapy HAL", Boolean(diagnostics?.values?.taskHal) && Boolean(diagnostics?.values?.gmoccapyHal), JSON.stringify(diagnostics?.values)),
|
||||
check("Screenshots are nonblank", report.steps.every((step) => step.pixelStats.nonBlackRatio > 0.1), report.steps.map((step) => `${step.name}:${step.pixelStats.nonBlackRatio}`).join(", ")),
|
||||
);
|
||||
}
|
||||
|
||||
async function writePdfReport(pdfPath, data) {
|
||||
const reportPage = await browser.newPage();
|
||||
const rows = data.checks.map((item) => `
|
||||
<tr>
|
||||
<td>${escapeHtml(item.name)}</td>
|
||||
<td class="${item.pass ? "pass" : "fail"}">${item.pass ? "PASS" : "FAIL"}</td>
|
||||
<td>${escapeHtml(item.detail)}</td>
|
||||
</tr>
|
||||
`).join("");
|
||||
const steps = data.steps.map((step) => `
|
||||
<section>
|
||||
<h2>${escapeHtml(step.name)} - ${escapeHtml(step.title)}</h2>
|
||||
<p>${escapeHtml(step.description)}</p>
|
||||
<p><strong>Screenshot:</strong> ${escapeHtml(step.screenshotPath)}</p>
|
||||
<p><strong>State:</strong> ${escapeHtml(JSON.stringify({
|
||||
machineProfile: step.state.machineProfile,
|
||||
runState: step.state.runState,
|
||||
machine: step.state.machine,
|
||||
axisPose: step.state.axisPose,
|
||||
feed: step.state.feed,
|
||||
spindle: step.state.spindle,
|
||||
coolant: step.state.coolant,
|
||||
sessionPersistence: step.state.sessionPersistence,
|
||||
}))}</p>
|
||||
</section>
|
||||
`).join("");
|
||||
await reportPage.setContent(`<!doctype html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<style>
|
||||
body { font-family: Arial, sans-serif; margin: 28px; color: #17202a; }
|
||||
h1 { font-size: 22px; margin-bottom: 6px; }
|
||||
h2 { font-size: 16px; margin-top: 18px; }
|
||||
table { border-collapse: collapse; width: 100%; margin-top: 14px; }
|
||||
th, td { border: 1px solid #9aa5b1; padding: 6px; font-size: 11px; vertical-align: top; }
|
||||
th { background: #eef2f7; }
|
||||
.pass { color: #126b37; font-weight: 700; }
|
||||
.fail { color: #a61b1b; font-weight: 700; }
|
||||
.meta { font-size: 12px; line-height: 1.45; }
|
||||
section { break-inside: avoid; border-top: 1px solid #d8dee6; padding-top: 8px; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<h1>working7 Manual Flow Evidence</h1>
|
||||
<div class="meta">
|
||||
<div><strong>Status:</strong> ${escapeHtml(data.status)}</div>
|
||||
<div><strong>Job ID:</strong> ${escapeHtml(data.jobId)}</div>
|
||||
<div><strong>Report ID:</strong> ${escapeHtml(data.reportId)}</div>
|
||||
<div><strong>Target:</strong> ${escapeHtml(data.targetUrl)}</div>
|
||||
<div><strong>Generated:</strong> ${escapeHtml(data.generatedAt)}</div>
|
||||
<div><strong>Screenshots:</strong> ${escapeHtml(data.screenshotsDir)}</div>
|
||||
</div>
|
||||
<h2>Checks</h2>
|
||||
<table>
|
||||
<thead><tr><th>Check</th><th>Status</th><th>Evidence</th></tr></thead>
|
||||
<tbody>${rows}</tbody>
|
||||
</table>
|
||||
<h2>Steps</h2>
|
||||
${steps}
|
||||
</body>
|
||||
</html>`, { waitUntil: "load" });
|
||||
await reportPage.pdf({
|
||||
path: pdfPath,
|
||||
format: "A4",
|
||||
printBackground: true,
|
||||
margin: { top: "12mm", right: "10mm", bottom: "12mm", left: "10mm" },
|
||||
});
|
||||
await reportPage.close();
|
||||
}
|
||||
|
||||
function summarizeState(state = {}) {
|
||||
return {
|
||||
machineProfile: state.machineProfile,
|
||||
activeProgram: state.activeProgram,
|
||||
runState: state.runState,
|
||||
activeLine: state.activeLine,
|
||||
machine: {
|
||||
powerOn: state.machine?.powerOn,
|
||||
taskState: state.machine?.taskState,
|
||||
mode: state.machine?.mode,
|
||||
allHomed: state.machine?.allHomed,
|
||||
interpState: state.machine?.interpState,
|
||||
taskPaused: state.machine?.taskPaused,
|
||||
mdiCommand: state.machine?.mdiCommand,
|
||||
},
|
||||
axisPose: pickAxes(state.axisPose),
|
||||
dro: pickAxes(state.dro),
|
||||
feed: {
|
||||
rapidOverride: Number(state.feed?.rapidOverride),
|
||||
feedOverride: Number(state.feed?.feedOverride),
|
||||
feedRate: Number(state.feed?.feedRate),
|
||||
currentVelocity: Number(state.feed?.currentVelocity),
|
||||
},
|
||||
spindle: {
|
||||
enabled: Boolean(state.spindle?.enabled),
|
||||
direction: state.spindle?.direction,
|
||||
override: Number(state.spindle?.override),
|
||||
rpm: Number(state.spindle?.rpm),
|
||||
},
|
||||
coolant: {
|
||||
flood: Boolean(state.coolant?.flood),
|
||||
mist: Boolean(state.coolant?.mist),
|
||||
},
|
||||
gmoccapyGui: {
|
||||
ignoreLimits: Boolean(state.gmoccapyGui?.ignoreLimits),
|
||||
optionalBlocks: Boolean(state.gmoccapyGui?.optionalBlocks),
|
||||
optionalStop: Boolean(state.gmoccapyGui?.optionalStop),
|
||||
lastHalPinEffect: state.gmoccapyGui?.lastHalPinEffect,
|
||||
},
|
||||
mdiHistory: state.mdiHistory || [],
|
||||
sessionPersistence: {
|
||||
status: state.sessionPersistence?.status,
|
||||
storageMode: state.sessionPersistence?.storageMode,
|
||||
path: state.sessionPersistence?.path,
|
||||
savedAt: state.sessionPersistence?.savedAt,
|
||||
restoredAt: state.sessionPersistence?.restoredAt,
|
||||
},
|
||||
machineFileStaging: {
|
||||
status: state.machineFileStaging?.status,
|
||||
fileCount: state.machineFileStaging?.fileCount,
|
||||
selectedGcodeSourceRel: state.machineFileStaging?.selectedGcodeSourceRel,
|
||||
},
|
||||
taskHalRuntimeReadiness: {
|
||||
loaded: Boolean(state.taskHalRuntimeReadiness?.loaded),
|
||||
halSyncReady: Boolean(state.taskHalRuntimeReadiness?.halSyncReady),
|
||||
},
|
||||
operatorMessage: state.operatorMessage,
|
||||
};
|
||||
}
|
||||
|
||||
function createStaticServer(rootDir) {
|
||||
return http.createServer(async (request, response) => {
|
||||
try {
|
||||
const requestPath = decodeURIComponent(new URL(request.url || "/", "http://127.0.0.1").pathname);
|
||||
const relativePath = requestPath === "/" ? "/index.html" : requestPath;
|
||||
const targetPath = path.resolve(rootDir, `.${relativePath}`);
|
||||
if (!targetPath.startsWith(rootDir)) {
|
||||
response.writeHead(403);
|
||||
response.end("forbidden");
|
||||
return;
|
||||
}
|
||||
let filePath = targetPath;
|
||||
let stat = await fs.stat(filePath).catch(() => null);
|
||||
if (stat?.isDirectory()) {
|
||||
filePath = path.join(filePath, "index.html");
|
||||
stat = await fs.stat(filePath).catch(() => null);
|
||||
}
|
||||
if (!stat?.isFile()) {
|
||||
response.writeHead(404);
|
||||
response.end("not found");
|
||||
return;
|
||||
}
|
||||
const body = await fs.readFile(filePath);
|
||||
response.writeHead(200, {
|
||||
"Content-Type": contentTypeFor(filePath),
|
||||
"Content-Length": String(body.byteLength),
|
||||
"Cache-Control": "no-store",
|
||||
});
|
||||
response.end(body);
|
||||
} catch (error) {
|
||||
response.writeHead(500);
|
||||
response.end(error instanceof Error ? error.message : String(error));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function contentTypeFor(filePath) {
|
||||
const ext = path.extname(filePath).toLowerCase();
|
||||
return {
|
||||
".css": "text/css; charset=utf-8",
|
||||
".html": "text/html; charset=utf-8",
|
||||
".js": "text/javascript; charset=utf-8",
|
||||
".json": "application/json; charset=utf-8",
|
||||
".mjs": "text/javascript; charset=utf-8",
|
||||
".svg": "image/svg+xml",
|
||||
".wasm": "application/wasm",
|
||||
".xml": "application/xml; charset=utf-8",
|
||||
}[ext] || "application/octet-stream";
|
||||
}
|
||||
|
||||
async function analyzePng(filePath) {
|
||||
const png = PNG.sync.read(await fs.readFile(filePath));
|
||||
let luminanceSum = 0;
|
||||
let nonBlack = 0;
|
||||
for (let index = 0; index < png.data.length; index += 4) {
|
||||
const luminance = png.data[index] * 0.2126 + png.data[index + 1] * 0.7152 + png.data[index + 2] * 0.0722;
|
||||
luminanceSum += luminance;
|
||||
if (luminance > 8) nonBlack += 1;
|
||||
}
|
||||
const total = png.width * png.height;
|
||||
return {
|
||||
width: png.width,
|
||||
height: png.height,
|
||||
averageLuminance: Number((luminanceSum / total).toFixed(2)),
|
||||
nonBlackRatio: Number((nonBlack / total).toFixed(4)),
|
||||
};
|
||||
}
|
||||
|
||||
function pickAxes(value = {}) {
|
||||
return {
|
||||
x: Number(value?.x || 0),
|
||||
y: Number(value?.y || 0),
|
||||
z: Number(value?.z || 0),
|
||||
a: Number(value?.a || 0),
|
||||
b: Number(value?.b || 0),
|
||||
c: Number(value?.c || 0),
|
||||
};
|
||||
}
|
||||
|
||||
function check(name, pass, detail) {
|
||||
return { name, pass: Boolean(pass), detail: String(detail ?? "-") };
|
||||
}
|
||||
|
||||
function escapeHtml(value) {
|
||||
return String(value ?? "")
|
||||
.replace(/&/g, "&")
|
||||
.replace(/</g, "<")
|
||||
.replace(/>/g, ">")
|
||||
.replace(/"/g, """)
|
||||
.replace(/'/g, "'");
|
||||
}
|
||||
|
||||
function wait(ms) {
|
||||
return new Promise((resolve) => setTimeout(resolve, ms));
|
||||
}
|
||||
|
After Width: | Height: | Size: 256 KiB |
|
After Width: | Height: | Size: 256 KiB |
|
After Width: | Height: | Size: 257 KiB |
|
After Width: | Height: | Size: 257 KiB |
|
After Width: | Height: | Size: 256 KiB |
|
After Width: | Height: | Size: 256 KiB |
|
After Width: | Height: | Size: 258 KiB |
|
After Width: | Height: | Size: 255 KiB |
|
After Width: | Height: | Size: 239 KiB |
|
After Width: | Height: | Size: 243 KiB |
|
After Width: | Height: | Size: 250 KiB |
|
After Width: | Height: | Size: 250 KiB |
|
After Width: | Height: | Size: 249 KiB |
|
After Width: | Height: | Size: 251 KiB |
|
After Width: | Height: | Size: 239 KiB |
|
After Width: | Height: | Size: 236 KiB |
|
After Width: | Height: | Size: 235 KiB |
|
After Width: | Height: | Size: 235 KiB |
|
After Width: | Height: | Size: 235 KiB |
|
After Width: | Height: | Size: 241 KiB |
|
After Width: | Height: | Size: 242 KiB |
|
After Width: | Height: | Size: 242 KiB |
|
After Width: | Height: | Size: 243 KiB |
|
After Width: | Height: | Size: 229 KiB |
|
After Width: | Height: | Size: 224 KiB |
|
After Width: | Height: | Size: 223 KiB |
|
After Width: | Height: | Size: 223 KiB |
|
After Width: | Height: | Size: 223 KiB |
@@ -4,7 +4,9 @@
|
||||
"description": "",
|
||||
"main": "index.js",
|
||||
"scripts": {
|
||||
"test": "echo \"Error: no test specified\" && exit 1"
|
||||
"evidence:working7": "node capture-working7-manual-flow-evidence.mjs",
|
||||
"evidence:working7:full": "node capture-working7-full-functional-evidence.mjs",
|
||||
"test": "node run-site-test.mjs"
|
||||
},
|
||||
"keywords": [],
|
||||
"author": "",
|
||||
|
||||
@@ -1,17 +1,72 @@
|
||||
import fs from "node:fs/promises";
|
||||
import http from "node:http";
|
||||
import path from "node:path";
|
||||
import puppeteer from "puppeteer-core";
|
||||
import { PNG } from "pngjs";
|
||||
|
||||
const REPO_ROOT = path.resolve("/home/meswork/cnc_wams");
|
||||
const ROOT = path.resolve("/home/meswork/cnc_wams/qa/web-rtcp-5axis-site-test");
|
||||
const OUTPUT_DIR = path.join(ROOT, "output");
|
||||
const SCREENSHOT_DIR = path.join(ROOT, "screenshots");
|
||||
const URL = "https://82.156.24.101:8092/";
|
||||
const CHROME_PATH = "/usr/bin/google-chrome";
|
||||
const TARGET_URL = process.env.TARGET_URL || "";
|
||||
const APP_URL = process.env.APP_URL || "/web-rtcp-5axis-sim-plan/app/index.html";
|
||||
const CHROME_PATH = process.env.CHROME_PATH || process.env.CHROMIUM || "/usr/bin/google-chrome";
|
||||
|
||||
const localProgramPath = path.join(ROOT, "fixtures", "test-program.ngc");
|
||||
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
||||
|
||||
const MIME_TYPES = {
|
||||
".css": "text/css; charset=utf-8",
|
||||
".html": "text/html; charset=utf-8",
|
||||
".js": "text/javascript; charset=utf-8",
|
||||
".json": "application/json; charset=utf-8",
|
||||
".mjs": "text/javascript; charset=utf-8",
|
||||
".png": "image/png",
|
||||
".svg": "image/svg+xml",
|
||||
".wasm": "application/wasm",
|
||||
".xml": "application/xml; charset=utf-8",
|
||||
};
|
||||
|
||||
function contentTypeFor(filePath) {
|
||||
return MIME_TYPES[path.extname(filePath).toLowerCase()] || "application/octet-stream";
|
||||
}
|
||||
|
||||
function createStaticServer(rootDir) {
|
||||
return http.createServer(async (request, response) => {
|
||||
try {
|
||||
const requestPath = decodeURIComponent(new URL(request.url || "/", "http://127.0.0.1").pathname);
|
||||
const relativePath = requestPath === "/" ? "/index.html" : requestPath;
|
||||
const targetPath = path.resolve(rootDir, `.${relativePath}`);
|
||||
if (!targetPath.startsWith(rootDir)) {
|
||||
response.writeHead(403);
|
||||
response.end("forbidden");
|
||||
return;
|
||||
}
|
||||
let stat = await fs.stat(targetPath).catch(() => null);
|
||||
let filePath = targetPath;
|
||||
if (stat?.isDirectory()) {
|
||||
filePath = path.join(targetPath, "index.html");
|
||||
stat = await fs.stat(filePath).catch(() => null);
|
||||
}
|
||||
if (!stat?.isFile()) {
|
||||
response.writeHead(404);
|
||||
response.end("not found");
|
||||
return;
|
||||
}
|
||||
const body = await fs.readFile(filePath);
|
||||
response.writeHead(200, {
|
||||
"Content-Type": contentTypeFor(filePath),
|
||||
"Content-Length": String(body.byteLength),
|
||||
"Cache-Control": "no-store",
|
||||
});
|
||||
response.end(body);
|
||||
} catch (error) {
|
||||
response.writeHead(500);
|
||||
response.end(error instanceof Error ? error.message : String(error));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
await fs.mkdir(OUTPUT_DIR, { recursive: true });
|
||||
await fs.mkdir(SCREENSHOT_DIR, { recursive: true });
|
||||
await fs.mkdir(path.dirname(localProgramPath), { recursive: true });
|
||||
@@ -20,6 +75,17 @@ const findings = [];
|
||||
const consoleLogs = [];
|
||||
const pageErrors = [];
|
||||
const requestFailures = [];
|
||||
let server = null;
|
||||
let targetUrl = TARGET_URL;
|
||||
if (!targetUrl) {
|
||||
server = createStaticServer(REPO_ROOT);
|
||||
await new Promise((resolve) => server.listen(0, "127.0.0.1", resolve));
|
||||
const address = server.address();
|
||||
if (!address || typeof address === "string") {
|
||||
throw new Error("failed to start local static server");
|
||||
}
|
||||
targetUrl = `http://127.0.0.1:${address.port}${APP_URL}`;
|
||||
}
|
||||
|
||||
const browser = await puppeteer.launch({
|
||||
headless: true,
|
||||
@@ -83,7 +149,7 @@ try {
|
||||
"",
|
||||
].join("\n"), "utf8");
|
||||
|
||||
await page.goto(URL, { waitUntil: "networkidle2", timeout: 60000 });
|
||||
await page.goto(targetUrl, { waitUntil: "networkidle2", timeout: 60000 });
|
||||
await page.waitForSelector('[data-shell="gmoccapy-5axis"]', { timeout: 15000 });
|
||||
await page.waitForFunction(() => Boolean(window.webRtcp5AxisSimulation?.getState), { timeout: 15000 });
|
||||
await page.waitForFunction(() => {
|
||||
@@ -140,6 +206,34 @@ try {
|
||||
).catch(() => null);
|
||||
}
|
||||
|
||||
async function waitForTaskHalReady(timeoutMs = 30000) {
|
||||
return waitForState((state) => (
|
||||
state.taskHalRuntimeReadiness?.loaded === true &&
|
||||
state.taskHalRuntimeReadiness?.taskRuntimeReady === true &&
|
||||
state.taskHalRuntimeReadiness?.motionRuntimeReady === true &&
|
||||
state.taskHalRuntimeReadiness?.halRuntimeReady === true &&
|
||||
state.taskHalRuntimeReadiness?.halSyncReady === true &&
|
||||
!state.taskHalExecutionPending &&
|
||||
!state.interpreterExecutionPending
|
||||
), timeoutMs, "Task/HAL ready and app idle").catch(() => null);
|
||||
}
|
||||
|
||||
async function clickJogAndWait(action, axis, direction, beforeValue) {
|
||||
const selector = `[data-action="${action}"]`;
|
||||
await waitForTaskHalReady(30000);
|
||||
for (let attempt = 0; attempt < 2; attempt += 1) {
|
||||
await clickAndWait(selector, 900);
|
||||
const changed = await waitForState((nextState) => {
|
||||
const nextValue = Number(nextState.axisPose?.[axis]);
|
||||
return direction > 0
|
||||
? nextValue > Number(beforeValue)
|
||||
: nextValue < Number(beforeValue);
|
||||
}, 6000, `${action} axis change`).catch(() => null);
|
||||
if (changed) return changed;
|
||||
}
|
||||
return getState();
|
||||
}
|
||||
|
||||
async function getSummary() {
|
||||
return page.evaluate(() => {
|
||||
const state = window.webRtcp5AxisSimulation.getState();
|
||||
@@ -179,12 +273,31 @@ try {
|
||||
}
|
||||
|
||||
async function clickAndWait(selector, waitMs = 600) {
|
||||
await page.click(selector);
|
||||
await page.waitForSelector(selector, { timeout: 10000 });
|
||||
await page.waitForFunction((targetSelector) => {
|
||||
const element = document.querySelector(targetSelector);
|
||||
if (!element) return false;
|
||||
const ariaDisabled = element.getAttribute("aria-disabled") === "true";
|
||||
const commandReady = element.getAttribute("data-command-ready") === "false";
|
||||
return !element.disabled && !ariaDisabled && !commandReady;
|
||||
}, { timeout: 15000 }, selector);
|
||||
await page.evaluate((targetSelector) => {
|
||||
const element = document.querySelector(targetSelector);
|
||||
if (!element) {
|
||||
throw new Error(`missing click target ${targetSelector}`);
|
||||
}
|
||||
if (element.disabled || element.getAttribute("aria-disabled") === "true") {
|
||||
throw new Error(`disabled click target ${targetSelector}`);
|
||||
}
|
||||
element.scrollIntoView({ block: "center", inline: "center" });
|
||||
element.click();
|
||||
}, selector);
|
||||
await sleep(waitMs);
|
||||
await waitForAppIdle();
|
||||
}
|
||||
|
||||
async function setInputValue(selector, value) {
|
||||
await page.waitForSelector(selector, { timeout: 10000 });
|
||||
await page.$eval(selector, (el, nextValue) => {
|
||||
el.value = nextValue;
|
||||
el.dispatchEvent(new Event("input", { bubbles: true }));
|
||||
@@ -193,6 +306,21 @@ try {
|
||||
await sleep(400);
|
||||
}
|
||||
|
||||
async function selectValue(selector, value) {
|
||||
await page.waitForSelector(selector, { timeout: 10000 });
|
||||
await page.evaluate((targetSelector, nextValue) => {
|
||||
const element = document.querySelector(targetSelector);
|
||||
if (!element) {
|
||||
throw new Error(`missing select target ${targetSelector}`);
|
||||
}
|
||||
element.value = nextValue;
|
||||
element.dispatchEvent(new Event("input", { bubbles: true }));
|
||||
element.dispatchEvent(new Event("change", { bubbles: true }));
|
||||
}, selector, value);
|
||||
await sleep(400);
|
||||
await waitForAppIdle();
|
||||
}
|
||||
|
||||
function classify(condition, passText, failText) {
|
||||
return condition ? { status: "PASS", text: passText } : { status: "FAIL", text: failText };
|
||||
}
|
||||
@@ -253,10 +381,11 @@ try {
|
||||
initialBoundaryStatus.status,
|
||||
);
|
||||
|
||||
const taskHalReadyState = await waitForTaskHalReady(30000);
|
||||
const initialTaskHalStatus = classifyWarn(
|
||||
!/pending|blocked/i.test(initial.taskHal),
|
||||
`Task/HAL 已就绪:${initial.taskHal}`,
|
||||
`Task/HAL 未完成就绪:${initial.taskHal}`,
|
||||
Boolean(taskHalReadyState),
|
||||
`Task/HAL 已就绪:${taskHalReadyState?.taskHalRuntimeReadiness?.semanticBoundary || initial.taskHal}`,
|
||||
`Task/HAL 冷启动时尚未完成就绪:${initial.taskHal}`,
|
||||
);
|
||||
await recordResult(
|
||||
"initial-task-hal",
|
||||
@@ -290,6 +419,7 @@ try {
|
||||
|
||||
await clickAndWait('[data-action="HOME"]');
|
||||
await waitForState((nextState) => nextState.machine.allHomed === true, 8000, "machine homed").catch(() => null);
|
||||
await waitForTaskHalReady(30000);
|
||||
state = await getState();
|
||||
await recordResult(
|
||||
"home",
|
||||
@@ -300,8 +430,7 @@ try {
|
||||
);
|
||||
|
||||
const beforeJogX = state.axisPose.x;
|
||||
await clickAndWait('[data-action="JOG_X_POS"]');
|
||||
state = await getState();
|
||||
state = await clickJogAndWait("JOG_X_POS", "x", 1, beforeJogX);
|
||||
await recordResult(
|
||||
"jog-x-plus",
|
||||
"JOG X+",
|
||||
@@ -311,8 +440,7 @@ try {
|
||||
);
|
||||
|
||||
const beforeJogY = state.axisPose.y;
|
||||
await clickAndWait('[data-action="JOG_Y_NEG"]');
|
||||
state = await getState();
|
||||
state = await clickJogAndWait("JOG_Y_NEG", "y", -1, beforeJogY);
|
||||
await recordResult(
|
||||
"jog-y-minus",
|
||||
"JOG Y-",
|
||||
@@ -332,6 +460,9 @@ try {
|
||||
state.machine.mode === "auto" ? "PASS" : "FAIL",
|
||||
);
|
||||
|
||||
await clickAndWait('[data-action="mode-manual"]');
|
||||
await waitForState((nextState) => nextState.machine.mode === "manual" && nextState.machine.interpState === "idle", 10000, "MANUAL idle before MDI").catch(() => null);
|
||||
await waitForTaskHalReady(30000);
|
||||
await clickAndWait('[data-action="mode-mdi"]');
|
||||
await waitForState((nextState) => nextState.machine.mode === "mdi", 8000, "MDI mode").catch(() => null);
|
||||
state = await getState();
|
||||
@@ -343,7 +474,17 @@ try {
|
||||
state.machine.mode === "mdi" ? "PASS" : "FAIL",
|
||||
);
|
||||
|
||||
if (state.machine.mode !== "mdi") {
|
||||
await clickAndWait('[data-action="mode-manual"]');
|
||||
await waitForState((nextState) => nextState.machine.mode === "manual", 10000, "retry manual before MDI").catch(() => null);
|
||||
await waitForTaskHalReady(30000);
|
||||
await clickAndWait('[data-action="mode-mdi"]');
|
||||
await waitForState((nextState) => nextState.machine.mode === "mdi", 10000, "retry MDI mode").catch(() => null);
|
||||
state = await getState();
|
||||
}
|
||||
|
||||
await setInputValue('[data-action="mdi-command"]', "M428");
|
||||
await waitForState((nextState) => nextState.machine.mode === "mdi", 10000, "MDI active before M428");
|
||||
await clickAndWait('[data-action="mdi-submit"]', 800);
|
||||
state = await getState();
|
||||
await recordResult(
|
||||
@@ -493,7 +634,7 @@ try {
|
||||
fullOn && stateAfterFullOff.preview.fullscreen === false ? "PASS" : "FAIL",
|
||||
);
|
||||
|
||||
await page.select('[data-action="select-profile"]', "xyzbc-trt");
|
||||
await selectValue('[data-action="select-profile"]', "xyzbc-trt");
|
||||
await sleep(2000);
|
||||
state = await getState();
|
||||
await recordResult(
|
||||
@@ -505,7 +646,7 @@ try {
|
||||
);
|
||||
await capture("02-profile-xyzbc");
|
||||
|
||||
await page.select('[data-action="select-profile"]', "xyzac-trt");
|
||||
await selectValue('[data-action="select-profile"]', "xyzac-trt");
|
||||
await sleep(2000);
|
||||
state = await getState();
|
||||
await recordResult(
|
||||
@@ -534,7 +675,7 @@ try {
|
||||
|
||||
if (stagedCount > 0) {
|
||||
const sourceRel = summary.state.machineFileStaging.gcodeSources[0].sourceRel;
|
||||
await page.select('[data-action="select-linuxcnc-gcode-source"]', sourceRel);
|
||||
await selectValue('[data-action="select-linuxcnc-gcode-source"]', sourceRel);
|
||||
await sleep(4000);
|
||||
summary = await getSummary();
|
||||
const loadedVendored = summary.state.programSource === "linuxcnc-vendored-5axis-gcode";
|
||||
@@ -604,9 +745,13 @@ try {
|
||||
await recordResult(
|
||||
"step-program",
|
||||
"Step 单步执行",
|
||||
"点击 Step 后 runState=stepping,activeLine 前进或保持受控",
|
||||
`runState=${state.runState}, activeLine=${state.activeLine}`,
|
||||
state.runState === "stepping" || state.activeLine !== beforeStepLine ? "PASS" : "FAIL",
|
||||
"点击 Step 后 Task/HAL 记录 singleStepping=true,并保持暂停态等待后续操作",
|
||||
`runState=${state.runState}, activeLine=${state.activeLine}, taskPaused=${state.machine.taskPaused}, singleStepping=${state.taskHalStatus?.task?.singleStepping}`,
|
||||
state.machine.taskPaused === true
|
||||
&& state.taskHalStatus?.task?.singleStepping === true
|
||||
&& Number(state.activeLine) >= Number(beforeStepLine)
|
||||
? "PASS"
|
||||
: "FAIL",
|
||||
);
|
||||
|
||||
await clickAndWait('[data-action="STOP"]', 1200);
|
||||
@@ -657,7 +802,10 @@ try {
|
||||
saveSessionStatus.status,
|
||||
);
|
||||
|
||||
await clickAndWait('[data-action="mode-manual"]');
|
||||
await waitForState((nextState) => nextState.machine.mode === "manual", 8000, "manual mode before session mutation").catch(() => null);
|
||||
await clickAndWait('[data-action="JOG_X_POS"]');
|
||||
await waitForState((nextState) => Number(nextState.axisPose.x) !== Number(state.axisPose.x), 8000, "axis changed before restore").catch(() => null);
|
||||
const modifiedState = await getState();
|
||||
await clickAndWait('[data-action="RESTORE_SESSION"]', 2500);
|
||||
state = await getState();
|
||||
@@ -712,7 +860,7 @@ try {
|
||||
|
||||
const report = {
|
||||
generatedAt: new Date().toISOString(),
|
||||
targetUrl: URL,
|
||||
targetUrl,
|
||||
chromePath: CHROME_PATH,
|
||||
screenshots,
|
||||
screenshotAnalysis,
|
||||
@@ -722,10 +870,20 @@ try {
|
||||
pageErrors,
|
||||
requestFailures,
|
||||
};
|
||||
report.status = findings.some((finding) => finding.status === "FAIL") || pageErrors.length > 0
|
||||
? "FAIL"
|
||||
: "PASS";
|
||||
|
||||
await fs.writeFile(path.join(OUTPUT_DIR, "site-test-report.json"), `${JSON.stringify(report, null, 2)}\n`, "utf8");
|
||||
console.log(`site_test_status=${report.status}`);
|
||||
console.log(`site_test_target_url=${targetUrl}`);
|
||||
console.log(`site_test_report=/home/meswork/cnc_wams/qa/web-rtcp-5axis-site-test/output/site-test-report.json`);
|
||||
if (report.status !== "PASS") {
|
||||
process.exitCode = 1;
|
||||
}
|
||||
} finally {
|
||||
await browser.close();
|
||||
await browser.close().catch(() => {});
|
||||
if (server) await new Promise((resolve) => server.close(resolve));
|
||||
}
|
||||
|
||||
async function analyzeScreenshot(filePath) {
|
||||
|
||||
|
Before Width: | Height: | Size: 204 KiB After Width: | Height: | Size: 284 KiB |
|
Before Width: | Height: | Size: 175 KiB After Width: | Height: | Size: 257 KiB |
|
Before Width: | Height: | Size: 175 KiB After Width: | Height: | Size: 258 KiB |
|
Before Width: | Height: | Size: 176 KiB After Width: | Height: | Size: 269 KiB |
|
Before Width: | Height: | Size: 166 KiB After Width: | Height: | Size: 240 KiB |
|
Before Width: | Height: | Size: 165 KiB After Width: | Height: | Size: 245 KiB |
|
Before Width: | Height: | Size: 166 KiB After Width: | Height: | Size: 247 KiB |