Add RTCP simulation QA updates
This commit is contained in:
752
qa/web-rtcp-5axis-site-test/run-site-test.mjs
Normal file
752
qa/web-rtcp-5axis-site-test/run-site-test.mjs
Normal file
@@ -0,0 +1,752 @@
|
||||
import fs from "node:fs/promises";
|
||||
import path from "node:path";
|
||||
import puppeteer from "puppeteer-core";
|
||||
import { PNG } from "pngjs";
|
||||
|
||||
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 localProgramPath = path.join(ROOT, "fixtures", "test-program.ngc");
|
||||
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
||||
|
||||
await fs.mkdir(OUTPUT_DIR, { recursive: true });
|
||||
await fs.mkdir(SCREENSHOT_DIR, { recursive: true });
|
||||
await fs.mkdir(path.dirname(localProgramPath), { recursive: true });
|
||||
|
||||
const findings = [];
|
||||
const consoleLogs = [];
|
||||
const pageErrors = [];
|
||||
const requestFailures = [];
|
||||
|
||||
const browser = await puppeteer.launch({
|
||||
headless: true,
|
||||
executablePath: CHROME_PATH,
|
||||
defaultViewport: { width: 1600, height: 1200, deviceScaleFactor: 1 },
|
||||
ignoreHTTPSErrors: true,
|
||||
args: [
|
||||
"--ignore-certificate-errors",
|
||||
"--disable-gpu",
|
||||
"--enable-webgl",
|
||||
"--use-angle=swiftshader",
|
||||
"--enable-unsafe-swiftshader",
|
||||
"--no-sandbox",
|
||||
],
|
||||
});
|
||||
|
||||
let page;
|
||||
|
||||
try {
|
||||
page = await browser.newPage();
|
||||
page.on("console", async (msg) => {
|
||||
let text = msg.text();
|
||||
if (msg.type() === "error" && msg.args().length > 0) {
|
||||
try {
|
||||
const values = await Promise.all(msg.args().map((arg) => arg.jsonValue().catch(() => null)));
|
||||
const serialized = values.filter((value) => value !== null);
|
||||
if (serialized.length > 0) {
|
||||
text = `${text} ${JSON.stringify(serialized)}`;
|
||||
}
|
||||
} catch {}
|
||||
}
|
||||
consoleLogs.push({
|
||||
type: msg.type(),
|
||||
text,
|
||||
location: msg.location(),
|
||||
});
|
||||
});
|
||||
page.on("pageerror", (error) => {
|
||||
pageErrors.push({
|
||||
message: error.message,
|
||||
stack: error.stack || "",
|
||||
});
|
||||
});
|
||||
page.on("requestfailed", (request) => {
|
||||
requestFailures.push({
|
||||
url: request.url(),
|
||||
method: request.method(),
|
||||
errorText: request.failure()?.errorText || "unknown",
|
||||
resourceType: request.resourceType(),
|
||||
});
|
||||
});
|
||||
|
||||
await fs.writeFile(localProgramPath, [
|
||||
"%",
|
||||
"G90 G17 G21",
|
||||
"G0 X0 Y0 Z5",
|
||||
"G1 Z-1 F200",
|
||||
"G1 X10 Y10 F300",
|
||||
"M30",
|
||||
"%",
|
||||
"",
|
||||
].join("\n"), "utf8");
|
||||
|
||||
await page.goto(URL, { 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(() => {
|
||||
const state = window.webRtcp5AxisSimulation?.getState?.();
|
||||
return Boolean(state?.iniConfigReadiness?.loaded);
|
||||
}, { timeout: 20000 }).catch(() => {});
|
||||
await waitForAppIdle(20000);
|
||||
await waitForState(
|
||||
(state) => state.taskHalRuntimeReadiness?.halSyncReady === true,
|
||||
20000,
|
||||
"task/HAL runtime readiness",
|
||||
).catch(() => null);
|
||||
|
||||
const screenshots = {};
|
||||
|
||||
async function capture(name, clipSelector = null) {
|
||||
const targetPath = path.join(SCREENSHOT_DIR, `${name}.png`);
|
||||
if (clipSelector) {
|
||||
const element = await page.$(clipSelector);
|
||||
if (element) {
|
||||
await element.screenshot({ path: targetPath });
|
||||
} else {
|
||||
await page.screenshot({ path: targetPath, fullPage: true });
|
||||
}
|
||||
} else {
|
||||
await page.screenshot({ path: targetPath, fullPage: true });
|
||||
}
|
||||
screenshots[name] = targetPath;
|
||||
return targetPath;
|
||||
}
|
||||
|
||||
async function getState() {
|
||||
return page.evaluate(() => {
|
||||
const state = window.webRtcp5AxisSimulation.getState();
|
||||
return JSON.parse(JSON.stringify(state));
|
||||
});
|
||||
}
|
||||
|
||||
async function waitForState(predicate, timeoutMs = 10000, label = "state condition") {
|
||||
const start = Date.now();
|
||||
while (Date.now() - start < timeoutMs) {
|
||||
const snapshot = await getState();
|
||||
if (predicate(snapshot)) return snapshot;
|
||||
await sleep(100);
|
||||
}
|
||||
throw new Error(`Timed out waiting for ${label}`);
|
||||
}
|
||||
|
||||
async function waitForAppIdle(timeoutMs = 8000) {
|
||||
await waitForState(
|
||||
(state) => !state.taskHalExecutionPending && !state.interpreterExecutionPending,
|
||||
timeoutMs,
|
||||
"app idle",
|
||||
).catch(() => null);
|
||||
}
|
||||
|
||||
async function getSummary() {
|
||||
return page.evaluate(() => {
|
||||
const state = window.webRtcp5AxisSimulation.getState();
|
||||
const regions = window.webRtcp5AxisSimulation.getRegions?.() || null;
|
||||
const machineSummary = document.querySelector('[data-machine-state="summary"]')?.textContent?.trim() || "";
|
||||
const frameBoundary = document.querySelector('[data-rtcp-diagnostic="boundary"]')?.textContent?.trim() || "";
|
||||
const boundaryReady = document.querySelector('[data-linuxcnc-boundary="readiness"]')?.textContent?.trim() || "";
|
||||
const taskHal = document.querySelector('[data-task-hal-runtime="readiness"]')?.textContent?.trim() || "";
|
||||
const machineFiles = document.querySelector('[data-machine-file-staging="status"]')?.textContent?.trim() || "";
|
||||
const activeLine = document.querySelector('[data-active-program-line]')?.textContent?.trim() || "";
|
||||
const linuxCncSourceStatus = document.querySelector('[data-linuxcnc-gcode-source="status"]')?.textContent?.trim() || "";
|
||||
const canvas = document.querySelector("[data-five-axis-canvas]");
|
||||
return {
|
||||
regions,
|
||||
machineSummary,
|
||||
frameBoundary,
|
||||
boundaryReady,
|
||||
taskHal,
|
||||
machineFiles,
|
||||
activeLine,
|
||||
linuxCncSourceStatus,
|
||||
canvas: canvas ? { ...canvas.dataset } : null,
|
||||
state,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
async function recordResult(key, title, expectation, actual, status, detail = "") {
|
||||
findings.push({
|
||||
key,
|
||||
title,
|
||||
expectation,
|
||||
actual,
|
||||
status,
|
||||
detail,
|
||||
});
|
||||
}
|
||||
|
||||
async function clickAndWait(selector, waitMs = 600) {
|
||||
await page.click(selector);
|
||||
await sleep(waitMs);
|
||||
await waitForAppIdle();
|
||||
}
|
||||
|
||||
async function setInputValue(selector, value) {
|
||||
await page.$eval(selector, (el, nextValue) => {
|
||||
el.value = nextValue;
|
||||
el.dispatchEvent(new Event("input", { bubbles: true }));
|
||||
el.dispatchEvent(new Event("change", { bubbles: true }));
|
||||
}, value);
|
||||
await sleep(400);
|
||||
}
|
||||
|
||||
function classify(condition, passText, failText) {
|
||||
return condition ? { status: "PASS", text: passText } : { status: "FAIL", text: failText };
|
||||
}
|
||||
|
||||
function classifyWarn(condition, passText, warnText) {
|
||||
return condition ? { status: "PASS", text: passText } : { status: "WARN", text: warnText };
|
||||
}
|
||||
|
||||
const initial = await getSummary();
|
||||
await capture("01-home");
|
||||
|
||||
const regionsPass = [
|
||||
"titlebar",
|
||||
"preview",
|
||||
"dro",
|
||||
"gcode",
|
||||
"status-sidebar",
|
||||
"info-tabs",
|
||||
"override",
|
||||
"spindle-coolant",
|
||||
"bottom-controls",
|
||||
].every((region) => initial.regions?.[region] === true);
|
||||
const regionsStatus = classify(regionsPass, "9个主区域全部渲染", "存在主区域未渲染");
|
||||
await recordResult(
|
||||
"layout-regions",
|
||||
"主界面九大区域渲染",
|
||||
"titlebar/preview/dro/gcode/sidebar/info/override/spindle/bottom 全部存在",
|
||||
regionsStatus.text,
|
||||
regionsStatus.status,
|
||||
);
|
||||
|
||||
const canvasFallback = initial.canvas?.threeRenderer === "2d-fallback";
|
||||
const canvasReady = initial.canvas?.threeReady === "true";
|
||||
const canvasStatus = canvasReady
|
||||
? (canvasFallback
|
||||
? { status: "WARN", text: `预览可用,但当前测试环境使用 ${initial.canvas.threeRenderer},原因:${initial.canvas.threeFallbackReason || "-"}` }
|
||||
: { status: "PASS", text: `预览已启用 ${initial.canvas.threeRenderer}` })
|
||||
: { status: "FAIL", text: "预览画布未进入 ready 状态" };
|
||||
await recordResult(
|
||||
"preview-canvas",
|
||||
"预览画布初始化",
|
||||
"预览区域可渲染机床与路径",
|
||||
canvasStatus.text,
|
||||
canvasStatus.status,
|
||||
initial.canvas ? JSON.stringify(initial.canvas) : "missing canvas dataset",
|
||||
);
|
||||
|
||||
const initialBoundaryStatus = classify(
|
||||
initial.frameBoundary.includes("linuxcnc_kinematics_wasm_c_abi"),
|
||||
`RTCP 帧边界已接入 LinuxCNC 运动学:${initial.frameBoundary}`,
|
||||
`RTCP 帧仍处于降级边界:${initial.frameBoundary}`,
|
||||
);
|
||||
await recordResult(
|
||||
"initial-boundary",
|
||||
"首屏 RTCP/运动学边界",
|
||||
"首屏应优先使用 LinuxCNC/WASM 运动学边界",
|
||||
initialBoundaryStatus.text,
|
||||
initialBoundaryStatus.status,
|
||||
);
|
||||
|
||||
const initialTaskHalStatus = classifyWarn(
|
||||
!/pending|blocked/i.test(initial.taskHal),
|
||||
`Task/HAL 已就绪:${initial.taskHal}`,
|
||||
`Task/HAL 未完成就绪:${initial.taskHal}`,
|
||||
);
|
||||
await recordResult(
|
||||
"initial-task-hal",
|
||||
"首屏 Task/HAL 运行态",
|
||||
"Task/HAL readiness 应就绪",
|
||||
initialTaskHalStatus.text,
|
||||
initialTaskHalStatus.status,
|
||||
);
|
||||
|
||||
await clickAndWait('[data-action="power"]');
|
||||
await waitForState((nextState) => nextState.machine.powerOn === true, 8000, "machine power on").catch(() => null);
|
||||
let state = await getState();
|
||||
await recordResult(
|
||||
"power-on",
|
||||
"POWER 上电",
|
||||
"点击 POWER 后 machine.powerOn=true,taskState=on",
|
||||
`powerOn=${state.machine.powerOn}, taskState=${state.machine.taskState}, runState=${state.runState}`,
|
||||
state.machine.powerOn && state.machine.taskState === "on" ? "PASS" : "FAIL",
|
||||
);
|
||||
|
||||
await clickAndWait('[data-action="mode-jog"]');
|
||||
await waitForState((nextState) => nextState.machine.mode === "manual", 8000, "JOG/manual mode").catch(() => null);
|
||||
state = await getState();
|
||||
await recordResult(
|
||||
"mode-jog",
|
||||
"JOG 模式切换",
|
||||
"点击 JOG 后 mode 归一到 manual",
|
||||
`mode=${state.machine.mode}`,
|
||||
state.machine.mode === "manual" ? "PASS" : "FAIL",
|
||||
);
|
||||
|
||||
await clickAndWait('[data-action="HOME"]');
|
||||
await waitForState((nextState) => nextState.machine.allHomed === true, 8000, "machine homed").catch(() => null);
|
||||
state = await getState();
|
||||
await recordResult(
|
||||
"home",
|
||||
"HOME 回参考点",
|
||||
"点击 HOME 后 allHomed=true,runState=idle",
|
||||
`allHomed=${state.machine.allHomed}, runState=${state.runState}`,
|
||||
state.machine.allHomed && state.runState === "idle" ? "PASS" : "FAIL",
|
||||
);
|
||||
|
||||
const beforeJogX = state.axisPose.x;
|
||||
await clickAndWait('[data-action="JOG_X_POS"]');
|
||||
state = await getState();
|
||||
await recordResult(
|
||||
"jog-x-plus",
|
||||
"JOG X+",
|
||||
"点击 X+ 后 X 坐标增加",
|
||||
`before=${beforeJogX}, after=${state.axisPose.x}`,
|
||||
state.axisPose.x > beforeJogX ? "PASS" : "FAIL",
|
||||
);
|
||||
|
||||
const beforeJogY = state.axisPose.y;
|
||||
await clickAndWait('[data-action="JOG_Y_NEG"]');
|
||||
state = await getState();
|
||||
await recordResult(
|
||||
"jog-y-minus",
|
||||
"JOG Y-",
|
||||
"点击 Y- 后 Y 坐标减小",
|
||||
`before=${beforeJogY}, after=${state.axisPose.y}`,
|
||||
state.axisPose.y < beforeJogY ? "PASS" : "FAIL",
|
||||
);
|
||||
|
||||
await clickAndWait('[data-action="mode-auto"]');
|
||||
await waitForState((nextState) => nextState.machine.mode === "auto", 8000, "AUTO mode").catch(() => null);
|
||||
state = await getState();
|
||||
await recordResult(
|
||||
"mode-auto",
|
||||
"AUTO 模式切换",
|
||||
"点击 AUTO 后 machine.mode=auto",
|
||||
`mode=${state.machine.mode}`,
|
||||
state.machine.mode === "auto" ? "PASS" : "FAIL",
|
||||
);
|
||||
|
||||
await clickAndWait('[data-action="mode-mdi"]');
|
||||
await waitForState((nextState) => nextState.machine.mode === "mdi", 8000, "MDI mode").catch(() => null);
|
||||
state = await getState();
|
||||
await recordResult(
|
||||
"mode-mdi",
|
||||
"MDI 模式切换",
|
||||
"点击 MDI 后 machine.mode=mdi",
|
||||
`mode=${state.machine.mode}`,
|
||||
state.machine.mode === "mdi" ? "PASS" : "FAIL",
|
||||
);
|
||||
|
||||
await setInputValue('[data-action="mdi-command"]', "M428");
|
||||
await clickAndWait('[data-action="mdi-submit"]', 800);
|
||||
state = await getState();
|
||||
await recordResult(
|
||||
"mdi-m428",
|
||||
"MDI 执行 M428",
|
||||
"执行 M428 后 RTCP 打开,kinsType 切到 tcp-*",
|
||||
`rtcp=${state.rtcpState}, kinsType=${state.kinsType}, message=${state.operatorMessage}`,
|
||||
state.rtcpState === "on" && String(state.kinsType).startsWith("tcp-") ? "PASS" : "FAIL",
|
||||
);
|
||||
|
||||
await clickAndWait('[data-action="mdi-history"][data-command="M429"]', 800);
|
||||
state = await getState();
|
||||
await recordResult(
|
||||
"mdi-m429",
|
||||
"MDI 快捷执行 M429",
|
||||
"执行 M429 后 RTCP 关闭,kinsType=identity",
|
||||
`rtcp=${state.rtcpState}, kinsType=${state.kinsType}, message=${state.operatorMessage}`,
|
||||
state.rtcpState === "off" && state.kinsType === "identity" ? "PASS" : "FAIL",
|
||||
);
|
||||
|
||||
await clickAndWait('[data-action="kins-tcp"]');
|
||||
state = await getState();
|
||||
await recordResult(
|
||||
"sidebar-tcp",
|
||||
"侧栏 TCP 按钮",
|
||||
"点击 TCP 后 RTCP 打开",
|
||||
`rtcp=${state.rtcpState}, kinsType=${state.kinsType}`,
|
||||
state.rtcpState === "on" && String(state.kinsType).startsWith("tcp-") ? "PASS" : "FAIL",
|
||||
);
|
||||
|
||||
await clickAndWait('[data-action="kins-identity"]');
|
||||
state = await getState();
|
||||
await recordResult(
|
||||
"sidebar-identity",
|
||||
"侧栏 IDENTITY 按钮",
|
||||
"点击 IDENTITY 后 RTCP 关闭",
|
||||
`rtcp=${state.rtcpState}, kinsType=${state.kinsType}`,
|
||||
state.rtcpState === "off" && state.kinsType === "identity" ? "PASS" : "FAIL",
|
||||
);
|
||||
|
||||
const beforeRapid = state.feed.rapidOverride;
|
||||
await clickAndWait('[data-action="rapid-override-up"]');
|
||||
state = await getState();
|
||||
await recordResult(
|
||||
"rapid-override",
|
||||
"Rapid Override 调整",
|
||||
"点击 + 后 rapidOverride 增加",
|
||||
`before=${beforeRapid}, after=${state.feed.rapidOverride}`,
|
||||
state.feed.rapidOverride > beforeRapid ? "PASS" : "FAIL",
|
||||
);
|
||||
|
||||
const beforeFeed = state.feed.feedOverride;
|
||||
await clickAndWait('[data-action="feed-override-down"]');
|
||||
state = await getState();
|
||||
await recordResult(
|
||||
"feed-override",
|
||||
"Feed Override 调整",
|
||||
"点击 - 后 feedOverride 减少",
|
||||
`before=${beforeFeed}, after=${state.feed.feedOverride}`,
|
||||
state.feed.feedOverride < beforeFeed ? "PASS" : "FAIL",
|
||||
);
|
||||
|
||||
const beforeSpindle = state.spindle.override;
|
||||
await clickAndWait('[data-action="spindle-override-up"]');
|
||||
state = await getState();
|
||||
await recordResult(
|
||||
"spindle-override",
|
||||
"Spindle Override 调整",
|
||||
"点击 + 后 spindle.override 增加",
|
||||
`before=${beforeSpindle}, after=${state.spindle.override}`,
|
||||
state.spindle.override > beforeSpindle ? "PASS" : "FAIL",
|
||||
);
|
||||
|
||||
const floodBefore = state.coolant.flood;
|
||||
await clickAndWait('[data-action="toggle-flood"]');
|
||||
state = await getState();
|
||||
await recordResult(
|
||||
"coolant-flood",
|
||||
"Flood 冷却开关",
|
||||
"点击 Flood 后 flood 状态切换",
|
||||
`before=${floodBefore}, after=${state.coolant.flood}`,
|
||||
state.coolant.flood !== floodBefore ? "PASS" : "FAIL",
|
||||
);
|
||||
|
||||
const mistBefore = state.coolant.mist;
|
||||
await clickAndWait('[data-action="toggle-mist"]');
|
||||
state = await getState();
|
||||
await recordResult(
|
||||
"coolant-mist",
|
||||
"Mist 冷却开关",
|
||||
"点击 Mist 后 mist 状态切换",
|
||||
`before=${mistBefore}, after=${state.coolant.mist}`,
|
||||
state.coolant.mist !== mistBefore ? "PASS" : "FAIL",
|
||||
);
|
||||
|
||||
await clickAndWait('[data-action="view-x"]');
|
||||
state = await getState();
|
||||
const viewXPass = state.preview.selectedView === "x";
|
||||
await recordResult(
|
||||
"view-x",
|
||||
"预览视角 X",
|
||||
"点击 X 后 selectedView=x",
|
||||
`selectedView=${state.preview.selectedView}`,
|
||||
viewXPass ? "PASS" : "FAIL",
|
||||
);
|
||||
|
||||
await clickAndWait('[data-action="view-y"]');
|
||||
state = await getState();
|
||||
await recordResult(
|
||||
"view-y",
|
||||
"预览视角 Y",
|
||||
"点击 Y 后 selectedView=y",
|
||||
`selectedView=${state.preview.selectedView}`,
|
||||
state.preview.selectedView === "y" ? "PASS" : "FAIL",
|
||||
);
|
||||
|
||||
await clickAndWait('[data-action="clear-preview"]');
|
||||
state = await getState();
|
||||
await recordResult(
|
||||
"clear-preview",
|
||||
"Clear Preview",
|
||||
"点击 Clear 后 pathPoints=0",
|
||||
`pathPoints=${state.preview.pathPoints}`,
|
||||
state.preview.pathPoints === 0 ? "PASS" : "FAIL",
|
||||
);
|
||||
|
||||
await clickAndWait('[data-action="reset-view"]');
|
||||
state = await getState();
|
||||
await recordResult(
|
||||
"reset-view",
|
||||
"Fit/Reset View",
|
||||
"点击 Fit 后 selectedView=iso",
|
||||
`selectedView=${state.preview.selectedView}`,
|
||||
state.preview.selectedView === "iso" ? "PASS" : "FAIL",
|
||||
);
|
||||
|
||||
await clickAndWait('[data-action="FULL"]');
|
||||
state = await getState();
|
||||
const fullOn = state.preview.fullscreen === true;
|
||||
await clickAndWait('[data-action="FULL"]');
|
||||
const stateAfterFullOff = await getState();
|
||||
await recordResult(
|
||||
"fullscreen-toggle",
|
||||
"Full 全屏切换",
|
||||
"连续点击两次 Full 后 fullscreen true 再 false",
|
||||
`first=${state.preview.fullscreen}, second=${stateAfterFullOff.preview.fullscreen}`,
|
||||
fullOn && stateAfterFullOff.preview.fullscreen === false ? "PASS" : "FAIL",
|
||||
);
|
||||
|
||||
await page.select('[data-action="select-profile"]', "xyzbc-trt");
|
||||
await sleep(2000);
|
||||
state = await getState();
|
||||
await recordResult(
|
||||
"profile-switch",
|
||||
"Profile 切换到 xyzbc-trt",
|
||||
"切换后 machineProfile=xyzbc-trt,INI 重新加载",
|
||||
`machineProfile=${state.machineProfile}, iniLoaded=${state.iniConfigReadiness.loaded}, iniPath=${state.iniConfigReadiness.path}`,
|
||||
state.machineProfile === "xyzbc-trt" && state.iniConfigReadiness.loaded ? "PASS" : "FAIL",
|
||||
);
|
||||
await capture("02-profile-xyzbc");
|
||||
|
||||
await page.select('[data-action="select-profile"]', "xyzac-trt");
|
||||
await sleep(2000);
|
||||
state = await getState();
|
||||
await recordResult(
|
||||
"profile-switch-back",
|
||||
"Profile 切回 xyzac-trt",
|
||||
"切回后 machineProfile=xyzac-trt",
|
||||
`machineProfile=${state.machineProfile}, iniLoaded=${state.iniConfigReadiness.loaded}`,
|
||||
state.machineProfile === "xyzac-trt" && state.iniConfigReadiness.loaded ? "PASS" : "FAIL",
|
||||
);
|
||||
|
||||
await clickAndWait('[data-action="stage-linuxcnc-sources"]', 5000);
|
||||
let summary = await getSummary();
|
||||
const stagedCount = summary.state.machineFileStaging?.gcodeSources?.length || 0;
|
||||
const stagingStatus = classifyWarn(
|
||||
stagedCount > 0 && summary.state.machineFileStaging.status === "staged",
|
||||
`已 staged ${stagedCount} 个 LinuxCNC 五轴 G-code 源文件`,
|
||||
`staging 未完成:status=${summary.state.machineFileStaging.status}, lastError=${summary.state.machineFileStaging.lastError || "-"}`,
|
||||
);
|
||||
await recordResult(
|
||||
"stage-linuxcnc-sources",
|
||||
"Stage LinuxCNC 5-axis 源程序",
|
||||
"点击 Stage 后应完成 machine files staging 并出现可选 G-code",
|
||||
stagingStatus.text,
|
||||
stagingStatus.status,
|
||||
);
|
||||
|
||||
if (stagedCount > 0) {
|
||||
const sourceRel = summary.state.machineFileStaging.gcodeSources[0].sourceRel;
|
||||
await page.select('[data-action="select-linuxcnc-gcode-source"]', sourceRel);
|
||||
await sleep(4000);
|
||||
summary = await getSummary();
|
||||
const loadedVendored = summary.state.programSource === "linuxcnc-vendored-5axis-gcode";
|
||||
await recordResult(
|
||||
"load-vendored-program",
|
||||
"加载 LinuxCNC 五轴源程序",
|
||||
"选择源程序后 programSource=linuxcnc-vendored-5axis-gcode",
|
||||
`programSource=${summary.state.programSource}, activeProgram=${summary.state.activeProgram}, sourceRel=${summary.state.programSourceRel || "-"}`,
|
||||
loadedVendored ? "PASS" : "FAIL",
|
||||
);
|
||||
await capture("03-vendored-program-loaded");
|
||||
} else {
|
||||
await recordResult(
|
||||
"load-vendored-program",
|
||||
"加载 LinuxCNC 五轴源程序",
|
||||
"应可加载 staged 后的 vendored 程序",
|
||||
"因 staging 未完成,本项无法继续",
|
||||
"FAIL",
|
||||
);
|
||||
}
|
||||
|
||||
state = await getState();
|
||||
if (!state.machine.powerOn) {
|
||||
await clickAndWait('[data-action="power"]');
|
||||
await waitForState((nextState) => nextState.machine.powerOn === true, 8000, "machine power on before run").catch(() => null);
|
||||
}
|
||||
if (!state.machine.allHomed) {
|
||||
await clickAndWait('[data-action="mode-jog"]');
|
||||
await clickAndWait('[data-action="HOME"]');
|
||||
await waitForState((nextState) => nextState.machine.allHomed === true, 8000, "machine homed before run").catch(() => null);
|
||||
}
|
||||
await clickAndWait('[data-action="mode-auto"]');
|
||||
await clickAndWait('[data-action="RUN"]', 2500);
|
||||
state = await getState();
|
||||
await recordResult(
|
||||
"run-program",
|
||||
"Run 程序",
|
||||
"点击 Run 后程序进入 running/complete,活动行或运行反馈推进",
|
||||
`runState=${state.runState}, activeLine=${state.activeLine}, source=${state.programExecutionSourceMode}, feedback=${state.programRuntimeFeedback?.apiName || "-"}`,
|
||||
["running", "complete"].includes(state.runState) || Number(state.activeLine) !== 501 ? "PASS" : "FAIL",
|
||||
);
|
||||
await capture("04-run-state");
|
||||
|
||||
await clickAndWait('[data-action="PAUSE"]', 1200);
|
||||
state = await getState();
|
||||
await recordResult(
|
||||
"pause-program",
|
||||
"Pause 程序",
|
||||
"点击 Pause 后 runState=paused",
|
||||
`runState=${state.runState}, interpState=${state.machine.interpState}`,
|
||||
state.runState === "paused" ? "PASS" : "FAIL",
|
||||
);
|
||||
|
||||
await clickAndWait('[data-action="RESUME"]', 1200);
|
||||
state = await getState();
|
||||
await recordResult(
|
||||
"resume-program",
|
||||
"Resume 程序",
|
||||
"点击 Resume 后 runState 返回 running/idle",
|
||||
`runState=${state.runState}, interpState=${state.machine.interpState}`,
|
||||
["running", "idle", "complete"].includes(state.runState) ? "PASS" : "FAIL",
|
||||
);
|
||||
|
||||
const beforeStepLine = state.activeLine;
|
||||
await clickAndWait('[data-action="STEP"]', 1500);
|
||||
state = await getState();
|
||||
await recordResult(
|
||||
"step-program",
|
||||
"Step 单步执行",
|
||||
"点击 Step 后 runState=stepping,activeLine 前进或保持受控",
|
||||
`runState=${state.runState}, activeLine=${state.activeLine}`,
|
||||
state.runState === "stepping" || state.activeLine !== beforeStepLine ? "PASS" : "FAIL",
|
||||
);
|
||||
|
||||
await clickAndWait('[data-action="STOP"]', 1200);
|
||||
state = await getState();
|
||||
await recordResult(
|
||||
"stop-program",
|
||||
"Stop 停止程序",
|
||||
"点击 Stop 后 runState=stopped",
|
||||
`runState=${state.runState}, interpState=${state.machine.interpState}`,
|
||||
state.runState === "stopped" ? "PASS" : "FAIL",
|
||||
);
|
||||
|
||||
await clickAndWait('[data-action="RELOAD"]', 1200);
|
||||
state = await getState();
|
||||
await recordResult(
|
||||
"reload-program",
|
||||
"Reload 程序",
|
||||
"点击 Reload 后 runState=idle,程序回到起始状态",
|
||||
`runState=${state.runState}, activeLine=${state.activeLine}`,
|
||||
state.runState === "idle" ? "PASS" : "FAIL",
|
||||
);
|
||||
|
||||
const fileInput = await page.$('[data-action="OPEN_FILE"]');
|
||||
await fileInput.uploadFile(localProgramPath);
|
||||
await sleep(3000);
|
||||
state = await getState();
|
||||
await recordResult(
|
||||
"open-local-program",
|
||||
"Open 本地 G-code 文件",
|
||||
"上传本地文件后 activeProgram 为上传文件,programSource=upload",
|
||||
`activeProgram=${state.activeProgram}, programSource=${state.programSource}, lineCount=${state.lineCount}`,
|
||||
state.activeProgram.endsWith("test-program.ngc") && ["upload", "operator-file"].includes(state.programSource) ? "PASS" : "FAIL",
|
||||
);
|
||||
await capture("05-local-program-opened");
|
||||
|
||||
await clickAndWait('[data-action="SAVE_SESSION"]', 2500);
|
||||
state = await getState();
|
||||
const saveSessionStatus = classifyWarn(
|
||||
state.sessionPersistence.status === "saved",
|
||||
`会话已保存:${state.sessionPersistence.path} (${state.sessionPersistence.storageMode})`,
|
||||
`会话保存未成功:status=${state.sessionPersistence.status}, error=${state.sessionPersistence.lastError || "-"}`,
|
||||
);
|
||||
await recordResult(
|
||||
"save-session",
|
||||
"Save Session",
|
||||
"点击 Save Session 后会话状态应为 saved",
|
||||
saveSessionStatus.text,
|
||||
saveSessionStatus.status,
|
||||
);
|
||||
|
||||
await clickAndWait('[data-action="JOG_X_POS"]');
|
||||
const modifiedState = await getState();
|
||||
await clickAndWait('[data-action="RESTORE_SESSION"]', 2500);
|
||||
state = await getState();
|
||||
await recordResult(
|
||||
"restore-session",
|
||||
"Restore Session",
|
||||
"点击 Restore Session 后会话恢复到最近保存快照",
|
||||
`modifiedX=${modifiedState.axisPose.x}, restoredX=${state.axisPose.x}, status=${state.sessionPersistence.status}`,
|
||||
state.sessionPersistence.status === "restored" && state.axisPose.x !== modifiedState.axisPose.x ? "PASS" : "FAIL",
|
||||
);
|
||||
|
||||
await clickAndWait('[data-action="AUDIT_FULL_BOUNDARY"]', 6000);
|
||||
summary = await getSummary();
|
||||
const auditStatus = classifyWarn(
|
||||
!summary.state.interpreterExecutionPending,
|
||||
`Audit 执行完成,fullBoundary=${summary.state.fullExecutionBoundary?.fullLinuxCncProgramExecutionReady}`,
|
||||
"Audit 触发后仍在 pending 或未返回结果",
|
||||
);
|
||||
await recordResult(
|
||||
"audit-full-boundary",
|
||||
"Audit Full Boundary",
|
||||
"点击 Audit 后应触发五轴 machine-file 运行审计并刷新边界状态",
|
||||
`${auditStatus.text}; boundaryStatus=${summary.state.fullExecutionBoundary?.boundaryStatus || "-"}; machineRun=${summary.state.machineFileExecution?.summary?.machineFileExecutionReady ?? "-"}`,
|
||||
auditStatus.status,
|
||||
);
|
||||
await capture("06-after-audit");
|
||||
|
||||
await clickAndWait('[data-action="estop"]');
|
||||
state = await getState();
|
||||
await recordResult(
|
||||
"estop",
|
||||
"E-STOP 急停",
|
||||
"点击 E-STOP 后 estopActive=true,runState=estopped",
|
||||
`estopActive=${state.machine.estopActive}, runState=${state.runState}, powerOn=${state.machine.powerOn}`,
|
||||
state.machine.estopActive && state.runState === "estopped" ? "PASS" : "FAIL",
|
||||
);
|
||||
|
||||
await clickAndWait('[data-action="reset"]');
|
||||
state = await getState();
|
||||
await recordResult(
|
||||
"reset",
|
||||
"RESET 复位",
|
||||
"点击 RESET 后 estopActive=false,powerOn=false,taskState=estop-reset",
|
||||
`estopActive=${state.machine.estopActive}, powerOn=${state.machine.powerOn}, taskState=${state.machine.taskState}`,
|
||||
!state.machine.estopActive && !state.machine.powerOn && state.machine.taskState === "estop-reset" ? "PASS" : "FAIL",
|
||||
);
|
||||
|
||||
const finalSummary = await getSummary();
|
||||
await capture("07-final");
|
||||
|
||||
const screenshotAnalysis = await analyzeScreenshot(screenshots["01-home"]);
|
||||
|
||||
const report = {
|
||||
generatedAt: new Date().toISOString(),
|
||||
targetUrl: URL,
|
||||
chromePath: CHROME_PATH,
|
||||
screenshots,
|
||||
screenshotAnalysis,
|
||||
finalSummary,
|
||||
findings,
|
||||
consoleLogs,
|
||||
pageErrors,
|
||||
requestFailures,
|
||||
};
|
||||
|
||||
await fs.writeFile(path.join(OUTPUT_DIR, "site-test-report.json"), `${JSON.stringify(report, null, 2)}\n`, "utf8");
|
||||
} finally {
|
||||
await browser.close();
|
||||
}
|
||||
|
||||
async function analyzeScreenshot(filePath) {
|
||||
if (!filePath) return null;
|
||||
const buffer = await fs.readFile(filePath);
|
||||
const png = PNG.sync.read(buffer);
|
||||
const { width, height, data } = png;
|
||||
let sum = 0;
|
||||
let nonBlackPixels = 0;
|
||||
for (let i = 0; i < data.length; i += 4) {
|
||||
const r = data[i];
|
||||
const g = data[i + 1];
|
||||
const b = data[i + 2];
|
||||
const luminance = (r + g + b) / 3;
|
||||
sum += luminance;
|
||||
if (luminance > 8) nonBlackPixels += 1;
|
||||
}
|
||||
return {
|
||||
width,
|
||||
height,
|
||||
averageLuminance: Number((sum / (width * height)).toFixed(2)),
|
||||
nonBlackRatio: Number((nonBlackPixels / (width * height)).toFixed(4)),
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user