144 lines
7.8 KiB
JavaScript
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();
|
|
}
|