import fs from "node:fs/promises"; import http from "node:http"; import path from "node:path"; import { randomUUID } from "node:crypto"; import puppeteer from "puppeteer-core"; import { PNG } from "pngjs"; const REPO_ROOT = path.resolve("/home/meswork/cnc_wams"); const QA_ROOT = path.join(REPO_ROOT, "qa/web-rtcp-5axis-site-test"); const OUTPUT_DIR = path.join(QA_ROOT, "output"); const CHROME_PATH = process.env.CHROME_PATH || process.env.CHROMIUM || "/usr/bin/google-chrome"; const TARGET_URL = process.env.TARGET_URL || ""; const APP_URL = process.env.APP_URL || "/web-rtcp-5axis-sim-plan/app/index.html"; const EVIDENCE_SCOPE = process.env.EVIDENCE_SCOPE || (TARGET_URL ? "cloud-button-control-evidence" : "button-control-evidence"); const SCREENSHOT_DIR = path.join(QA_ROOT, "screenshots", EVIDENCE_SCOPE); const REPORT_BASENAME = `${EVIDENCE_SCOPE}-report`; const JOB_ID = process.env.JOB_ID || `btn-${new Date().toISOString().replace(/[-:.TZ]/g, "").slice(0, 14)}-${randomUUID().slice(0, 8)}`; const REPORT_ID = process.env.REPORT_ID || `report-${JOB_ID}`; await fs.mkdir(OUTPUT_DIR, { recursive: true }); await fs.mkdir(SCREENSHOT_DIR, { recursive: true }); let server = null; let targetUrl = TARGET_URL; if (!targetUrl) { server = createStaticServer(REPO_ROOT); await new Promise((resolve) => server.listen(0, "127.0.0.1", resolve)); const address = server.address(); if (!address || typeof address === "string") throw new Error("failed to start static server"); targetUrl = `http://127.0.0.1:${address.port}${APP_URL}`; } const browser = await puppeteer.launch({ headless: true, executablePath: CHROME_PATH, defaultViewport: { width: 1500, height: 1050, deviceScaleFactor: 1 }, ignoreHTTPSErrors: true, args: [ "--ignore-certificate-errors", "--disable-gpu", "--enable-webgl", "--use-angle=swiftshader", "--enable-unsafe-swiftshader", "--no-sandbox", ], }); const page = await browser.newPage(); const consoleErrors = []; const pageErrors = []; page.on("console", (msg) => { if (msg.type() === "error") consoleErrors.push(msg.text()); }); page.on("pageerror", (error) => pageErrors.push(error.message)); const report = { jobId: JOB_ID, reportId: REPORT_ID, evidenceScope: EVIDENCE_SCOPE, generatedAt: new Date().toISOString(), targetUrl, chromePath: CHROME_PATH, screenshotsDir: SCREENSHOT_DIR, steps: [], checks: [], consoleErrors, pageErrors, }; try { await page.goto(report.targetUrl, { waitUntil: "networkidle2", timeout: 60000 }); await page.waitForSelector('[data-shell="gmoccapy-5axis"]', { timeout: 15000 }); await page.waitForFunction(() => Boolean(window.webRtcp5AxisSimulation?.getState), { timeout: 15000 }); await page.waitForFunction(() => { const canvas = document.querySelector("[data-five-axis-canvas]"); return canvas?.dataset?.threeReady === "true"; }, { timeout: 20000 }); await windowReady(); await captureStep("01-initial", "Initial UI", "Browser app loaded before machine preparation."); await loadOperatorProgram(); await captureStep("02-program-loaded", "Program loaded", "Short operator G-code loaded through LinuxCNC interpreter WASM."); await click("power"); await waitForState((state) => state.machine.powerOn === true, 10000, "power on"); await click("mode-manual"); await waitForState((state) => state.machine.mode === "manual", 10000, "manual mode"); await captureStep("03-before-home", "Before HOME", "Machine powered on in manual mode before HOME."); await click("HOME"); await waitForState((state) => state.machine.allHomed === true && state.machine.mode === "manual", 10000, "HOME complete"); await captureStep("04-after-home", "After HOME", "HOME command preserves allHomed state in Web/task gate."); await click("mode-auto"); await waitForState((state) => state.machine.mode === "auto", 10000, "auto mode"); await captureStep("05-ready-for-run", "Ready for RUN", "POWER, HOME, AUTO, and loaded G-code are ready before RUN."); await click("RUN"); await waitForState((state) => ( state.runState === "running" && state.programRuntimeFeedback?.sourceMode === "linuxcnc-task-motion-hal-wasm" ), 15000, "RUN active"); await wait(250); await captureStep("06-after-run", "After RUN", "RUN starts task/HAL backed program execution."); await captureStep("07-before-pause", "Before PAUSE", "Program is running before PAUSE."); await click("PAUSE"); await waitForState((state) => state.runState === "paused" && state.machine.taskPaused === true, 10000, "PAUSE active"); await captureStep("08-after-pause", "After PAUSE", "PAUSE sets runState paused and taskPaused true."); await captureStep("09-before-resume", "Before RESUME", "Program is paused before RESUME."); await click("RESUME"); await waitForState((state) => state.runState === "running" && state.machine.interpState === "reading", 10000, "RESUME active"); await wait(150); await captureStep("10-after-resume", "After RESUME", "RESUME returns task/HAL execution to running/reading."); await click("PAUSE"); await waitForState((state) => state.runState === "paused" && state.machine.taskPaused === true, 10000, "PAUSE before STEP"); await captureStep("11-before-step", "Before STEP", "Program is paused before STEP."); await click("STEP"); await waitForState((state) => ( state.machine.interpState === "paused" && state.machine.taskPaused === true && state.taskHalStatus?.task?.singleStepping === true ), 10000, "STEP active"); await captureStep("12-after-step", "After STEP", "STEP sends EMC_TASK_PLAN_STEP and leaves task paused with singleStepping true."); await captureStep("13-before-stop", "Before STOP", "Paused single-step state before STOP."); await click("STOP"); await waitForState((state) => ( state.runState === "stopped" && state.machine.interpState === "idle" && state.taskHalStatus?.motionStatus?.motion?.aborted === true ), 10000, "STOP active"); await captureStep("14-after-stop", "After STOP", "STOP aborts task/HAL motion and returns interpreter state to idle."); addChecks(); report.status = report.checks.every((check) => check.pass) && pageErrors.length === 0 ? "PASS" : "FAIL"; const jsonPath = path.join(OUTPUT_DIR, `${REPORT_BASENAME}.json`); report.jsonPath = jsonPath; const pdfPath = path.join(OUTPUT_DIR, `${REPORT_BASENAME}.pdf`); report.pdfPath = pdfPath; await fs.writeFile(jsonPath, `${JSON.stringify(report, null, 2)}\n`, "utf8"); await writePdfReport(pdfPath, report); console.log(`button_control_evidence_status=${report.status}`); console.log(`button_control_evidence_job_id=${report.jobId}`); console.log(`button_control_evidence_report_id=${report.reportId}`); console.log(`button_control_evidence_json=${jsonPath}`); console.log(`button_control_evidence_pdf=${pdfPath}`); console.log(`button_control_evidence_screenshots=${SCREENSHOT_DIR}`); if (report.status !== "PASS") process.exitCode = 1; } finally { await page.close().catch(() => {}); await browser.close().catch(() => {}); if (server) await new Promise((resolve) => server.close(resolve)); } async function windowReady() { await waitForState((state) => ( state.kinematicsRuntimeReadiness?.loaded === true && state.interpreterRuntimeReadiness?.loaded === true && state.taskHalRuntimeReadiness?.loaded === true ), 30000, "runtime readiness"); await waitForState((state) => ( !state.interpreterExecutionPending && state.machineFileStaging?.status === "staged" ), 30000, "machine file seed ready") .catch(() => null); } async function loadOperatorProgram() { await page.evaluate(() => { window.webRtcp5AxisSimulation.dispatch({ type: "LOAD_PROGRAM", filename: "button-control-evidence.ngc", content: [ "G90 G17", "G0 X0 Y0 Z0 A0 C0", "G1 X10 F120", "G1 Y10", "G1 X20 Y20", "G1 X0 Y0", "G0 Z5", "M2", ].join("\n"), }); }); await waitForState((state) => ( state.activeProgram === "button-control-evidence.ngc" && state.programExecutionSourceMode === "linuxcnc-interpreter-wasm" && state.programExecution?.summary?.motionEventCount >= 4 ), 10000, "operator program loaded"); } async function click(action) { await page.evaluate((selector) => { const button = document.querySelector(selector); if (!button) throw new Error(`missing button ${selector}`); button.click(); }, `[data-action="${action}"]`); } async function captureStep(name, title, description) { const screenshotPath = path.join(SCREENSHOT_DIR, `${name}.png`); await page.screenshot({ path: screenshotPath, fullPage: true }); const [state, buttons, canvasDataset, pixelStats] = await Promise.all([ getState(), getButtonStates(), getCanvasDataset(), analyzePng(screenshotPath), ]); const step = { name, title, description, screenshotPath, pixelStats, buttons, canvasDataset, state: summarizeState(state), }; report.steps.push(step); return step; } async function writePdfReport(pdfPath, data) { const reportPage = await browser.newPage(); const rows = data.checks.map((item) => `
${escapeHtml(step.description)}
Screenshot: ${escapeHtml(step.screenshotPath)}
State: ${escapeHtml(JSON.stringify({ runState: step.state.runState, machine: step.state.machine, task: step.state.taskHalStatus?.task, motion: step.state.taskHalStatus?.motionStatus?.motion, }))}
| Check | Status | Evidence |
|---|