Add RTCP simulation QA updates

This commit is contained in:
2026-06-22 09:30:27 -04:00
parent 61e2fe8441
commit ea8e10031b
51 changed files with 10008 additions and 178 deletions

View File

@@ -0,0 +1,356 @@
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 QA_ROOT = path.resolve("/home/meswork/cnc_wams/qa/web-rtcp-5axis-site-test");
const OUTPUT_DIR = path.join(QA_ROOT, "output");
const SCREENSHOT_DIR = path.join(QA_ROOT, "screenshots", "toolpath-preview-cases");
const CHROME_PATH = process.env.CHROME_PATH || "/usr/bin/google-chrome";
const FIXTURE_URL = "/web-rtcp-5axis-sim-plan/app/index.html";
await fs.mkdir(OUTPUT_DIR, { recursive: true });
await fs.mkdir(SCREENSHOT_DIR, { recursive: true });
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",
".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));
}
});
}
const 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");
}
const baseUrl = `http://127.0.0.1:${address.port}`;
const browser = await puppeteer.launch({
headless: true,
executablePath: CHROME_PATH,
defaultViewport: { width: 1600, height: 1200, deviceScaleFactor: 1 },
args: [
"--disable-gpu",
"--enable-webgl",
"--use-angle=swiftshader",
"--enable-unsafe-swiftshader",
"--no-sandbox",
],
});
const page = await browser.newPage();
const consoleErrors = [];
page.on("console", (msg) => {
if (msg.type() === "error") {
consoleErrors.push(msg.text());
}
});
page.on("pageerror", (error) => {
consoleErrors.push(error.message);
});
const report = {
generatedAt: new Date().toISOString(),
targetUrl: `${baseUrl}${FIXTURE_URL}`,
projectPath: path.join(REPO_ROOT, "web-rtcp-5axis-sim-plan", "app", "index.html"),
chromePath: CHROME_PATH,
screenshots: {},
cases: [],
consoleErrors,
};
async function wait(ms) {
await new Promise((resolve) => setTimeout(resolve, ms));
}
async function waitForState(predicate, timeoutMs = 20000, label = "state condition") {
const start = Date.now();
while (Date.now() - start < timeoutMs) {
const snapshot = await page.evaluate(() => JSON.parse(JSON.stringify(window.webRtcp5AxisSimulation.getState())));
if (predicate(snapshot)) return snapshot;
await wait(100);
}
throw new Error(`timeout waiting for ${label}`);
}
async function getCanvasDataset() {
return page.$eval("[data-five-axis-canvas]", (canvas) => ({ ...canvas.dataset }));
}
async function waitForCanvasReady(timeoutMs = 20000) {
await page.waitForFunction(() => {
const canvas = document.querySelector("[data-five-axis-canvas]");
return canvas?.dataset?.threeReady === "true"
&& canvas?.dataset?.threePreviewScope === "machine-reference-and-toolpath";
}, { timeout: timeoutMs });
}
async function captureCase(name) {
const screenshotPath = path.join(SCREENSHOT_DIR, `${name}.png`);
const canvas = await page.$("[data-five-axis-canvas]");
if (!canvas) {
throw new Error(`missing canvas for ${name}`);
}
await canvas.screenshot({ path: screenshotPath });
report.screenshots[name] = screenshotPath;
return screenshotPath;
}
async function recordCase(name, summary, checks = []) {
const dataset = await getCanvasDataset();
const state = await page.evaluate(() => JSON.parse(JSON.stringify(window.webRtcp5AxisSimulation.getState())));
const screenshotPath = report.screenshots[name];
const pixelStats = screenshotPath ? await analyzePng(screenshotPath) : null;
report.cases.push({
name,
summary,
status: checks.every((check) => check.pass) ? "PASS" : "FAIL",
checks,
pixelStats,
dataset,
state: {
activeProgram: state.activeProgram,
activeLine: state.activeLine,
programSource: state.programSource,
programExecutionSourceMode: state.programExecutionSourceMode,
programExecutionSummary: state.programExecution?.summary || null,
runState: state.runState,
rtcpState: state.rtcpState,
kinsType: state.kinsType,
activeLine: state.activeLine,
programExecutionMotionIndex: state.programExecutionMotionIndex,
programExecutionSampleIndex: state.programExecutionSampleIndex,
programRuntimeFeedbackSource: state.programRuntimeFeedback?.sourceMode || null,
},
});
}
function previewChecks(dataset, {
requirePathPoints = true,
requireExecutedPath = false,
requireArcPoints = false,
requireRapidFeed = false,
expectPathPoints = null,
expectRtcp = null,
} = {}) {
const checks = [
check("canvas ready", dataset.threeReady === "true", `threeReady=${dataset.threeReady}`),
check("WebGL renderer", dataset.threeRenderer === "webgl", `renderer=${dataset.threeRenderer}`),
check("机床参考模型", dataset.threePreviewScope === "machine-reference-and-toolpath" && dataset.threeMachineReferenceModel === "webgl-five-axis-reference", `scope=${dataset.threePreviewScope}, model=${dataset.threeMachineReferenceModel}`),
check("TCP 球标记", dataset.threeTcpMarker === "sphere" && dataset.threeToolExecutionMarker === "true", `tcp=${dataset.threeTcpMarker}, marker=${dataset.threeToolExecutionMarker}`),
check("刀轴线", dataset.threeToolAxisMarker === "line", `toolAxisMarker=${dataset.threeToolAxisMarker}`),
check("场景对象数量", Number(dataset.threeSceneObjects || 0) >= 12, `sceneObjects=${dataset.threeSceneObjects}`),
check("无 G-code 语义生成", dataset.threeNoGcodeSemanticsGeneration === "ok", `semanticGuard=${dataset.threeNoGcodeSemanticsGeneration}`),
];
if (requirePathPoints) {
checks.push(check("刀路预览点", Number(dataset.threePathPoints || 0) >= 1, `pathPoints=${dataset.threePathPoints}`));
}
if (requireExecutedPath) {
checks.push(check("执行轨迹点", Number(dataset.threeExecutedPathPoints || 0) >= 1, `executed=${dataset.threeExecutedPathPoints}`));
}
if (requireArcPoints) {
checks.push(check("圆弧轨迹点", Number(dataset.threeArcPathPoints || 0) >= 1, `arc=${dataset.threeArcPathPoints}`));
}
if (requireRapidFeed) {
checks.push(check("rapid/feed 区分", Number(dataset.threeRapidPathPoints || 0) >= 1 && Number(dataset.threeFeedPathPoints || 0) >= 1, `rapid=${dataset.threeRapidPathPoints}, feed=${dataset.threeFeedPathPoints}`));
}
if (expectPathPoints !== null) {
checks.push(check("路径点期望", Number(dataset.threePathPoints || 0) === expectPathPoints, `pathPoints=${dataset.threePathPoints}, expected=${expectPathPoints}`));
}
if (expectRtcp !== null) {
checks.push(check("RTCP 状态", dataset.threeRtcpState === expectRtcp, `rtcp=${dataset.threeRtcpState}`));
}
return checks;
}
function check(name, pass, detail) {
return { name, pass: Boolean(pass), detail };
}
function assertChecks(caseName, checks) {
const failed = checks.filter((item) => !item.pass);
if (failed.length > 0) {
console.warn(`${caseName} failed: ${failed.map((item) => `${item.name} (${item.detail})`).join("; ")}`);
}
}
try {
await page.goto(`${baseUrl}${FIXTURE_URL}`, { waitUntil: "networkidle2", timeout: 60000 });
await page.waitForSelector('[data-shell="gmoccapy-5axis"]', { timeout: 15000 });
await page.waitForFunction(() => Boolean(window.webRtcp5AxisSimulation?.getState), { timeout: 15000 });
await waitForState((state) => state.kinematicsRuntimeReadiness?.loaded === true, 20000, "kinematics runtime ready");
await waitForCanvasReady();
await wait(1200);
let dataset = await getCanvasDataset();
let checks = previewChecks(dataset, { requirePathPoints: true, requireExecutedPath: true });
assertChecks("01-home-toolpath", checks);
await captureCase("01-home-toolpath");
await recordCase("01-home-toolpath", "默认首屏预览机床参考模型、TCP 球、刀轴线、fixture 路径和执行轨迹可见", checks);
await page.evaluate(() => {
window.webRtcp5AxisSimulation.dispatch({
type: "LOAD_PROGRAM",
filename: "operator-demo.ngc",
content: [
"G90 G17",
"G0 X0 Y0 Z0",
"G1 X10 F100",
"G1 Y10",
"G1 X0",
"G1 Y0",
"G0 Z5",
"M5",
"M2",
].join("\n"),
});
});
await waitForState((state) => state.programExecutionSourceMode === "linuxcnc-interpreter-wasm" && state.activeProgram === "operator-demo.ngc", 20000, "operator demo loaded");
await waitForCanvasReady();
await wait(500);
dataset = await getCanvasDataset();
checks = previewChecks(dataset, { requirePathPoints: true, requireExecutedPath: true, requireRapidFeed: true });
assertChecks("02-operator-demo-toolpath", checks);
await captureCase("02-operator-demo-toolpath");
await recordCase("02-operator-demo-toolpath", "本地矩形 G-code验证 LinuxCNC interpreter canonical motion、rapid/feed 区分、执行轨迹和 TCP 标记", checks);
await page.evaluate(() => {
window.webRtcp5AxisSimulation.dispatch({
type: "LOAD_PROGRAM",
filename: "operator-arc-demo.ngc",
content: [
"G90 G17",
"G0 X1 Y0 Z0",
"G2 X0 Y1 I-1 J0 F60",
"M2",
].join("\n"),
});
});
await waitForState((state) => state.activeProgram === "operator-arc-demo.ngc" && state.programExecution?.summary?.motionTypes?.includes("ARC_FEED"), 20000, "arc demo loaded");
await waitForCanvasReady();
await wait(500);
dataset = await getCanvasDataset();
checks = previewChecks(dataset, { requirePathPoints: true, requireExecutedPath: true, requireArcPoints: true });
assertChecks("03-arc-demo-toolpath", checks);
await captureCase("03-arc-demo-toolpath");
await recordCase("03-arc-demo-toolpath", "圆弧 G-code验证 ARC_FEED 进入弧线轨迹图层并保留执行轨迹", checks);
await page.evaluate(() => document.querySelector('[data-action="clear-preview"]').click());
await waitForCanvasReady();
await wait(500);
dataset = await getCanvasDataset();
checks = previewChecks(dataset, { requirePathPoints: false, expectPathPoints: 0 });
assertChecks("04-clear-preview-reference", checks);
await captureCase("04-clear-preview-reference");
await recordCase("04-clear-preview-reference", "清空刀路后:路径点为 0但机床参考模型、TCP 球和刀轴线仍应可见", checks);
await waitForState((state) => state.machineFileStaging?.status === "staged" && (state.machineFileStaging?.gcodeSources?.length || 0) >= 4, 25000, "machine files staged");
await page.select('[data-action="select-linuxcnc-gcode-source"]', "configs/sim/axis/vismach/5axis/table-rotary-tilting/demos/impeller-7bl-xyzac.ngc");
await waitForState((state) => state.activeProgram?.endsWith("impeller-7bl-xyzac.ngc"), 25000, "vendored impeller loaded");
await waitForCanvasReady();
await wait(1200);
dataset = await getCanvasDataset();
checks = previewChecks(dataset, { requirePathPoints: true, requireExecutedPath: true, requireRapidFeed: true, expectRtcp: "on" });
assertChecks("05-vendored-impeller-toolpath", checks);
await captureCase("05-vendored-impeller-toolpath");
await recordCase("05-vendored-impeller-toolpath", "LinuxCNC vendored 五轴 impeller 程序验证长路径、switchkins/RTCP 状态、rapid/feed 图层和 TCP 执行轨迹", checks);
await page.evaluate(() => document.querySelector('[data-action="power"]').click());
await waitForState((state) => state.machine.taskState === "on", 10000, "machine powered on");
await page.evaluate(() => document.querySelector('[data-action="mode-manual"]').click());
await waitForState((state) => state.machine.mode === "manual", 10000, "manual mode selected");
await page.evaluate(() => document.querySelector('[data-action="HOME"]').click());
await waitForState((state) => state.machine.allHomed === true, 10000, "machine homed");
await page.evaluate(() => document.querySelector('[data-action="mode-auto"]').click());
await waitForState((state) => state.machine.mode === "auto", 10000, "auto mode selected");
await page.evaluate(() => document.querySelector('[data-action="kins-tcp"]').click());
await waitForState((state) => state.rtcpState === "on" && state.kinsType === "tcp-xyzac", 10000, "RTCP enabled");
await page.evaluate(() => document.querySelector('[data-action="RUN"]').click());
await waitForState((state) => state.runState === "running" && state.programRuntimeFeedback?.sourceMode === "linuxcnc-task-motion-hal-wasm", 15000, "program running");
await waitForCanvasReady();
await wait(600);
dataset = await getCanvasDataset();
checks = previewChecks(dataset, { requirePathPoints: true, requireExecutedPath: true, requireRapidFeed: true, expectRtcp: "on" });
assertChecks("06-running-rtcp-toolpath", checks);
await captureCase("06-running-rtcp-toolpath");
await recordCase("06-running-rtcp-toolpath", "G-code 运行态:验证 task/HAL runtime feedback 驱动执行轨迹、当前段高亮、TCP 球和刀轴线跟随", checks);
report.consoleErrors = consoleErrors;
await fs.writeFile(
path.join(OUTPUT_DIR, "toolpath-preview-cases.json"),
`${JSON.stringify(report, null, 2)}\n`,
"utf8",
);
} finally {
await page.close().catch(() => {});
await browser.close().catch(() => {});
await new Promise((resolve) => server.close(resolve));
}
async function analyzePng(filePath) {
const buffer = await fs.readFile(filePath);
const png = PNG.sync.read(buffer);
const { width, height, data } = png;
let luminanceSum = 0;
let nonBlack = 0;
for (let index = 0; index < data.length; index += 4) {
const luminance = data[index] * 0.2126 + data[index + 1] * 0.7152 + data[index + 2] * 0.0722;
luminanceSum += luminance;
if (luminance > 8) nonBlack += 1;
}
const total = width * height;
return {
width,
height,
averageLuminance: Number((luminanceSum / total).toFixed(2)),
nonBlackRatio: Number((nonBlack / total).toFixed(4)),
};
}

View File

@@ -0,0 +1,7 @@
%
G90 G17 G21
G0 X0 Y0 Z5
G1 Z-1 F200
G1 X10 Y10 F300
M30
%

View File

@@ -0,0 +1,260 @@
import fs from "node:fs/promises";
import path from "node:path";
import {
AlignmentType,
Document,
HeadingLevel,
ImageRun,
Packer,
Paragraph,
Table,
TableCell,
TableRow,
TextRun,
WidthType,
} from "docx";
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 reportPath = path.join(OUTPUT_DIR, "site-test-report.json");
const rawReport = JSON.parse(await fs.readFile(reportPath, "utf8"));
const generated = new Date(rawReport.generatedAt || Date.now());
const ymd = generated.toISOString().slice(0, 10);
const docxPath = path.join(OUTPUT_DIR, `web-rtcp-5axis-site-test-report-${ymd}.docx`);
const counts = countByStatus(rawReport.findings || []);
const total = rawReport.findings?.length || 0;
const nonPassFindings = (rawReport.findings || []).filter((finding) => finding.status !== "PASS");
const screenshotEntries = Object.entries(rawReport.screenshots || {}).sort(([a], [b]) => a.localeCompare(b));
const environmentRows = [
["测试目标", rawReport.targetUrl || "https://82.156.24.101:8092/"],
["测试日期", ymd],
["测试方式", "Google Chrome 真实浏览器自动化测试 + DOM/runtime state 校验 + 截图留证"],
["浏览器执行路径", rawReport.chromePath || "/usr/bin/google-chrome"],
["报告数据", reportPath],
["截图目录", SCREENSHOT_DIR],
];
const scopeRows = [
["界面结构", "标题栏、预览区、DRO、G-code 区、右侧模式栏、信息区、override、主轴/冷却、底部控制栏"],
["机床/模式", "POWER、E-STOP、RESET、AUTO、MANUAL/JOG、MDI、HOME、JOG"],
["五轴/RTCP", "IDENTITY/TCP、MDI M428/M429、RTCP/kinematics 诊断"],
["程序工作流", "Stage LinuxCNC 源程序、加载 vendored 程序、打开本地 G-code、Run/Pause/Resume/Step/Stop/Reload"],
["操作控制", "Rapid Override、Feed Override、Spindle Override、Flood/Mist、预览视角、全屏"],
["会话与诊断", "Save Session、Restore Session、Audit Full Boundary、Profile 切换"],
];
const children = [
new Paragraph({
text: "Web RTCP 5 Axis Simulation 功能测试报告",
heading: HeadingLevel.TITLE,
alignment: AlignmentType.CENTER,
}),
centered(`测试对象:${rawReport.targetUrl || "https://82.156.24.101:8092/"}`),
centered(`生成时间:${formatDateTime(generated)}`),
blank(),
heading("1. 结论摘要"),
para(`本次共测试 ${total} 个功能点PASS ${counts.PASS || 0}FAIL ${counts.FAIL || 0}WARN ${counts.WARN || 0} 项。`),
para(summaryText(counts)),
summaryTable(total, counts),
heading("2. 测试环境"),
kvTable(environmentRows),
heading("3. 测试范围"),
kvTable(scopeRows),
heading("4. 问题清单"),
nonPassFindings.length > 0
? findingTable(nonPassFindings)
: para("未发现 FAIL/WARN 项。"),
heading("5. 详细测试结果"),
resultTable(rawReport.findings || []),
heading("6. 截图证据"),
];
for (const [name, filePath] of screenshotEntries) {
children.push(...(await imageBlock(name, filePath)));
}
children.push(
heading("7. 运行日志摘要"),
kvTable([
["Console error", String((rawReport.consoleLogs || []).filter((log) => log.type === "error").length)],
["Page error", String((rawReport.pageErrors || []).length)],
["Request failure", String((rawReport.requestFailures || []).length)],
["首屏截图平均亮度", String(rawReport.screenshotAnalysis?.averageLuminance ?? "-")],
["首屏非黑像素比例", String(rawReport.screenshotAnalysis?.nonBlackRatio ?? "-")],
["最终 RTCP 边界", rawReport.finalSummary?.frameBoundary || "-"],
["最终 Task/HAL", rawReport.finalSummary?.taskHal || "-"],
]),
heading("8. 原始证据文件"),
para(`原始 JSON${reportPath}`),
para(`Word 报告:${docxPath}`),
);
const doc = new Document({
sections: [{ properties: {}, children: children.flat() }],
});
await fs.writeFile(docxPath, await Packer.toBuffer(doc));
console.log(`docx_report=${docxPath}`);
function countByStatus(findings) {
return findings.reduce((acc, finding) => {
acc[finding.status] = (acc[finding.status] || 0) + 1;
return acc;
}, {});
}
function summaryText(statusCounts) {
if ((statusCounts.FAIL || 0) > 0) {
return "主流程可执行,但仍存在需修复的问题;重点集中在预览 ready 诊断、首屏 LinuxCNC kinematics 自动挂接、HOME 后 JOG 坐标连续性、MDI M428 和会话恢复验证。";
}
if ((statusCounts.WARN || 0) > 0) {
return "主流程通过,存在需要后续确认的 WARN 项。";
}
return "全部测试项通过。";
}
function heading(text) {
return new Paragraph({
text,
heading: HeadingLevel.HEADING_1,
spacing: { before: 240, after: 120 },
});
}
function centered(text) {
return new Paragraph({
alignment: AlignmentType.CENTER,
children: [new TextRun(text)],
});
}
function blank() {
return new Paragraph({ text: "" });
}
function para(text) {
return new Paragraph({
children: [new TextRun(String(text))],
spacing: { after: 100 },
});
}
function summaryTable(summaryTotal, statusCounts) {
return new Table({
width: { size: 100, type: WidthType.PERCENTAGE },
rows: [
new TableRow({
children: [
cell("总项数", true),
cell(String(summaryTotal)),
cell("PASS", true),
cell(String(statusCounts.PASS || 0)),
cell("FAIL", true),
cell(String(statusCounts.FAIL || 0)),
cell("WARN", true),
cell(String(statusCounts.WARN || 0)),
],
}),
],
});
}
function kvTable(rows) {
return new Table({
width: { size: 100, type: WidthType.PERCENTAGE },
rows: [
new TableRow({ children: [cell("项目", true), cell("内容", true)] }),
...rows.map(([key, value]) => new TableRow({ children: [cell(key), cell(value)] })),
],
});
}
function findingTable(findings) {
return new Table({
width: { size: 100, type: WidthType.PERCENTAGE },
rows: [
new TableRow({
children: [cell("结果", true), cell("功能点", true), cell("实际表现/证据", true), cell("备注", true)],
}),
...findings.map((finding) => new TableRow({
children: [
cell(finding.status),
cell(`${finding.key}\n${finding.title}`),
cell(finding.actual),
cell(finding.detail || finding.expectation || "-"),
],
})),
],
});
}
function resultTable(findings) {
return new Table({
width: { size: 100, type: WidthType.PERCENTAGE },
rows: [
new TableRow({
children: [cell("序号", true), cell("功能点", true), cell("预期", true), cell("实际", true), cell("结果", true)],
}),
...findings.map((finding, index) => new TableRow({
children: [
cell(String(index + 1)),
cell(`${finding.key}\n${finding.title}`),
cell(finding.expectation),
cell(finding.actual),
cell(finding.status),
],
})),
],
});
}
function cell(text, bold = false) {
return new TableCell({
width: { size: 25, type: WidthType.PERCENTAGE },
children: String(text || "-").split("\n").map((line) => new Paragraph({
children: [new TextRun({ text: line, bold })],
})),
});
}
async function imageBlock(name, filePath) {
const image = await fs.readFile(filePath);
return [
new Paragraph({
text: screenshotTitle(name),
heading: HeadingLevel.HEADING_2,
spacing: { before: 180, after: 120 },
}),
new Paragraph({
alignment: AlignmentType.CENTER,
children: [
new ImageRun({
data: image,
type: "png",
transformation: { width: 520, height: 390 },
}),
],
}),
para(`${screenshotTitle(name)};文件:${filePath}`),
];
}
function screenshotTitle(name) {
const titles = {
"01-home": "图 1 首屏界面",
"02-profile-xyzbc": "图 2 Profile 切换到 xyzbc-trt",
"03-vendored-program-loaded": "图 3 LinuxCNC 五轴源程序加载",
"04-run-state": "图 4 程序运行状态",
"05-local-program-opened": "图 5 本地 G-code 文件导入",
"06-after-audit": "图 6 Audit Full Boundary 后界面",
"07-final": "图 7 急停/复位后最终界面",
};
return titles[name] || name;
}
function formatDateTime(date) {
return date.toISOString().replace("T", " ").replace(/\.\d+Z$/, " UTC");
}

View File

@@ -0,0 +1,252 @@
import fs from "node:fs/promises";
import path from "node:path";
import {
AlignmentType,
Document,
HeadingLevel,
ImageRun,
Packer,
Paragraph,
Table,
TableCell,
TableRow,
TextRun,
WidthType,
} from "docx";
const ROOT = path.resolve("/home/meswork/cnc_wams/qa/web-rtcp-5axis-site-test");
const OUTPUT_DIR = path.join(ROOT, "output");
const reportJsonPath = path.join(OUTPUT_DIR, "toolpath-preview-cases.json");
const report = JSON.parse(await fs.readFile(reportJsonPath, "utf8"));
const generated = new Date(report.generatedAt || Date.now());
const ymd = generated.toISOString().slice(0, 10);
const docxPath = path.join(OUTPUT_DIR, `web-rtcp-5axis-toolpath-preview-report-${ymd}.docx`);
const counts = report.cases.reduce((acc, testCase) => {
acc[testCase.status] = (acc[testCase.status] || 0) + 1;
return acc;
}, {});
const children = [
new Paragraph({
text: "刀具预览与 G-code 执行刀具轨迹专项测试报告",
heading: HeadingLevel.TITLE,
alignment: AlignmentType.CENTER,
}),
centered(`测试对象:${report.targetUrl}`),
centered(`项目路径:${report.projectPath || "/home/meswork/cnc_wams/web-rtcp-5axis-sim-plan/app/index.html"}`),
centered(`生成时间:${formatDateTime(generated)}`),
blank(),
heading("1. 测试结论"),
para(`本次专项覆盖 6 个刀具预览/刀具轨迹场景PASS ${counts.PASS || 0}FAIL ${counts.FAIL || 0} 项。`),
para("已验证首屏预览、矩形 G-code、圆弧 G-code、清空预览、LinuxCNC vendored impeller 长路径、RTCP 运行态执行轨迹。截图中机床参考模型、TCP 球、刀轴线和刀路均有可见性证据。"),
para("发现 1 项关键问题vendored impeller 程序在预览态为 RTCP on但进入 G-code 运行态后 canvas dataset 中 RTCP 状态变为 off。"),
summaryTable(),
heading("2. 测试范围与判定口径"),
kvTable([
["预览对象", "五轴机床参考模型、工作台、XYZ 坐标轴、旋转轴、刀具/TCP 球、刀轴线"],
["轨迹对象", "G-code canonical motion 预览路径、rapid/feed/arc 分层、已执行轨迹、当前段高亮"],
["运行反馈", "LinuxCNC interpreter WASM、TP/runtime samples、task/HAL runtime feedback"],
["可见性口径", "canvas dataset ready + WebGL renderer + scene objects + pixel luminance/non-black ratio + 截图"],
["语义边界", "可视化层只消费 runtime/canonical motion/task-HAL feedback不生成 G-code/CNC 语义"],
]),
heading("3. 场景结果总表"),
caseSummaryTable(report.cases),
heading("4. 失败/风险项"),
issueTable(report.cases.filter((testCase) => testCase.status !== "PASS")),
heading("5. 场景明细与截图"),
];
for (const testCase of report.cases) {
children.push(...(await caseBlock(testCase)));
}
children.push(
heading("6. 原始证据"),
kvTable([
["原始 JSON", reportJsonPath],
["截图数量", String(Object.keys(report.screenshots || {}).length)],
["Console error", String((report.consoleErrors || []).length)],
["Chrome", report.chromePath || "-"],
["Word 报告", docxPath],
["项目路径", report.projectPath || "-"],
]),
);
if ((report.consoleErrors || []).length > 0) {
children.push(
heading("7. Console 记录"),
...report.consoleErrors.map((entry) => para(entry)),
);
}
const doc = new Document({
sections: [{ properties: {}, children: children.flat() }],
});
await fs.writeFile(docxPath, await Packer.toBuffer(doc));
console.log(`toolpath_docx_report=${docxPath}`);
function heading(text) {
return new Paragraph({
text,
heading: HeadingLevel.HEADING_1,
spacing: { before: 240, after: 120 },
});
}
function centered(text) {
return new Paragraph({
alignment: AlignmentType.CENTER,
children: [new TextRun(String(text))],
});
}
function blank() {
return new Paragraph({ text: "" });
}
function para(text) {
return new Paragraph({
children: [new TextRun(String(text))],
spacing: { after: 100 },
});
}
function summaryTable() {
return new Table({
width: { size: 100, type: WidthType.PERCENTAGE },
rows: [
new TableRow({
children: [
cell("场景总数", true),
cell(String(report.cases.length)),
cell("PASS", true),
cell(String(counts.PASS || 0)),
cell("FAIL", true),
cell(String(counts.FAIL || 0)),
],
}),
],
});
}
function kvTable(rows) {
return new Table({
width: { size: 100, type: WidthType.PERCENTAGE },
rows: [
new TableRow({ children: [cell("项目", true), cell("内容", true)] }),
...rows.map(([key, value]) => new TableRow({ children: [cell(key), cell(value)] })),
],
});
}
function caseSummaryTable(cases) {
return new Table({
width: { size: 100, type: WidthType.PERCENTAGE },
rows: [
new TableRow({
children: [cell("场景", true), cell("结果", true), cell("路径点", true), cell("执行点", true), cell("RTCP", true), cell("可见性", true)],
}),
...cases.map((testCase) => new TableRow({
children: [
cell(`${testCase.name}\n${testCase.summary}`),
cell(testCase.status),
cell(testCase.dataset.threePathPoints),
cell(testCase.dataset.threeExecutedPathPoints),
cell(testCase.dataset.threeRtcpState),
cell(`luma=${testCase.pixelStats?.averageLuminance ?? "-"}\nnonBlack=${testCase.pixelStats?.nonBlackRatio ?? "-"}`),
],
})),
],
});
}
function issueTable(failedCases) {
if (failedCases.length === 0) return para("未发现失败场景。");
return new Table({
width: { size: 100, type: WidthType.PERCENTAGE },
rows: [
new TableRow({ children: [cell("场景", true), cell("失败检查", true), cell("影响", true), cell("建议", true)] }),
...failedCases.map((testCase) => new TableRow({
children: [
cell(testCase.name),
cell(testCase.checks.filter((item) => !item.pass).map((item) => `${item.name}: ${item.detail}`).join("\n")),
cell("G-code 运行态下 RTCP/TCP 轨迹状态与预览态不一致,可能误导操作者判断刀具姿态和 TCP 执行轨迹。"),
cell("检查 RUN/task-HAL status 合并逻辑,避免运行反馈将 RTCP/kinesType 从已加载程序的 TCP 状态回退到 identity/off。"),
],
})),
],
});
}
async function caseBlock(testCase) {
const screenshotPath = report.screenshots[testCase.name];
const blocks = [
new Paragraph({
text: `${testCase.name} - ${testCase.status}`,
heading: HeadingLevel.HEADING_2,
spacing: { before: 180, after: 120 },
}),
para(testCase.summary),
kvTable([
["activeProgram", testCase.state.activeProgram],
["programSource", testCase.state.programSource],
["programExecutionSourceMode", testCase.state.programExecutionSourceMode],
["runState", testCase.state.runState],
["rtcpState / kinsType", `${testCase.state.rtcpState} / ${testCase.state.kinsType}`],
["motion/sample index", `${testCase.state.programExecutionMotionIndex} / ${testCase.state.programExecutionSampleIndex}`],
["runtime feedback", testCase.state.programRuntimeFeedbackSource || "-"],
["path/executed/rapid/feed/arc", `${testCase.dataset.threePathPoints}/${testCase.dataset.threeExecutedPathPoints}/${testCase.dataset.threeRapidPathPoints}/${testCase.dataset.threeFeedPathPoints}/${testCase.dataset.threeArcPathPoints}`],
["renderer/model", `${testCase.dataset.threeRenderer} / ${testCase.dataset.threeMachineReferenceModel}`],
["pixel stats", `averageLuminance=${testCase.pixelStats?.averageLuminance ?? "-"}, nonBlackRatio=${testCase.pixelStats?.nonBlackRatio ?? "-"}`],
]),
new Paragraph({
text: "检查项",
heading: HeadingLevel.HEADING_3,
spacing: { before: 120, after: 80 },
}),
checksTable(testCase.checks),
];
if (screenshotPath) {
const image = await fs.readFile(screenshotPath);
blocks.push(
new Paragraph({
alignment: AlignmentType.CENTER,
children: [
new ImageRun({
data: image,
type: "png",
transformation: { width: 520, height: 390 },
}),
],
}),
para(`截图文件:${screenshotPath}`),
);
}
return blocks;
}
function checksTable(checks) {
return new Table({
width: { size: 100, type: WidthType.PERCENTAGE },
rows: [
new TableRow({ children: [cell("检查", true), cell("结果", true), cell("证据", true)] }),
...checks.map((item) => new TableRow({
children: [cell(item.name), cell(item.pass ? "PASS" : "FAIL"), cell(item.detail)],
})),
],
});
}
function cell(text, bold = false) {
return new TableCell({
width: { size: 25, type: WidthType.PERCENTAGE },
children: String(text ?? "-").split("\n").map((line) => new Paragraph({
children: [new TextRun({ text: line, bold })],
})),
});
}
function formatDateTime(date) {
return date.toISOString().replace("T", " ").replace(/\.\d+Z$/, " UTC");
}

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,881 @@
{
"generatedAt": "2026-06-22T13:12:11.146Z",
"targetUrl": "http://127.0.0.1:43627/web-rtcp-5axis-sim-plan/app/index.html",
"projectPath": "/home/meswork/cnc_wams/web-rtcp-5axis-sim-plan/app/index.html",
"chromePath": "/usr/bin/google-chrome",
"screenshots": {
"01-home-toolpath": "/home/meswork/cnc_wams/qa/web-rtcp-5axis-site-test/screenshots/toolpath-preview-cases/01-home-toolpath.png",
"02-operator-demo-toolpath": "/home/meswork/cnc_wams/qa/web-rtcp-5axis-site-test/screenshots/toolpath-preview-cases/02-operator-demo-toolpath.png",
"03-arc-demo-toolpath": "/home/meswork/cnc_wams/qa/web-rtcp-5axis-site-test/screenshots/toolpath-preview-cases/03-arc-demo-toolpath.png",
"04-clear-preview-reference": "/home/meswork/cnc_wams/qa/web-rtcp-5axis-site-test/screenshots/toolpath-preview-cases/04-clear-preview-reference.png",
"05-vendored-impeller-toolpath": "/home/meswork/cnc_wams/qa/web-rtcp-5axis-site-test/screenshots/toolpath-preview-cases/05-vendored-impeller-toolpath.png",
"06-running-rtcp-toolpath": "/home/meswork/cnc_wams/qa/web-rtcp-5axis-site-test/screenshots/toolpath-preview-cases/06-running-rtcp-toolpath.png"
},
"cases": [
{
"name": "01-home-toolpath",
"summary": "默认首屏预览机床参考模型、TCP 球、刀轴线、fixture 路径和执行轨迹可见",
"status": "PASS",
"checks": [
{
"name": "canvas ready",
"pass": true,
"detail": "threeReady=true"
},
{
"name": "WebGL renderer",
"pass": true,
"detail": "renderer=webgl"
},
{
"name": "机床参考模型",
"pass": true,
"detail": "scope=machine-reference-and-toolpath, model=webgl-five-axis-reference"
},
{
"name": "TCP 球标记",
"pass": true,
"detail": "tcp=sphere, marker=true"
},
{
"name": "刀轴线",
"pass": true,
"detail": "toolAxisMarker=line"
},
{
"name": "场景对象数量",
"pass": true,
"detail": "sceneObjects=20"
},
{
"name": "无 G-code 语义生成",
"pass": true,
"detail": "semanticGuard=ok"
},
{
"name": "刀路预览点",
"pass": true,
"detail": "pathPoints=2"
},
{
"name": "执行轨迹点",
"pass": true,
"detail": "executed=1"
}
],
"pixelStats": {
"width": 803,
"height": 874,
"averageLuminance": 62.52,
"nonBlackRatio": 0.7491
},
"dataset": {
"fiveAxisCanvas": "true",
"engine": "three.js r183",
"threeReady": "true",
"threeRevision": "183",
"threePathPoints": "2",
"threeExecutedPathPoints": "1",
"threeSceneObjects": "20",
"threeToolhead": "{\"x\":0,\"y\":0,\"z\":0.35}",
"threeToolAxis": "{\"x\":0,\"y\":0,\"z\":1}",
"threeTcpPose": "{\"x\":0,\"y\":0,\"z\":0,\"a\":0,\"c\":0}",
"threeRtcpState": "on",
"threeSelectedView": "iso",
"threeFrameApi": "web-rtcp-5axis-motion-frame",
"threeRenderer": "webgl",
"threeSceneMode": "program-preview-and-tool-execution",
"threePreviewScope": "machine-reference-and-toolpath",
"threeMachineReferenceModel": "webgl-five-axis-reference",
"threeCameraControls": "orbit-pan-zoom",
"threeProgramPreviewSource": "linuxcnc-interpreter-wasm",
"threeToolExecutionMarker": "true",
"threeTcpMarker": "sphere",
"threeToolAxisMarker": "line",
"threeToolpathPreviewSource": "linuxcnc_interpreter_canonical_motion",
"threeToolExecutionTraceSource": "linuxcnc_tp_samples_or_task_motion_hal_feedback",
"threePathFitBounds": "ok",
"threeCurrentSegmentHighlight": "ok",
"threeRapidFeedVisualDistinction": "ok",
"threeNoGcodeSemanticsGeneration": "ok",
"threeRapidPathPoints": "2",
"threeFeedPathPoints": "0",
"threeArcPathPoints": "0",
"threeCurrentSegmentPoints": "2",
"threeCurrentSegmentType": "STRAIGHT_TRAVERSE"
},
"state": {
"activeProgram": "configs/sim/axis/vismach/5axis/table-rotary-tilting/demos/boat-xyzac.ngc",
"activeLine": 1,
"programSource": "linuxcnc-vendored-5axis-gcode",
"programExecutionSourceMode": "linuxcnc-interpreter-wasm",
"programExecutionSummary": {
"ready": true,
"programLineCount": 1868,
"canonicalEventCount": 29,
"motionEventCount": 2,
"motionTypes": [
"STRAIGHT_TRAVERSE"
],
"finalAxes": {
"x": 0,
"y": 0,
"z": 5,
"a": 0,
"b": 0,
"c": 0,
"u": 0,
"v": 0,
"w": 0
},
"switchkinsEventCount": 2,
"switchkinsCodes": [
"M428",
"M429"
],
"switchkinsRemapBoundary": "linuxcnc_switchkins_remap_mcode_preserved_web_runtime_applied",
"remapRuntimeReady": false,
"plannerRuntimeReady": true,
"plannerSemanticBoundary": "linuxcnc_tp_queue_runtime_timing_from_canonical_motion",
"machineFileExecutionReady": false,
"fullLinuxCncProgramExecutionReady": false
},
"runState": "idle",
"rtcpState": "on",
"kinsType": "tcp-xyzac",
"programExecutionMotionIndex": 0,
"programExecutionSampleIndex": 0,
"programRuntimeFeedbackSource": "linuxcnc-task-motion-hal-wasm"
}
},
{
"name": "02-operator-demo-toolpath",
"summary": "本地矩形 G-code验证 LinuxCNC interpreter canonical motion、rapid/feed 区分、执行轨迹和 TCP 标记",
"status": "PASS",
"checks": [
{
"name": "canvas ready",
"pass": true,
"detail": "threeReady=true"
},
{
"name": "WebGL renderer",
"pass": true,
"detail": "renderer=webgl"
},
{
"name": "机床参考模型",
"pass": true,
"detail": "scope=machine-reference-and-toolpath, model=webgl-five-axis-reference"
},
{
"name": "TCP 球标记",
"pass": true,
"detail": "tcp=sphere, marker=true"
},
{
"name": "刀轴线",
"pass": true,
"detail": "toolAxisMarker=line"
},
{
"name": "场景对象数量",
"pass": true,
"detail": "sceneObjects=20"
},
{
"name": "无 G-code 语义生成",
"pass": true,
"detail": "semanticGuard=ok"
},
{
"name": "刀路预览点",
"pass": true,
"detail": "pathPoints=6"
},
{
"name": "执行轨迹点",
"pass": true,
"detail": "executed=1"
},
{
"name": "rapid/feed 区分",
"pass": true,
"detail": "rapid=2, feed=4"
}
],
"pixelStats": {
"width": 803,
"height": 874,
"averageLuminance": 59.4,
"nonBlackRatio": 0.7092
},
"dataset": {
"fiveAxisCanvas": "true",
"engine": "three.js r183",
"threeReady": "true",
"threeRevision": "183",
"threePathPoints": "6",
"threeExecutedPathPoints": "1",
"threeSceneObjects": "20",
"threeToolhead": "{\"x\":0,\"y\":0,\"z\":0.35}",
"threeToolAxis": "{\"x\":0,\"y\":0,\"z\":1}",
"threeTcpPose": "{\"x\":0,\"y\":0,\"z\":0,\"a\":0,\"c\":0}",
"threeRtcpState": "on",
"threeSelectedView": "iso",
"threeFrameApi": "web-rtcp-5axis-motion-frame",
"threeRenderer": "webgl",
"threeSceneMode": "program-preview-and-tool-execution",
"threePreviewScope": "machine-reference-and-toolpath",
"threeMachineReferenceModel": "webgl-five-axis-reference",
"threeCameraControls": "orbit-pan-zoom",
"threeProgramPreviewSource": "linuxcnc-interpreter-wasm",
"threeToolExecutionMarker": "true",
"threeTcpMarker": "sphere",
"threeToolAxisMarker": "line",
"threeToolpathPreviewSource": "linuxcnc_interpreter_canonical_motion",
"threeToolExecutionTraceSource": "linuxcnc_tp_samples_or_task_motion_hal_feedback",
"threePathFitBounds": "ok",
"threeCurrentSegmentHighlight": "ok",
"threeRapidFeedVisualDistinction": "ok",
"threeNoGcodeSemanticsGeneration": "ok",
"threeRapidPathPoints": "2",
"threeFeedPathPoints": "4",
"threeArcPathPoints": "0",
"threeCurrentSegmentPoints": "2",
"threeCurrentSegmentType": "STRAIGHT_FEED"
},
"state": {
"activeProgram": "operator-demo.ngc",
"activeLine": 2,
"programSource": "operator-file",
"programExecutionSourceMode": "linuxcnc-interpreter-wasm",
"programExecutionSummary": {
"ready": true,
"programLineCount": 9,
"canonicalEventCount": 37,
"motionEventCount": 6,
"motionTypes": [
"STRAIGHT_TRAVERSE",
"STRAIGHT_FEED"
],
"finalAxes": {
"x": 0,
"y": 0,
"z": 5,
"a": 0,
"b": 0,
"c": 0,
"u": 0,
"v": 0,
"w": 0
},
"switchkinsEventCount": 0,
"switchkinsCodes": [],
"switchkinsRemapBoundary": null,
"remapRuntimeReady": false,
"plannerRuntimeReady": true,
"plannerSemanticBoundary": "linuxcnc_tp_queue_runtime_timing_from_canonical_motion",
"machineFileExecutionReady": false,
"fullLinuxCncProgramExecutionReady": false
},
"runState": "idle",
"rtcpState": "on",
"kinsType": "tcp-xyzac",
"programExecutionMotionIndex": 0,
"programExecutionSampleIndex": 0,
"programRuntimeFeedbackSource": "linuxcnc-tp-runtime-sample"
}
},
{
"name": "03-arc-demo-toolpath",
"summary": "圆弧 G-code验证 ARC_FEED 进入弧线轨迹图层并保留执行轨迹",
"status": "PASS",
"checks": [
{
"name": "canvas ready",
"pass": true,
"detail": "threeReady=true"
},
{
"name": "WebGL renderer",
"pass": true,
"detail": "renderer=webgl"
},
{
"name": "机床参考模型",
"pass": true,
"detail": "scope=machine-reference-and-toolpath, model=webgl-five-axis-reference"
},
{
"name": "TCP 球标记",
"pass": true,
"detail": "tcp=sphere, marker=true"
},
{
"name": "刀轴线",
"pass": true,
"detail": "toolAxisMarker=line"
},
{
"name": "场景对象数量",
"pass": true,
"detail": "sceneObjects=20"
},
{
"name": "无 G-code 语义生成",
"pass": true,
"detail": "semanticGuard=ok"
},
{
"name": "刀路预览点",
"pass": true,
"detail": "pathPoints=2"
},
{
"name": "执行轨迹点",
"pass": true,
"detail": "executed=1"
},
{
"name": "圆弧轨迹点",
"pass": true,
"detail": "arc=1"
}
],
"pixelStats": {
"width": 803,
"height": 874,
"averageLuminance": 65.26,
"nonBlackRatio": 0.7885
},
"dataset": {
"fiveAxisCanvas": "true",
"engine": "three.js r183",
"threeReady": "true",
"threeRevision": "183",
"threePathPoints": "2",
"threeExecutedPathPoints": "1",
"threeSceneObjects": "20",
"threeToolhead": "{\"x\":0,\"y\":0,\"z\":0.35}",
"threeToolAxis": "{\"x\":0,\"y\":0,\"z\":1}",
"threeTcpPose": "{\"x\":1,\"y\":0,\"z\":0,\"a\":0,\"c\":0}",
"threeRtcpState": "on",
"threeSelectedView": "iso",
"threeFrameApi": "web-rtcp-5axis-motion-frame",
"threeRenderer": "webgl",
"threeSceneMode": "program-preview-and-tool-execution",
"threePreviewScope": "machine-reference-and-toolpath",
"threeMachineReferenceModel": "webgl-five-axis-reference",
"threeCameraControls": "orbit-pan-zoom",
"threeProgramPreviewSource": "linuxcnc-interpreter-wasm",
"threeToolExecutionMarker": "true",
"threeTcpMarker": "sphere",
"threeToolAxisMarker": "line",
"threeToolpathPreviewSource": "linuxcnc_interpreter_canonical_motion",
"threeToolExecutionTraceSource": "linuxcnc_tp_samples_or_task_motion_hal_feedback",
"threePathFitBounds": "ok",
"threeCurrentSegmentHighlight": "ok",
"threeRapidFeedVisualDistinction": "ok",
"threeNoGcodeSemanticsGeneration": "ok",
"threeRapidPathPoints": "1",
"threeFeedPathPoints": "0",
"threeArcPathPoints": "1",
"threeCurrentSegmentPoints": "1",
"threeCurrentSegmentType": "STRAIGHT_TRAVERSE"
},
"state": {
"activeProgram": "operator-arc-demo.ngc",
"activeLine": 2,
"programSource": "operator-file",
"programExecutionSourceMode": "linuxcnc-interpreter-wasm",
"programExecutionSummary": {
"ready": true,
"programLineCount": 4,
"canonicalEventCount": 19,
"motionEventCount": 2,
"motionTypes": [
"STRAIGHT_TRAVERSE",
"ARC_FEED"
],
"finalAxes": {
"x": 0,
"y": 1,
"z": 0,
"a": 0,
"b": 0,
"c": 0,
"u": 0,
"v": 0,
"w": 0,
"arc": {
"plane": 170,
"firstAxis": "x",
"secondAxis": "y",
"thirdAxis": "z",
"firstEnd": 0,
"secondEnd": 1,
"centerFirst": 0,
"centerSecond": 0,
"rotation": -1,
"axisEndPoint": 0
}
},
"switchkinsEventCount": 0,
"switchkinsCodes": [],
"switchkinsRemapBoundary": null,
"remapRuntimeReady": false,
"plannerRuntimeReady": true,
"plannerSemanticBoundary": "linuxcnc_tp_queue_runtime_timing_from_canonical_motion",
"machineFileExecutionReady": false,
"fullLinuxCncProgramExecutionReady": false
},
"runState": "idle",
"rtcpState": "on",
"kinsType": "tcp-xyzac",
"programExecutionMotionIndex": 0,
"programExecutionSampleIndex": 0,
"programRuntimeFeedbackSource": "linuxcnc-tp-runtime-sample"
}
},
{
"name": "04-clear-preview-reference",
"summary": "清空刀路后:路径点为 0但机床参考模型、TCP 球和刀轴线仍应可见",
"status": "PASS",
"checks": [
{
"name": "canvas ready",
"pass": true,
"detail": "threeReady=true"
},
{
"name": "WebGL renderer",
"pass": true,
"detail": "renderer=webgl"
},
{
"name": "机床参考模型",
"pass": true,
"detail": "scope=machine-reference-and-toolpath, model=webgl-five-axis-reference"
},
{
"name": "TCP 球标记",
"pass": true,
"detail": "tcp=sphere, marker=true"
},
{
"name": "刀轴线",
"pass": true,
"detail": "toolAxisMarker=line"
},
{
"name": "场景对象数量",
"pass": true,
"detail": "sceneObjects=20"
},
{
"name": "无 G-code 语义生成",
"pass": true,
"detail": "semanticGuard=ok"
},
{
"name": "路径点期望",
"pass": true,
"detail": "pathPoints=0, expected=0"
}
],
"pixelStats": {
"width": 803,
"height": 874,
"averageLuminance": 64.72,
"nonBlackRatio": 0.7874
},
"dataset": {
"fiveAxisCanvas": "true",
"engine": "three.js r183",
"threeReady": "true",
"threeRevision": "183",
"threePathPoints": "0",
"threeExecutedPathPoints": "0",
"threeSceneObjects": "20",
"threeToolhead": "{\"x\":0,\"y\":0,\"z\":0.35}",
"threeToolAxis": "{\"x\":0,\"y\":0,\"z\":1}",
"threeTcpPose": "{\"x\":1,\"y\":0,\"z\":0,\"a\":0,\"c\":0}",
"threeRtcpState": "on",
"threeSelectedView": "iso",
"threeFrameApi": "web-rtcp-5axis-motion-frame",
"threeRenderer": "webgl",
"threeSceneMode": "program-preview-and-tool-execution",
"threePreviewScope": "machine-reference-and-toolpath",
"threeMachineReferenceModel": "webgl-five-axis-reference",
"threeCameraControls": "orbit-pan-zoom",
"threeProgramPreviewSource": "linuxcnc-interpreter-wasm",
"threeToolExecutionMarker": "true",
"threeTcpMarker": "sphere",
"threeToolAxisMarker": "line",
"threeToolpathPreviewSource": "linuxcnc_interpreter_canonical_motion",
"threeToolExecutionTraceSource": "linuxcnc_tp_samples_or_task_motion_hal_feedback",
"threePathFitBounds": "ok",
"threeCurrentSegmentHighlight": "pending",
"threeRapidFeedVisualDistinction": "pending",
"threeNoGcodeSemanticsGeneration": "ok",
"threeRapidPathPoints": "0",
"threeFeedPathPoints": "0",
"threeArcPathPoints": "0",
"threeCurrentSegmentPoints": "0",
"threeCurrentSegmentType": "STRAIGHT_TRAVERSE"
},
"state": {
"activeProgram": "operator-arc-demo.ngc",
"activeLine": 2,
"programSource": "operator-file",
"programExecutionSourceMode": "linuxcnc-interpreter-wasm",
"programExecutionSummary": {
"ready": true,
"programLineCount": 4,
"canonicalEventCount": 19,
"motionEventCount": 2,
"motionTypes": [
"STRAIGHT_TRAVERSE",
"ARC_FEED"
],
"finalAxes": {
"x": 0,
"y": 1,
"z": 0,
"a": 0,
"b": 0,
"c": 0,
"u": 0,
"v": 0,
"w": 0,
"arc": {
"plane": 170,
"firstAxis": "x",
"secondAxis": "y",
"thirdAxis": "z",
"firstEnd": 0,
"secondEnd": 1,
"centerFirst": 0,
"centerSecond": 0,
"rotation": -1,
"axisEndPoint": 0
}
},
"switchkinsEventCount": 0,
"switchkinsCodes": [],
"switchkinsRemapBoundary": null,
"remapRuntimeReady": false,
"plannerRuntimeReady": true,
"plannerSemanticBoundary": "linuxcnc_tp_queue_runtime_timing_from_canonical_motion",
"machineFileExecutionReady": false,
"fullLinuxCncProgramExecutionReady": false
},
"runState": "idle",
"rtcpState": "on",
"kinsType": "tcp-xyzac",
"programExecutionMotionIndex": 0,
"programExecutionSampleIndex": 0,
"programRuntimeFeedbackSource": "linuxcnc-tp-runtime-sample"
}
},
{
"name": "05-vendored-impeller-toolpath",
"summary": "LinuxCNC vendored 五轴 impeller 程序验证长路径、switchkins/RTCP 状态、rapid/feed 图层和 TCP 执行轨迹",
"status": "PASS",
"checks": [
{
"name": "canvas ready",
"pass": true,
"detail": "threeReady=true"
},
{
"name": "WebGL renderer",
"pass": true,
"detail": "renderer=webgl"
},
{
"name": "机床参考模型",
"pass": true,
"detail": "scope=machine-reference-and-toolpath, model=webgl-five-axis-reference"
},
{
"name": "TCP 球标记",
"pass": true,
"detail": "tcp=sphere, marker=true"
},
{
"name": "刀轴线",
"pass": true,
"detail": "toolAxisMarker=line"
},
{
"name": "场景对象数量",
"pass": true,
"detail": "sceneObjects=20"
},
{
"name": "无 G-code 语义生成",
"pass": true,
"detail": "semanticGuard=ok"
},
{
"name": "刀路预览点",
"pass": true,
"detail": "pathPoints=1498"
},
{
"name": "执行轨迹点",
"pass": true,
"detail": "executed=1"
},
{
"name": "rapid/feed 区分",
"pass": true,
"detail": "rapid=186, feed=1436"
},
{
"name": "RTCP 状态",
"pass": true,
"detail": "rtcp=on"
}
],
"pixelStats": {
"width": 803,
"height": 874,
"averageLuminance": 35.56,
"nonBlackRatio": 0.4577
},
"dataset": {
"fiveAxisCanvas": "true",
"engine": "three.js r183",
"threeReady": "true",
"threeRevision": "183",
"threePathPoints": "1498",
"threeExecutedPathPoints": "1",
"threeSceneObjects": "20",
"threeToolhead": "{\"x\":0,\"y\":0,\"z\":0.35}",
"threeToolAxis": "{\"x\":0.558,\"y\":0.769,\"z\":0.312}",
"threeTcpPose": "{\"x\":36.474,\"y\":-22.486,\"z\":-13.749,\"a\":-71.841,\"c\":-35.93}",
"threeRtcpState": "on",
"threeSelectedView": "iso",
"threeFrameApi": "web-rtcp-5axis-motion-frame",
"threeRenderer": "webgl",
"threeSceneMode": "program-preview-and-tool-execution",
"threePreviewScope": "machine-reference-and-toolpath",
"threeMachineReferenceModel": "webgl-five-axis-reference",
"threeCameraControls": "orbit-pan-zoom",
"threeProgramPreviewSource": "linuxcnc-interpreter-wasm",
"threeToolExecutionMarker": "true",
"threeTcpMarker": "sphere",
"threeToolAxisMarker": "line",
"threeToolpathPreviewSource": "linuxcnc_interpreter_canonical_motion",
"threeToolExecutionTraceSource": "linuxcnc_tp_samples_or_task_motion_hal_feedback",
"threePathFitBounds": "ok",
"threeCurrentSegmentHighlight": "ok",
"threeRapidFeedVisualDistinction": "ok",
"threeNoGcodeSemanticsGeneration": "ok",
"threeRapidPathPoints": "186",
"threeFeedPathPoints": "1436",
"threeArcPathPoints": "0",
"threeCurrentSegmentPoints": "1",
"threeCurrentSegmentType": "STRAIGHT_TRAVERSE"
},
"state": {
"activeProgram": "configs/sim/axis/vismach/5axis/table-rotary-tilting/demos/impeller-7bl-xyzac.ngc",
"activeLine": 8,
"programSource": "linuxcnc-vendored-5axis-gcode",
"programExecutionSourceMode": "linuxcnc-interpreter-wasm",
"programExecutionSummary": {
"ready": true,
"programLineCount": 4507,
"canonicalEventCount": 22313,
"motionEventCount": 4492,
"motionTypes": [
"STRAIGHT_TRAVERSE",
"STRAIGHT_FEED"
],
"finalAxes": {
"x": 0,
"y": 0,
"z": 40,
"a": 0,
"b": 0,
"c": 0,
"u": 0,
"v": 0,
"w": 0
},
"switchkinsEventCount": 2,
"switchkinsCodes": [
"M428",
"M429"
],
"switchkinsRemapBoundary": "linuxcnc_switchkins_remap_mcode_preserved_web_runtime_applied",
"remapRuntimeReady": false,
"plannerRuntimeReady": true,
"plannerSemanticBoundary": "linuxcnc_tp_queue_runtime_timing_from_canonical_motion",
"machineFileExecutionReady": false,
"fullLinuxCncProgramExecutionReady": false
},
"runState": "idle",
"rtcpState": "on",
"kinsType": "tcp-xyzac",
"programExecutionMotionIndex": 0,
"programExecutionSampleIndex": 0,
"programRuntimeFeedbackSource": "linuxcnc-tp-runtime-sample"
}
},
{
"name": "06-running-rtcp-toolpath",
"summary": "G-code 运行态:验证 task/HAL runtime feedback 驱动执行轨迹、当前段高亮、TCP 球和刀轴线跟随",
"status": "PASS",
"checks": [
{
"name": "canvas ready",
"pass": true,
"detail": "threeReady=true"
},
{
"name": "WebGL renderer",
"pass": true,
"detail": "renderer=webgl"
},
{
"name": "机床参考模型",
"pass": true,
"detail": "scope=machine-reference-and-toolpath, model=webgl-five-axis-reference"
},
{
"name": "TCP 球标记",
"pass": true,
"detail": "tcp=sphere, marker=true"
},
{
"name": "刀轴线",
"pass": true,
"detail": "toolAxisMarker=line"
},
{
"name": "场景对象数量",
"pass": true,
"detail": "sceneObjects=20"
},
{
"name": "无 G-code 语义生成",
"pass": true,
"detail": "semanticGuard=ok"
},
{
"name": "刀路预览点",
"pass": true,
"detail": "pathPoints=1498"
},
{
"name": "执行轨迹点",
"pass": true,
"detail": "executed=1"
},
{
"name": "rapid/feed 区分",
"pass": true,
"detail": "rapid=186, feed=1436"
},
{
"name": "RTCP 状态",
"pass": true,
"detail": "rtcp=on"
}
],
"pixelStats": {
"width": 803,
"height": 874,
"averageLuminance": 36.05,
"nonBlackRatio": 0.4607
},
"dataset": {
"fiveAxisCanvas": "true",
"engine": "three.js r183",
"threeReady": "true",
"threeRevision": "183",
"threePathPoints": "1498",
"threeExecutedPathPoints": "1",
"threeSceneObjects": "20",
"threeToolhead": "{\"x\":0.26,\"y\":-0.458,\"z\":1.485}",
"threeToolAxis": "{\"x\":0.558,\"y\":0.769,\"z\":0.312}",
"threeTcpPose": "{\"x\":7.417,\"y\":-13.098,\"z\":28.366,\"a\":-71.841,\"c\":-35.93}",
"threeRtcpState": "on",
"threeSelectedView": "iso",
"threeFrameApi": "web-rtcp-5axis-motion-frame",
"threeRenderer": "webgl",
"threeSceneMode": "program-preview-and-tool-execution",
"threePreviewScope": "machine-reference-and-toolpath",
"threeMachineReferenceModel": "webgl-five-axis-reference",
"threeCameraControls": "orbit-pan-zoom",
"threeProgramPreviewSource": "linuxcnc-interpreter-wasm",
"threeToolExecutionMarker": "true",
"threeTcpMarker": "sphere",
"threeToolAxisMarker": "line",
"threeToolpathPreviewSource": "linuxcnc_interpreter_canonical_motion",
"threeToolExecutionTraceSource": "linuxcnc_tp_samples_or_task_motion_hal_feedback",
"threePathFitBounds": "ok",
"threeCurrentSegmentHighlight": "ok",
"threeRapidFeedVisualDistinction": "ok",
"threeNoGcodeSemanticsGeneration": "ok",
"threeRapidPathPoints": "186",
"threeFeedPathPoints": "1436",
"threeArcPathPoints": "0",
"threeCurrentSegmentPoints": "1",
"threeCurrentSegmentType": "STRAIGHT_TRAVERSE"
},
"state": {
"activeProgram": "configs/sim/axis/vismach/5axis/table-rotary-tilting/demos/impeller-7bl-xyzac.ngc",
"activeLine": 5,
"programSource": "linuxcnc-vendored-5axis-gcode",
"programExecutionSourceMode": "linuxcnc-interpreter-wasm",
"programExecutionSummary": {
"ready": true,
"programLineCount": 4507,
"canonicalEventCount": 22313,
"motionEventCount": 4492,
"motionTypes": [
"STRAIGHT_TRAVERSE",
"STRAIGHT_FEED"
],
"finalAxes": {
"x": 0,
"y": 0,
"z": 40,
"a": 0,
"b": 0,
"c": 0,
"u": 0,
"v": 0,
"w": 0
},
"switchkinsEventCount": 2,
"switchkinsCodes": [
"M428",
"M429"
],
"switchkinsRemapBoundary": "linuxcnc_switchkins_remap_mcode_preserved_web_runtime_applied",
"remapRuntimeReady": false,
"plannerRuntimeReady": true,
"plannerSemanticBoundary": "linuxcnc_tp_queue_runtime_timing_from_canonical_motion",
"machineFileExecutionReady": false,
"fullLinuxCncProgramExecutionReady": false
},
"runState": "running",
"rtcpState": "on",
"kinsType": "tcp-xyzac",
"programExecutionMotionIndex": 0,
"programExecutionSampleIndex": 0,
"programRuntimeFeedbackSource": "linuxcnc-task-motion-hal-wasm"
}
}
],
"consoleErrors": [
"Failed to load resource: the server responded with a status of 404 (Not Found)",
"Failed to load resource: the server responded with a status of 404 (Not Found)"
]
}

View File

@@ -0,0 +1,516 @@
{
"name": "web-rtcp-5axis-site-test",
"version": "1.0.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "web-rtcp-5axis-site-test",
"version": "1.0.0",
"license": "ISC",
"dependencies": {
"docx": "^9.7.1",
"pngjs": "^7.0.0",
"puppeteer-core": "^25.1.0"
}
},
"node_modules/@puppeteer/browsers": {
"version": "3.0.4",
"resolved": "https://registry.npmjs.org/@puppeteer/browsers/-/browsers-3.0.4.tgz",
"integrity": "sha512-HGM8iAmGTf+Y7t0373szVbTmt3d7vPkYL/1bpOkOFO0YUYLgSeuYBCzESklogNPvOBnZ/MRD5f07OkpqH1trtA==",
"license": "Apache-2.0",
"dependencies": {
"modern-tar": "^0.7.6",
"yargs": "^17.7.2"
},
"bin": {
"browsers": "lib/main-cli.js"
},
"engines": {
"node": ">=22.12.0"
},
"peerDependencies": {
"proxy-agent": ">=8.0.1"
},
"peerDependenciesMeta": {
"proxy-agent": {
"optional": true
}
}
},
"node_modules/@types/node": {
"version": "25.9.4",
"resolved": "https://registry.npmjs.org/@types/node/-/node-25.9.4.tgz",
"integrity": "sha512-dszCsrKb5U7ZsVZBWiHFklTloVl0mSEnWH/iZXfZUlI4rzCUnsvGmgqfuVRHL54ugE7/wRuxEIXRa2iMZ+BG6g==",
"license": "MIT",
"dependencies": {
"undici-types": ">=7.24.0 <7.24.7"
}
},
"node_modules/ansi-regex": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
"integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
"license": "MIT",
"engines": {
"node": ">=8"
}
},
"node_modules/ansi-styles": {
"version": "4.3.0",
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
"integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
"license": "MIT",
"dependencies": {
"color-convert": "^2.0.1"
},
"engines": {
"node": ">=8"
},
"funding": {
"url": "https://github.com/chalk/ansi-styles?sponsor=1"
}
},
"node_modules/chromium-bidi": {
"version": "16.0.1",
"resolved": "https://registry.npmjs.org/chromium-bidi/-/chromium-bidi-16.0.1.tgz",
"integrity": "sha512-J63PGu/9PpeCwLIcKYyzWP6yaVL5pxuBc0shlYCYM8BaAkmlwiQboXO1iNbOgSDbVklEyYFfNEcHD8oOAWacUA==",
"license": "Apache-2.0",
"dependencies": {
"mitt": "^3.0.1",
"zod": "^3.24.1"
},
"engines": {
"node": ">=20.19.0 <22.0.0 || >=22.12.0"
},
"peerDependencies": {
"devtools-protocol": "*"
}
},
"node_modules/cliui": {
"version": "8.0.1",
"resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz",
"integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==",
"license": "ISC",
"dependencies": {
"string-width": "^4.2.0",
"strip-ansi": "^6.0.1",
"wrap-ansi": "^7.0.0"
},
"engines": {
"node": ">=12"
}
},
"node_modules/color-convert": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
"integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
"license": "MIT",
"dependencies": {
"color-name": "~1.1.4"
},
"engines": {
"node": ">=7.0.0"
}
},
"node_modules/color-name": {
"version": "1.1.4",
"resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
"integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
"license": "MIT"
},
"node_modules/core-util-is": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz",
"integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==",
"license": "MIT"
},
"node_modules/devtools-protocol": {
"version": "0.0.1624250",
"resolved": "https://registry.npmjs.org/devtools-protocol/-/devtools-protocol-0.0.1624250.tgz",
"integrity": "sha512-YFAat/lOiIk0ARmBweG+ygrEcbZrq5B9urRyUoeQKp53MlidHXE2TmTbxKcaXoQj7u/aX+jebDO4BW55rs0WwA==",
"license": "BSD-3-Clause"
},
"node_modules/docx": {
"version": "9.7.1",
"resolved": "https://registry.npmjs.org/docx/-/docx-9.7.1.tgz",
"integrity": "sha512-ilXFf9Moz47ABjFpDiA5s1w9lpb4EFSp7+5iiJSbfyYDM+bpZdAgLlSr7fW4aXhVe/E+F6QCv0EvRVFEd5CsWg==",
"license": "MIT",
"dependencies": {
"@types/node": "^25.2.3",
"hash.js": "^1.1.7",
"jszip": "^3.10.1",
"nanoid": "^5.1.3",
"xml": "^1.0.1",
"xml-js": "^1.6.8"
},
"engines": {
"node": ">=10"
}
},
"node_modules/emoji-regex": {
"version": "8.0.0",
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
"integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
"license": "MIT"
},
"node_modules/escalade": {
"version": "3.2.0",
"resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz",
"integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==",
"license": "MIT",
"engines": {
"node": ">=6"
}
},
"node_modules/get-caller-file": {
"version": "2.0.5",
"resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz",
"integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==",
"license": "ISC",
"engines": {
"node": "6.* || 8.* || >= 10.*"
}
},
"node_modules/hash.js": {
"version": "1.1.7",
"resolved": "https://registry.npmjs.org/hash.js/-/hash.js-1.1.7.tgz",
"integrity": "sha512-taOaskGt4z4SOANNseOviYDvjEJinIkRgmp7LbKP2YTTmVxWBl87s/uzK9r+44BclBSp2X7K1hqeNfz9JbBeXA==",
"license": "MIT",
"dependencies": {
"inherits": "^2.0.3",
"minimalistic-assert": "^1.0.1"
}
},
"node_modules/immediate": {
"version": "3.0.6",
"resolved": "https://registry.npmjs.org/immediate/-/immediate-3.0.6.tgz",
"integrity": "sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ==",
"license": "MIT"
},
"node_modules/inherits": {
"version": "2.0.4",
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
"integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==",
"license": "ISC"
},
"node_modules/is-fullwidth-code-point": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz",
"integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==",
"license": "MIT",
"engines": {
"node": ">=8"
}
},
"node_modules/isarray": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz",
"integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==",
"license": "MIT"
},
"node_modules/jszip": {
"version": "3.10.1",
"resolved": "https://registry.npmjs.org/jszip/-/jszip-3.10.1.tgz",
"integrity": "sha512-xXDvecyTpGLrqFrvkrUSoxxfJI5AH7U8zxxtVclpsUtMCq4JQ290LY8AW5c7Ggnr/Y/oK+bQMbqK2qmtk3pN4g==",
"license": "(MIT OR GPL-3.0-or-later)",
"dependencies": {
"lie": "~3.3.0",
"pako": "~1.0.2",
"readable-stream": "~2.3.6",
"setimmediate": "^1.0.5"
}
},
"node_modules/lie": {
"version": "3.3.0",
"resolved": "https://registry.npmjs.org/lie/-/lie-3.3.0.tgz",
"integrity": "sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ==",
"license": "MIT",
"dependencies": {
"immediate": "~3.0.5"
}
},
"node_modules/minimalistic-assert": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz",
"integrity": "sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A==",
"license": "ISC"
},
"node_modules/mitt": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/mitt/-/mitt-3.0.1.tgz",
"integrity": "sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw==",
"license": "MIT"
},
"node_modules/modern-tar": {
"version": "0.7.6",
"resolved": "https://registry.npmjs.org/modern-tar/-/modern-tar-0.7.6.tgz",
"integrity": "sha512-sweCIVXzx1aIGTCdzcMlSZt1h8k5Tmk08VNAuRk3IU28XamGiOH5ypi11g6De2CH7PhYqSSnGy2A/EFhbWnVKg==",
"license": "MIT",
"engines": {
"node": ">=18.0.0"
}
},
"node_modules/nanoid": {
"version": "5.1.15",
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-5.1.15.tgz",
"integrity": "sha512-kBg3RpGtIe+RpTbyXwoI6pk5yD7KUiI3sygUqgeBMRst42KmhB4RZC7eiO9Wa1HIpaCCtpE2DJ6OI4Wi5ebwFw==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/ai"
}
],
"license": "MIT",
"bin": {
"nanoid": "bin/nanoid.js"
},
"engines": {
"node": "^18 || >=20"
}
},
"node_modules/pako": {
"version": "1.0.11",
"resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz",
"integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==",
"license": "(MIT AND Zlib)"
},
"node_modules/pngjs": {
"version": "7.0.0",
"resolved": "https://registry.npmjs.org/pngjs/-/pngjs-7.0.0.tgz",
"integrity": "sha512-LKWqWJRhstyYo9pGvgor/ivk2w94eSjE3RGVuzLGlr3NmD8bf7RcYGze1mNdEHRP6TRP6rMuDHk5t44hnTRyow==",
"license": "MIT",
"engines": {
"node": ">=14.19.0"
}
},
"node_modules/process-nextick-args": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz",
"integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==",
"license": "MIT"
},
"node_modules/puppeteer-core": {
"version": "25.1.0",
"resolved": "https://registry.npmjs.org/puppeteer-core/-/puppeteer-core-25.1.0.tgz",
"integrity": "sha512-jKzy5y4WG6uNuFbTWgW1D7mqoT9o0nllc/6a1DGF775T1mPmgw3scdFEtEq67yVFikavQmbYq6NLfbTfxHSlqQ==",
"license": "Apache-2.0",
"dependencies": {
"@puppeteer/browsers": "3.0.4",
"chromium-bidi": "16.0.1",
"devtools-protocol": "0.0.1624250",
"typed-query-selector": "^2.12.2",
"webdriver-bidi-protocol": "0.4.2",
"ws": "^8.21.0"
},
"engines": {
"node": ">=22.12.0"
}
},
"node_modules/readable-stream": {
"version": "2.3.8",
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz",
"integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==",
"license": "MIT",
"dependencies": {
"core-util-is": "~1.0.0",
"inherits": "~2.0.3",
"isarray": "~1.0.0",
"process-nextick-args": "~2.0.0",
"safe-buffer": "~5.1.1",
"string_decoder": "~1.1.1",
"util-deprecate": "~1.0.1"
}
},
"node_modules/require-directory": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz",
"integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==",
"license": "MIT",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/safe-buffer": {
"version": "5.1.2",
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz",
"integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==",
"license": "MIT"
},
"node_modules/sax": {
"version": "1.6.0",
"resolved": "https://registry.npmjs.org/sax/-/sax-1.6.0.tgz",
"integrity": "sha512-6R3J5M4AcbtLUdZmRv2SygeVaM7IhrLXu9BmnOGmmACak8fiUtOsYNWUS4uK7upbmHIBbLBeFeI//477BKLBzA==",
"license": "BlueOak-1.0.0",
"engines": {
"node": ">=11.0.0"
}
},
"node_modules/setimmediate": {
"version": "1.0.5",
"resolved": "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz",
"integrity": "sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA==",
"license": "MIT"
},
"node_modules/string_decoder": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz",
"integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==",
"license": "MIT",
"dependencies": {
"safe-buffer": "~5.1.0"
}
},
"node_modules/string-width": {
"version": "4.2.3",
"resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
"integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
"license": "MIT",
"dependencies": {
"emoji-regex": "^8.0.0",
"is-fullwidth-code-point": "^3.0.0",
"strip-ansi": "^6.0.1"
},
"engines": {
"node": ">=8"
}
},
"node_modules/strip-ansi": {
"version": "6.0.1",
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
"integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
"license": "MIT",
"dependencies": {
"ansi-regex": "^5.0.1"
},
"engines": {
"node": ">=8"
}
},
"node_modules/typed-query-selector": {
"version": "2.12.2",
"resolved": "https://registry.npmjs.org/typed-query-selector/-/typed-query-selector-2.12.2.tgz",
"integrity": "sha512-EOPFbyIub4ngnEdqi2yOcNeDLaX/0jcE1JoAXQDDMIthap7FoN795lc/SHfIq2d416VufXpM8z/lD+WRm2gfOQ==",
"license": "MIT"
},
"node_modules/undici-types": {
"version": "7.24.6",
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.24.6.tgz",
"integrity": "sha512-WRNW+sJgj5OBN4/0JpHFqtqzhpbnV0GuB+OozA9gCL7a993SmU+1JBZCzLNxYsbMfIeDL+lTsphD5jN5N+n0zg==",
"license": "MIT"
},
"node_modules/util-deprecate": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
"integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==",
"license": "MIT"
},
"node_modules/webdriver-bidi-protocol": {
"version": "0.4.2",
"resolved": "https://registry.npmjs.org/webdriver-bidi-protocol/-/webdriver-bidi-protocol-0.4.2.tgz",
"integrity": "sha512-VSV+fzfChirL3e7jay2yUC7B4HQCGtEWEg/MSSQbK+qWbqeGlRLlXTzPpYr3XGUvbpDHumWZBJxgesg4N7dbtA==",
"license": "Apache-2.0"
},
"node_modules/wrap-ansi": {
"version": "7.0.0",
"resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz",
"integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==",
"license": "MIT",
"dependencies": {
"ansi-styles": "^4.0.0",
"string-width": "^4.1.0",
"strip-ansi": "^6.0.0"
},
"engines": {
"node": ">=10"
},
"funding": {
"url": "https://github.com/chalk/wrap-ansi?sponsor=1"
}
},
"node_modules/ws": {
"version": "8.21.0",
"resolved": "https://registry.npmjs.org/ws/-/ws-8.21.0.tgz",
"integrity": "sha512-Vsp28b7DRcimFQvrqu2Wek3z1iYxDCWqHYB8Qsnk/S4RfaCQzPGPyBNuVjJV3cd6UiKtUtp6sNM77gWvzcCH+g==",
"license": "MIT",
"engines": {
"node": ">=10.0.0"
},
"peerDependencies": {
"bufferutil": "^4.0.1",
"utf-8-validate": ">=5.0.2"
},
"peerDependenciesMeta": {
"bufferutil": {
"optional": true
},
"utf-8-validate": {
"optional": true
}
}
},
"node_modules/xml": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/xml/-/xml-1.0.1.tgz",
"integrity": "sha512-huCv9IH9Tcf95zuYCsQraZtWnJvBtLVE0QHMOs8bWyZAFZNDcYjsPq1nEx8jKA9y+Beo9v+7OBPRisQTjinQMw==",
"license": "MIT"
},
"node_modules/xml-js": {
"version": "1.6.11",
"resolved": "https://registry.npmjs.org/xml-js/-/xml-js-1.6.11.tgz",
"integrity": "sha512-7rVi2KMfwfWFl+GpPg6m80IVMWXLRjO+PxTq7V2CDhoGak0wzYzFgUY2m4XJ47OGdXd8eLE8EmwfAmdjw7lC1g==",
"license": "MIT",
"dependencies": {
"sax": "^1.2.4"
},
"bin": {
"xml-js": "bin/cli.js"
}
},
"node_modules/y18n": {
"version": "5.0.8",
"resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz",
"integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==",
"license": "ISC",
"engines": {
"node": ">=10"
}
},
"node_modules/yargs": {
"version": "17.7.3",
"resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.3.tgz",
"integrity": "sha512-GZtjxm/J/4TSxuL3FNYjCmLktBTnIw/rVmKSIyKeYAZpmJB2ig9VauCC5xsa82GNKVKDAqpOn3KVzNt0zmrU0g==",
"license": "MIT",
"dependencies": {
"cliui": "^8.0.1",
"escalade": "^3.1.1",
"get-caller-file": "^2.0.5",
"require-directory": "^2.1.1",
"string-width": "^4.2.3",
"y18n": "^5.0.5",
"yargs-parser": "^21.1.1"
},
"engines": {
"node": ">=12"
}
},
"node_modules/yargs-parser": {
"version": "21.1.1",
"resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz",
"integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==",
"license": "ISC",
"engines": {
"node": ">=12"
}
},
"node_modules/zod": {
"version": "3.25.76",
"resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz",
"integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==",
"license": "MIT",
"funding": {
"url": "https://github.com/sponsors/colinhacks"
}
}
}
}

View File

@@ -0,0 +1,17 @@
{
"name": "web-rtcp-5axis-site-test",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"keywords": [],
"author": "",
"license": "ISC",
"dependencies": {
"docx": "^9.7.1",
"pngjs": "^7.0.0",
"puppeteer-core": "^25.1.0"
}
}

View 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=truetaskState=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=truerunState=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-trtINI 重新加载",
`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=steppingactiveLine 前进或保持受控",
`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=truerunState=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=falsepowerOn=falsetaskState=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)),
};
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 170 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 151 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 154 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 155 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 146 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 145 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 146 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 37 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 34 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 37 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 37 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 113 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 115 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 135 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 132 KiB