import { readFile } from "node:fs/promises"; import { pathToFileURL } from "node:url"; import { fnv1a64 } from "../web/worker/replay.mjs"; 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-refresh-single-program-browser.v1", status: "blocked", code: "playwright_unavailable" })); process.exit(0); } const defaultPath = "linuxcnc/configs/sim/axis/vismach/5axis/table-rotary-tilting/demos/xyzac_switchkins.ngc"; const legacyPath = "linuxcnc/nc_files/3D_Chips.ngc"; const defaultSource = await readFile(new URL(`../${defaultPath}`, import.meta.url), "utf8"); const legacySource = await readFile(new URL(`../${legacyPath}`, import.meta.url), "utf8"); const defaultHash = fnv1a64(defaultSource.replace(/\r\n/g, "\n").replace(/\r/g, "\n")); const legacyHash = fnv1a64(legacySource.replace(/\r\n/g, "\n").replace(/\r/g, "\n")); 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 }); const page = await context.newPage(); 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) => ({ loading: element.dataset.axisProgramLoading, path: element.dataset.axisProgramPath, sourceHash: element.dataset.axisProgramSourceHash, program: element.dataset.axisProgramStatus, runEnabled: element.dataset.axisRunEnabled, machine: `Machine ${element.dataset.axisStateMachine}`, operation: `Operation ${element.dataset.axisStateOperation}`, homed: element.dataset.axisHomed, line: element.dataset.axisCurrentLine, queue: element.dataset.axisTpQueueDepth, velocity: element.dataset.axisTpVelocity, case: element.querySelector(".case-status")?.textContent?.trim(), previewCount: element.querySelector('[data-xyzac-preview="toolpath"]')?.getAttribute("data-xyzac-toolpath-count") ?? "0", powerDisabled: element.querySelector('button[aria-label="Power on [F2]"]')?.disabled, runDisabled: element.querySelector('button[aria-label="Begin executing current file [R]"]')?.disabled, 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`); }; await page.goto(`${url}?refresh-single-program-seed=20260907`, { waitUntil: "domcontentloaded", timeout: 30000 }); await wait((s) => s.loading === "false" && s.path === defaultPath && s.sourceHash === defaultHash, "initial_default_program"); const legacyState = { version: 1, selectedAxis: "x", leftTab: "manual", rightTab: "preview", view: "p", zoom: 1, rotateMode: false, coordType: "machine", displayType: "actual", metric: true, layers: { grid: true, tool: true, path: true, live: true, limits: false, velocity: false, offsets: false, extents: false }, casePath: "linuxcnc/configs/sim/axis/vismach/5axis/table-rotary-tilting/xyzac-trt.ini", touchOffSystem: "P1 G54", program: { path: legacyPath, source: legacySource } }; const savedAt = Date.now(); const legacyRecordUnsigned = { schema: "linuxcnc-ui-temp-json.v1", context_id: "linuxcnc-2.10.0-pre0-wasm-m1-py-c1", key: "xyzac-trt", generation: 1000, saved_at_ms: savedAt, state: legacyState }; const legacyRecord = { ...legacyRecordUnsigned, state_hash: fnv1a64(legacyState), record_hash: fnv1a64(legacyRecordUnsigned) }; await page.evaluate(async (record) => { const root = await navigator.storage.getDirectory(); const app = await root.getDirectoryHandle("linuxcnc-web-wasm", { create: true }); const states = await app.getDirectoryHandle("ui-state", { create: true }); const project = await states.getDirectoryHandle("xyzac-trt", { create: true }); for (const name of ["state-a.json", "state-b.json", "manifest.json"]) { try { await project.removeEntry(name); } catch { /* empty cache */ } } const handle = await project.getFileHandle("state-a.json", { create: true }); const writable = await handle.createWritable(); await writable.write(JSON.stringify(record)); await writable.close(); }, legacyRecord); await page.reload({ waitUntil: "domcontentloaded", timeout: 30000 }); const transitions = []; for (let index = 0; index < 140; index += 1) { const sample = await state(); transitions.push(sample); if (sample.path === legacyPath || sample.sourceHash === legacyHash || sample.case?.includes("3D_Chips.ngc")) throw new Error(`legacy_program_auto_loaded:${JSON.stringify(sample)}`); if (sample.loading === "true" && (sample.powerDisabled !== true || sample.runDisabled !== true || sample.previewCount !== "0")) throw new Error(`startup_loading_gate_invalid:${JSON.stringify(sample)}`); await page.waitForTimeout(250); } const stable = await state(); if (stable.loading !== "false" || stable.path !== defaultPath || stable.sourceHash !== defaultHash || stable.previewCount !== "29" || stable.error !== null) throw new Error(`configured_program_not_stable:${JSON.stringify(stable)}`); await click(main.locator('button[aria-label="Power on [F2]"]'), "power_on"); await wait((s) => s.machine === "Machine ON", "power_on_state"); await click(main.locator('button[aria-label="Home all axes"]'), "home_all"); await wait((s) => s.homed === "[true,true,true,true,true]", "home_state"); await click(main.locator('button[aria-label="Begin executing current file [R]"]'), "run"); const running = await wait((s) => s.program === "running" && s.operation === "Operation RUNNING", "program_running"); await page.waitForTimeout(3000); const moving = await state(); if (Number(moving.line) <= 0 && Number(moving.queue) <= 0 && Number(moving.velocity) <= 0) throw new Error(`program_not_moving:${JSON.stringify(moving)}`); await page.screenshot({ path: "/tmp/xyzac-refresh-single-program-browser.png", fullPage: true }); console.log(JSON.stringify({ schema: "xyzac-refresh-single-program-browser.v1", status: browserErrors.length ? "failed" : "verified", configured_program: { path: defaultPath, source_hash: defaultHash, preview_count: Number(stable.previewCount) }, ignored_legacy_program: { path: legacyPath, source_hash: legacyHash }, sampled_ms: transitions.length * 250, running, moving, browser_errors: browserErrors, screenshot: "/tmp/xyzac-refresh-single-program-browser.png" })); await browser.close();