261 lines
8.4 KiB
JavaScript
261 lines
8.4 KiB
JavaScript
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");
|
||
}
|