Add RTCP simulation QA updates
This commit is contained in:
260
qa/web-rtcp-5axis-site-test/generate-docx-report.mjs
Normal file
260
qa/web-rtcp-5axis-site-test/generate-docx-report.mjs
Normal 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");
|
||||
}
|
||||
Reference in New Issue
Block a user