Align AXIS Web control lifecycle with LinuxCNC
This commit is contained in:
122
scripts/test_xyzac_live_path_lifecycle_browser.mjs
Normal file
122
scripts/test_xyzac_live_path_lifecycle_browser.mjs
Normal file
@@ -0,0 +1,122 @@
|
||||
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-live-path-lifecycle-browser.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 browserErrors = [];
|
||||
page.on("pageerror", (error) => browserErrors.push(`pageerror:${error.message}`));
|
||||
page.on("console", (message) => { if (message.type() === "error") browserErrors.push(`console:${message.text()}`); });
|
||||
const main = page.locator("main.axis-app");
|
||||
const state = () => main.evaluate((element) => {
|
||||
const viewer = element.querySelector('[data-xyzac-preview="toolpath"]');
|
||||
return {
|
||||
loading: element.dataset.axisProgramLoading,
|
||||
path: element.dataset.axisProgramPath,
|
||||
program: element.dataset.axisProgramStatus,
|
||||
operation: element.querySelector(".operation-state")?.textContent?.trim(),
|
||||
machine: element.querySelector(".machine-state")?.textContent?.trim(),
|
||||
homed: element.dataset.axisHomed,
|
||||
line: element.dataset.axisCurrentLine,
|
||||
queue: element.dataset.axisTpQueueDepth,
|
||||
velocity: element.dataset.axisTpVelocity,
|
||||
epoch: Number(viewer?.getAttribute("data-xyzac-live-path-epoch") ?? -1),
|
||||
liveCount: Number(viewer?.getAttribute("data-xyzac-live-trace-count") ?? 0),
|
||||
previewCount: Number(viewer?.getAttribute("data-xyzac-toolpath-count") ?? 0),
|
||||
error: element.querySelector(".diagnostic-error")?.textContent?.trim() ?? null,
|
||||
};
|
||||
});
|
||||
const wait = async (predicate, label, timeout = 90000) => {
|
||||
const started = Date.now(); let last;
|
||||
while (Date.now() - started < timeout) { last = await state(); if (predicate(last)) return last; await page.waitForTimeout(50); }
|
||||
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(100); }
|
||||
throw new Error(`${label}:disabled_or_missing`);
|
||||
};
|
||||
const load = async (name, source) => {
|
||||
const input = main.locator('input[type="file"][aria-label="Open G-code file"]');
|
||||
await input.setInputFiles({ name, mimeType: "text/plain", buffer: Buffer.from(source) });
|
||||
return wait((value) => value.loading === "false" && value.path === `browser/${name}` && value.program === "loaded" && value.error === null, `${name}_loaded`);
|
||||
};
|
||||
const canvasPixels = async (targetPage) => targetPage.locator('[data-xyzac-preview="toolpath"] canvas').evaluate((canvas) => {
|
||||
const gl = canvas.getContext("webgl2", { preserveDrawingBuffer: true }) ?? canvas.getContext("webgl", { preserveDrawingBuffer: true });
|
||||
if (!gl) return { width: canvas.width, height: canvas.height, changed: 0 };
|
||||
const pixels = new Uint8Array(canvas.width * canvas.height * 4);
|
||||
gl.readPixels(0, 0, canvas.width, canvas.height, gl.RGBA, gl.UNSIGNED_BYTE, pixels);
|
||||
let changed = 0;
|
||||
for (let index = 0; index < pixels.length; index += 4) if (Math.abs(pixels[index] - 5) + Math.abs(pixels[index + 1] - 11) + Math.abs(pixels[index + 2] - 15) > 20) changed += 1;
|
||||
return { width: canvas.width, height: canvas.height, changed };
|
||||
});
|
||||
|
||||
const longMoves = Array.from({ length: 160 }, (_, index) => `G1 X${index % 2 ? 0 : 6} Y${index % 2 ? 0 : 4} Z${index % 2 ? 10 : 8} A${index % 2 ? 0 : 10} C${index % 2 ? 0 : 15} F300`);
|
||||
const longSource = ["G21", "G90", ...longMoves, "M2", ""].join("\n");
|
||||
const shortMoves = Array.from({ length: 12 }, (_, index) => `G1 X${index % 2 ? -0.2 : 0.2} Y${index % 2 ? -0.1 : 0.1} F600`);
|
||||
const shortSource = ["G21", "G91", ...shortMoves, "M2", ""].join("\n");
|
||||
const replacementSource = "G21\nG90\nG1 X1 Y1 Z1 F300\nM2\n";
|
||||
|
||||
await page.goto(`${url}?live-path-lifecycle=20260907`, { waitUntil: "domcontentloaded", timeout: 30000 });
|
||||
await wait((value) => value.loading === "false" && value.program === "loaded", "startup");
|
||||
await click(main.locator('button[aria-label="Power on [F2]"]'), "power_on");
|
||||
await wait((value) => value.machine === "Machine ON", "power_state");
|
||||
await click(main.locator('button[aria-label="Home all axes"]'), "home_all");
|
||||
await wait((value) => value.homed === "[true,true,true,true,true]", "home_state");
|
||||
|
||||
const loadedLong = await load("live-long.ngc", longSource);
|
||||
await click(main.locator('button[aria-label="Begin executing current file [R]"]'), "run_long");
|
||||
const running = await wait((value) => value.program === "running" && value.liveCount >= 3, "long_live_trace");
|
||||
await click(main.locator('button[aria-label="Pause execution [P]"]'), "pause");
|
||||
const paused = await wait((value) => value.program === "paused", "paused");
|
||||
await page.waitForTimeout(1000);
|
||||
const pausedStable = await state();
|
||||
if (paused.epoch !== running.epoch || pausedStable.epoch !== running.epoch || pausedStable.liveCount < running.liveCount || pausedStable.previewCount !== loadedLong.previewCount) throw new Error(`pause_cleared_path:${JSON.stringify({ loadedLong, running, paused, pausedStable })}`);
|
||||
await click(main.locator('button[aria-label="Resume execution [S]"]'), "resume");
|
||||
const resumed = await wait((value) => value.program === "running" && value.liveCount >= pausedStable.liveCount, "resumed");
|
||||
if (resumed.epoch !== running.epoch || resumed.previewCount !== loadedLong.previewCount) throw new Error(`resume_changed_epoch:${JSON.stringify(resumed)}`);
|
||||
await click(main.locator('button[aria-label="Stop program execution [ESC]"]'), "stop");
|
||||
const stopped = await wait((value) => value.operation === "Operation IDLE" && value.program === "aborted", "stopped");
|
||||
if (stopped.epoch !== running.epoch || stopped.liveCount < pausedStable.liveCount || stopped.previewCount !== loadedLong.previewCount) throw new Error(`stop_cleared_path:${JSON.stringify(stopped)}`);
|
||||
|
||||
const loadedShort = await load("live-short.ngc", shortSource);
|
||||
if (loadedShort.epoch <= stopped.epoch || loadedShort.liveCount !== 0) throw new Error(`load_did_not_clear_live_path:${JSON.stringify({ stopped, loadedShort })}`);
|
||||
await click(main.locator('button[aria-label="Begin executing current file [R]"]'), "run_short");
|
||||
const completed = await wait((value) => value.program === "completed" && value.operation === "Operation IDLE" && value.liveCount >= 2, "short_completed");
|
||||
await page.waitForTimeout(1000);
|
||||
const completedStable = await state();
|
||||
if (completedStable.epoch !== completed.epoch || completedStable.liveCount < completed.liveCount || completedStable.previewCount !== loadedShort.previewCount) throw new Error(`completion_cleared_path:${JSON.stringify({ completed, completedStable })}`);
|
||||
|
||||
await click(main.locator('button[aria-label="Begin executing current file [R]"]'), "rerun_short");
|
||||
const rerun = await wait((value) => value.epoch > completedStable.epoch && value.liveCount < completedStable.liveCount, "rerun_new_epoch");
|
||||
await wait((value) => value.program === "completed" && value.operation === "Operation IDLE", "rerun_completed");
|
||||
|
||||
const replacement = await load("live-replacement.ngc", replacementSource);
|
||||
if (replacement.epoch <= rerun.epoch || replacement.liveCount !== 0 || replacement.previewCount !== 1) throw new Error(`replacement_lifecycle_invalid:${JSON.stringify({ rerun, replacement })}`);
|
||||
await click(main.locator('button[aria-label="Begin executing current file [R]"]'), "run_replacement");
|
||||
await wait((value) => value.liveCount >= 1 && Number(value.line) > 0, "replacement_trace");
|
||||
await wait((value) => value.operation === "Operation IDLE", "replacement_idle");
|
||||
const beforeClear = await state();
|
||||
await click(main.locator('button[aria-label="Clear live plot [Ctrl-K]"]'), "clear_live_plot");
|
||||
const cleared = await wait((value) => value.epoch > beforeClear.epoch && value.liveCount <= 1, "cleared_live_plot");
|
||||
if (cleared.previewCount !== beforeClear.previewCount) throw new Error(`clear_removed_static_preview:${JSON.stringify({ beforeClear, cleared })}`);
|
||||
|
||||
const desktopPixels = await canvasPixels(page);
|
||||
if (desktopPixels.changed < 100) throw new Error(`desktop_canvas_blank:${JSON.stringify(desktopPixels)}`);
|
||||
await page.screenshot({ path: "/tmp/xyzac-live-path-lifecycle-desktop.png", fullPage: true });
|
||||
await page.setViewportSize({ width: 390, height: 844 });
|
||||
await page.waitForTimeout(500);
|
||||
const mobilePixels = await canvasPixels(page);
|
||||
if (mobilePixels.changed < 100) throw new Error(`mobile_canvas_blank:${JSON.stringify(mobilePixels)}`);
|
||||
await page.screenshot({ path: "/tmp/xyzac-live-path-lifecycle-mobile.png", fullPage: true });
|
||||
|
||||
console.log(JSON.stringify({ schema: "xyzac-live-path-lifecycle-browser.v1", status: browserErrors.length ? "failed" : "verified", lifecycle: { loadedLong, running, paused: pausedStable, resumed, stopped, loadedShort, completed: completedStable, rerun, replacement, beforeClear, cleared }, canvas_pixels: { desktop: desktopPixels, mobile: mobilePixels }, browser_errors: browserErrors, screenshots: ["/tmp/xyzac-live-path-lifecycle-desktop.png", "/tmp/xyzac-live-path-lifecycle-mobile.png"] }));
|
||||
await browser.close();
|
||||
Reference in New Issue
Block a user