Files
linuxcncWebWASM/scripts/test_xyzac_live_path_lifecycle_browser.mjs

137 lines
11 KiB
JavaScript

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: `Operation ${element.dataset.axisStateOperation}`,
machine: `Machine ${element.dataset.axisStateMachine}`,
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 < stopped.liveCount) throw new Error(`open_cleared_native_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('[data-axis-control="toolbar-reload"]'), "reload_same_file");
await wait((value) => value.program === "loaded" && value.loading === "false", "same_file_reloaded");
const reloaded = await state();
if (reloaded.epoch !== completedStable.epoch || reloaded.liveCount < completedStable.liveCount) throw new Error(`reload_cleared_native_live_path:${JSON.stringify(reloaded)}`);
await click(main.locator('button[aria-label="Begin executing current file [R]"]'), "rerun_short");
const rerun = await wait((value) => value.program === "running" && value.epoch === completedStable.epoch && value.liveCount >= completedStable.liveCount, "rerun_retains_native_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 < rerun.liveCount || 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 })}`);
await load("live-key-clear.ngc", shortSource);
await click(main.locator('[data-axis-control="toolbar-run"]'), "run_for_keyboard_clear");
await wait((value) => value.operation === "Operation IDLE" && value.program === "completed" && value.liveCount > 2, "regenerate_live_trace");
const beforeKeyClear = await state();
await page.evaluate(() => document.activeElement?.blur());
await page.keyboard.press("Control+k");
const keyCleared = await wait((value) => value.epoch === beforeKeyClear.epoch + 1 && value.liveCount === 0, "ctrl_k_clears_live_trace");
if (keyCleared.previewCount !== beforeKeyClear.previewCount || keyCleared.queue !== beforeKeyClear.queue || keyCleared.program !== beforeKeyClear.program) throw new Error("ctrl_k_changed_program_or_queue");
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, reloaded, rerun, replacement, beforeClear, cleared, beforeKeyClear, keyCleared }, canvas_pixels: { desktop: desktopPixels, mobile: mobilePixels }, browser_errors: browserErrors, screenshots: ["/tmp/xyzac-live-path-lifecycle-desktop.png", "/tmp/xyzac-live-path-lifecycle-mobile.png"] }));
if (browserErrors.length) process.exitCode = 1;
await browser.close();