Files
linuxcncWebWASM/scripts/test_xyzac_program_execution_browser_strict.mjs

144 lines
7.8 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-program-execution-browser-strict.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 failures = [];
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) => ({
source: element.dataset.axisStateSource,
authority: element.dataset.axisAuthority,
motion: element.dataset.axisMotionAuthority,
machine: element.querySelector(".machine-state")?.textContent?.trim(),
operation: element.querySelector(".operation-state")?.textContent?.trim(),
mode: element.dataset.axisTaskMode,
program: element.dataset.axisProgramStatus,
runEnabled: element.dataset.axisRunEnabled,
loading: element.querySelector(".case-status")?.getAttribute("aria-busy") === "true",
homed: element.dataset.axisHomed,
line: element.dataset.axisCurrentLine,
queue: element.dataset.axisTpQueueDepth,
velocity: element.dataset.axisTpVelocity,
command: element.querySelector(".command-status")?.textContent?.trim(),
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`);
};
const record = async (name, action) => {
try { const result = await action(); return { name, result: "verified", ...(result ?? {}) }; }
catch (error) { const row = { name, result: "failed", reason: error?.message ?? String(error) }; failures.push(row); return row; }
};
const samples = [];
const runAndObserve = async (name, waitMs = 5000) => record(name, async () => {
await click(main.locator('button[aria-label="Begin executing current file [R]"]'), `${name}_run`);
const started = await wait((s) => s.program === "running" && s.operation === "Operation RUNNING", `${name}_running`);
await page.waitForTimeout(waitMs);
const observed = await state();
samples.push({ name, started, observed });
if (observed.error !== null) throw new Error(`${name}_error:${observed.error}`);
if (Number(observed.line) <= 0 && Number(observed.queue) <= 0 && Number(observed.velocity) <= 0) throw new Error(`${name}_no_motion:${JSON.stringify(observed)}`);
return { observed };
});
const results = [];
try {
await page.goto(url, { waitUntil: "domcontentloaded", timeout: 30000 });
results.push(await record("startup", async () => {
const s = await wait((v) => v.source === "linuxcnc-wasm" && v.motion === "wasm", "wasm_startup");
if (s.authority !== "linuxcnc-wasm-worker") throw new Error("authority_marker_missing");
}));
results.push(await record("initial_program_load", async () => {
await click(main.getByRole("button", { name: "Load 3D_Chips.ngc", exact: true }), "load_initial");
return { state: await wait((s) => s.program === "loaded" && !s.loading && s.error === null, "initial_program_loaded") };
}));
results.push(await record("power_on", async () => {
await click(main.locator('button[aria-label="Power on [F2]"]'), "power_on");
await wait((s) => s.machine === "Machine ON", "power_state");
}));
results.push(await record("home_all", async () => {
await click(main.locator('button[aria-label="Home all axes"]'), "home_all");
await wait((s) => s.homed === "[true,true,true,true,true]", "home_state");
}));
results.push(await runAndObserve("first_run", 5000));
results.push(await record("stop_first_run", async () => {
await click(main.locator('button[aria-label="Stop program execution [ESC]"]'), "stop_first_run");
return { state: await wait((s) => s.operation === "Operation IDLE" && ["loaded", "aborted"].includes(s.program), "first_stop_idle") };
}));
results.push(await runAndObserve("rerun_after_abort_without_reload", 3000));
results.push(await record("stop_direct_rerun", async () => {
await click(main.locator('button[aria-label="Stop program execution [ESC]"]'), "stop_direct_rerun");
await wait((s) => s.operation === "Operation IDLE" && ["loaded", "aborted"].includes(s.program), "direct_rerun_stop_idle");
}));
results.push(await record("reload_after_stop", async () => {
await click(main.getByRole("button", { name: "Load 3D_Chips.ngc", exact: true }), "reload_after_stop");
return { state: await wait((s) => s.program === "loaded" && s.queue === "0" && !s.loading && s.error === null, "reload_after_stop_loaded") };
}));
results.push(await runAndObserve("rerun_after_reload", 3000));
results.push(await record("stop_second_run", async () => {
await click(main.locator('button[aria-label="Stop program execution [ESC]"]'), "stop_second_run");
await wait((s) => s.operation === "Operation IDLE" && ["loaded", "aborted"].includes(s.program), "second_stop_idle");
}));
results.push(await record("load_second_gcode", async () => {
const moves = Array.from({ length: 160 }, (_, index) => {
const direction = index % 2 === 0 ? 1 : 0;
return `G1 X${direction ? 20 : 0} Y${direction ? 10 : 0} Z${direction ? 5 : 10} A${direction ? 30 : 0} C${direction ? 40 : 0} F100`;
});
const source = ["G21", "G90", "G0 X0 Y0 Z10 A0 C0", ...moves, "M2", ""].join("\n");
const fileInput = main.locator('input[type="file"][aria-label="Open G-code file"]');
await fileInput.setInputFiles({ name: "strict-second-load.ngc", mimeType: "text/plain", buffer: Buffer.from(source) });
return { state: await wait((s) => s.program === "loaded" && !s.loading && s.command?.includes("PROGRAM_LOAD acknowledged") && s.error === null, "second_gcode_loaded") };
}));
results.push(await runAndObserve("run_second_gcode", 3000));
results.push(await record("stop_second_gcode", async () => {
await click(main.locator('button[aria-label="Stop program execution [ESC]"]'), "stop_second_gcode");
await wait((s) => s.operation === "Operation IDLE" && ["loaded", "aborted", "completed"].includes(s.program), "second_gcode_stop_idle");
}));
} finally {
await page.screenshot({ path: "/tmp/xyzac-program-execution-browser-strict.png", fullPage: true });
console.log(JSON.stringify({
schema: "xyzac-program-execution-browser-strict.v1",
status: failures.length === 0 && browserErrors.length === 0 ? "verified" : "failed",
url,
results,
samples,
failures,
browser_errors: browserErrors,
final_state: await state(),
screenshot: "/tmp/xyzac-program-execution-browser-strict.png",
}));
await browser.close();
}