100 lines
7.3 KiB
JavaScript
100 lines
7.3 KiB
JavaScript
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: element.querySelector(".machine-state")?.textContent?.trim(),
|
|
operation: element.querySelector(".operation-state")?.textContent?.trim(),
|
|
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();
|