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