123 lines
9.4 KiB
JavaScript
123 lines
9.4 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: 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();
|