Files
cnc_wams/qa/web-rtcp-5axis-site-test/generate-docx-report.mjs

261 lines
8.4 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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");
}