import { pathToFileURL } from "node:url"; const modulePath = process.env.PLAYWRIGHT_MODULE; const url = process.env.XYZAC_PANEL_URL ?? "https://49.232.250.35:58010/"; if (!modulePath) { console.log(JSON.stringify({ schema: "xyzac-program-execution-browser-strict.v1", status: "blocked", code: "playwright_unavailable" })); process.exit(0); } const imported = await import(pathToFileURL(modulePath).href); const playwright = imported.default ?? imported; const browser = await playwright.chromium.launch({ headless: true, executablePath: process.env.BROWSER_EXECUTABLE_PATH ?? "/usr/bin/google-chrome", args: ["--ignore-certificate-errors", "--disable-gpu"], }); const context = await browser.newContext({ viewport: { width: 1440, height: 1000 }, ignoreHTTPSErrors: true }); await context.addInitScript(() => { localStorage.clear(); sessionStorage.clear(); }); const page = await context.newPage(); const failures = []; const browserErrors = []; page.on("pageerror", (error) => browserErrors.push(`pageerror:${error.message}`)); page.on("console", (message) => { if (message.type() === "error") browserErrors.push(`console:${message.text()}`); }); page.on("dialog", async (dialog) => { await dialog.accept(); }); const main = page.locator("main.axis-app"); const state = () => main.evaluate((element) => ({ source: element.dataset.axisStateSource, authority: element.dataset.axisAuthority, motion: element.dataset.axisMotionAuthority, machine: `Machine ${element.dataset.axisStateMachine}`, operation: `Operation ${element.dataset.axisStateOperation}`, mode: element.dataset.axisTaskMode, program: element.dataset.axisProgramStatus, runEnabled: element.dataset.axisRunEnabled, loading: element.querySelector(".case-status")?.getAttribute("aria-busy") === "true", homed: element.dataset.axisHomed, line: element.dataset.axisCurrentLine, queue: element.dataset.axisTpQueueDepth, velocity: element.dataset.axisTpVelocity, command: element.querySelector(".command-status")?.textContent?.trim(), error: element.querySelector(".diagnostic-error")?.textContent?.trim() ?? null, })); const wait = async (predicate, label, timeout = 60000) => { const started = Date.now(); let last; while (Date.now() - started < timeout) { last = await state(); if (predicate(last)) return last; await page.waitForTimeout(200); } throw new Error(`${label}:${JSON.stringify(last)}`); }; const click = async (locator, label, timeout = 30000) => { const started = Date.now(); while (Date.now() - started < timeout) { if (await locator.count() && !await locator.first().isDisabled()) { await locator.first().click(); return; } await page.waitForTimeout(200); } throw new Error(`${label}:disabled_or_missing`); }; const record = async (name, action) => { try { const result = await action(); return { name, result: "verified", ...(result ?? {}) }; } catch (error) { const row = { name, result: "failed", reason: error?.message ?? String(error) }; failures.push(row); return row; } }; const samples = []; const runAndObserve = async (name, waitMs = 5000) => record(name, async () => { await click(main.locator('button[aria-label="Begin executing current file [R]"]'), `${name}_run`); const started = await wait((s) => s.program === "running" && s.operation === "Operation RUNNING", `${name}_running`); await page.waitForTimeout(waitMs); const observed = await state(); samples.push({ name, started, observed }); if (observed.error !== null) throw new Error(`${name}_error:${observed.error}`); if (Number(observed.line) <= 0 && Number(observed.queue) <= 0 && Number(observed.velocity) <= 0) throw new Error(`${name}_no_motion:${JSON.stringify(observed)}`); return { observed }; }); const results = []; try { await page.goto(url, { waitUntil: "domcontentloaded", timeout: 30000 }); results.push(await record("startup", async () => { const s = await wait((v) => v.source === "linuxcnc-wasm" && v.motion === "wasm", "wasm_startup"); if (s.authority !== "linuxcnc-wasm-worker") throw new Error("authority_marker_missing"); })); results.push(await record("initial_program_load", async () => { await click(main.getByRole("button", { name: "Load 3D_Chips.ngc", exact: true }), "load_initial"); return { state: await wait((s) => s.program === "loaded" && !s.loading && s.error === null, "initial_program_loaded") }; })); results.push(await record("power_on", async () => { await click(main.locator('button[aria-label="Power on [F2]"]'), "power_on"); await wait((s) => s.machine === "Machine ON", "power_state"); })); results.push(await record("home_all", async () => { await click(main.locator('button[aria-label="Home all axes"]'), "home_all"); await wait((s) => s.homed === "[true,true,true,true,true]", "home_state"); })); results.push(await runAndObserve("first_run", 5000)); results.push(await record("stop_first_run", async () => { await click(main.locator('button[aria-label="Stop program execution [ESC]"]'), "stop_first_run"); return { state: await wait((s) => s.operation === "Operation IDLE" && ["loaded", "aborted"].includes(s.program), "first_stop_idle") }; })); results.push(await runAndObserve("rerun_after_abort_without_reload", 3000)); results.push(await record("stop_direct_rerun", async () => { await click(main.locator('button[aria-label="Stop program execution [ESC]"]'), "stop_direct_rerun"); await wait((s) => s.operation === "Operation IDLE" && ["loaded", "aborted"].includes(s.program), "direct_rerun_stop_idle"); })); results.push(await record("reload_after_stop", async () => { await click(main.getByRole("button", { name: "Load 3D_Chips.ngc", exact: true }), "reload_after_stop"); return { state: await wait((s) => s.program === "loaded" && s.queue === "0" && !s.loading && s.error === null, "reload_after_stop_loaded") }; })); results.push(await runAndObserve("rerun_after_reload", 3000)); results.push(await record("stop_second_run", async () => { await click(main.locator('button[aria-label="Stop program execution [ESC]"]'), "stop_second_run"); await wait((s) => s.operation === "Operation IDLE" && ["loaded", "aborted"].includes(s.program), "second_stop_idle"); })); results.push(await record("load_second_gcode", async () => { const moves = Array.from({ length: 160 }, (_, index) => { const direction = index % 2 === 0 ? 1 : 0; return `G1 X${direction ? 20 : 0} Y${direction ? 10 : 0} Z${direction ? 5 : 10} A${direction ? 30 : 0} C${direction ? 40 : 0} F100`; }); const source = ["G21", "G90", "G0 X0 Y0 Z10 A0 C0", ...moves, "M2", ""].join("\n"); const fileInput = main.locator('input[type="file"][aria-label="Open G-code file"]'); await fileInput.setInputFiles({ name: "strict-second-load.ngc", mimeType: "text/plain", buffer: Buffer.from(source) }); return { state: await wait((s) => s.program === "loaded" && !s.loading && s.command?.includes("PROGRAM_LOAD acknowledged") && s.error === null, "second_gcode_loaded") }; })); results.push(await runAndObserve("run_second_gcode", 3000)); results.push(await record("stop_second_gcode", async () => { await click(main.locator('button[aria-label="Stop program execution [ESC]"]'), "stop_second_gcode"); await wait((s) => s.operation === "Operation IDLE" && ["loaded", "aborted", "completed"].includes(s.program), "second_gcode_stop_idle"); })); } finally { await page.screenshot({ path: "/tmp/xyzac-program-execution-browser-strict.png", fullPage: true }); console.log(JSON.stringify({ schema: "xyzac-program-execution-browser-strict.v1", status: failures.length === 0 && browserErrors.length === 0 ? "verified" : "failed", url, results, samples, failures, browser_errors: browserErrors, final_state: await state(), screenshot: "/tmp/xyzac-program-execution-browser-strict.png", })); await browser.close(); }