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,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");
}