Align AXIS Web control lifecycle with LinuxCNC
This commit is contained in:
112
scripts/test_xyzac_multi_program_load_browser.mjs
Normal file
112
scripts/test_xyzac_multi_program_load_browser.mjs
Normal file
@@ -0,0 +1,112 @@
|
||||
import { pathToFileURL } from "node:url";
|
||||
import { fnv1a64 } from "../web/worker/replay.mjs";
|
||||
|
||||
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-multi-program-load-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 failures = [];
|
||||
const browserErrors = [];
|
||||
const dialogs = [];
|
||||
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) => { dialogs.push({ type: dialog.type(), message: dialog.message() }); await dialog.dismiss(); });
|
||||
const main = page.locator("main.axis-app");
|
||||
const state = () => main.evaluate((element) => ({
|
||||
source: element.dataset.axisStateSource,
|
||||
machine: element.querySelector(".machine-state")?.textContent?.trim(),
|
||||
operation: element.querySelector(".operation-state")?.textContent?.trim(),
|
||||
program: element.dataset.axisProgramStatus,
|
||||
line: element.dataset.axisCurrentLine,
|
||||
queue: element.dataset.axisTpQueueDepth,
|
||||
velocity: element.dataset.axisTpVelocity,
|
||||
homed: element.dataset.axisHomed,
|
||||
loading: element.querySelector(".case-status")?.getAttribute("aria-busy") === "true",
|
||||
command: element.querySelector(".command-status")?.textContent?.trim(),
|
||||
error: element.querySelector(".diagnostic-error")?.textContent?.trim() ?? null,
|
||||
editor: element.querySelector("#axis-program")?.value ?? "",
|
||||
sourceHash: element.querySelector(".xyzac-program header code")?.textContent?.trim() ?? "",
|
||||
previewCount: element.querySelector('[data-xyzac-preview="toolpath"]')?.getAttribute("data-xyzac-toolpath-count") ?? "0",
|
||||
}));
|
||||
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 { return { name, result: "verified", ...(await action() ?? {}) }; } catch (error) { const row = { name, result: "failed", reason: error?.message ?? String(error) }; failures.push(row); return row; } };
|
||||
|
||||
const absoluteMoves = Array.from({ length: 12 }, (_, index) => {
|
||||
const positive = index % 2 === 0;
|
||||
return `G1 X${positive ? 20 : 0} Y${positive ? 10 : 0} Z${positive ? 5 : 10} A${positive ? 30 : 0} C${positive ? 40 : 0} F600`;
|
||||
});
|
||||
const incrementalMoves = (x, y, z, a, c) => Array.from({ length: 12 }, (_, index) => {
|
||||
const sign = index % 2 === 0 ? 1 : -1;
|
||||
return `G1 X${sign * x} Y${sign * y} Z${sign * z} A${sign * a} C${sign * c} F600`;
|
||||
});
|
||||
const programs = [
|
||||
{ name: "browser-a.ngc", source: ["G21", "G90", "G0 X0 Y0 Z10 A0 C0", ...absoluteMoves, "M2", ""].join("\n") },
|
||||
{ name: "browser-b.ngc", source: ["G21", "G91", ...incrementalMoves(1, 0.5, 0, 1, 2), "M2", ""].join("\n") },
|
||||
{ name: "browser-c.ngc", source: ["G21", "G91", ...incrementalMoves(0.5, 1, 0.5, 2, 1), "M2", ""].join("\n") },
|
||||
];
|
||||
const results = [];
|
||||
const evidence = [];
|
||||
try {
|
||||
await page.goto(`${url}?strict-multi-load=20260907`, { waitUntil: "domcontentloaded", timeout: 30000 });
|
||||
results.push(await record("startup", async () => { await wait((s) => s.source === "linuxcnc-wasm", "startup"); }));
|
||||
results.push(await record("power_home", async () => {
|
||||
await click(main.getByRole("button", { name: "Load 3D_Chips.ngc", exact: true }), "initial_load");
|
||||
await wait((s) => s.program === "loaded" && !s.loading && s.error === null, "initial_loaded");
|
||||
await click(main.locator('button[aria-label="Power on [F2]"]'), "power");
|
||||
await wait((s) => s.machine === "Machine ON", "power_state");
|
||||
await click(main.locator('button[aria-label="Home all axes"]'), "home");
|
||||
await wait((s) => s.homed === "[true,true,true,true,true]", "home_state");
|
||||
}));
|
||||
for (const program of programs) {
|
||||
results.push(await record(`load_${program.name}`, async () => {
|
||||
const input = main.locator('input[type="file"][aria-label="Open G-code file"]');
|
||||
await input.setInputFiles({ name: program.name, mimeType: "text/plain", buffer: Buffer.from(program.source) });
|
||||
const loaded = await wait((s) => s.program === "loaded" && !s.loading && s.command?.includes("PROGRAM_LOAD acknowledged") && s.error === null, `${program.name}_loaded`);
|
||||
const hash = fnv1a64(program.source);
|
||||
if (!loaded.editor.includes(program.source.trim())) throw new Error(`${program.name}:editor_source_mismatch`);
|
||||
if (!loaded.sourceHash.includes(hash.slice(0, 12))) throw new Error(`${program.name}:source_hash_mismatch:${loaded.sourceHash}:${hash}`);
|
||||
if (Number(loaded.previewCount) <= 0) throw new Error(`${program.name}:preview_empty`);
|
||||
evidence.push({ name: program.name, source_hash: hash, preview_count: Number(loaded.previewCount), line_after_load: loaded.line });
|
||||
await click(main.locator('button[aria-label="Begin executing current file [R]"]'), `${program.name}_run`);
|
||||
const running = await wait((s) => ["running", "completed"].includes(s.program) && (Number(s.line) > 0 || Number(s.queue) > 0 || Number(s.velocity) > 0), `${program.name}_running`);
|
||||
await page.waitForTimeout(2000);
|
||||
const active = await state();
|
||||
if (Number(active.line) <= 0 && Number(active.queue) <= 0 && Number(active.velocity) <= 0) throw new Error(`${program.name}:motion_not_observed:${JSON.stringify(active)}`);
|
||||
let stopped = active;
|
||||
if (active.operation !== "Operation IDLE") {
|
||||
await click(main.locator('button[aria-label="Stop program execution [ESC]"]'), `${program.name}_stop`);
|
||||
stopped = await wait((s) => s.operation === "Operation IDLE" && ["loaded", "aborted", "completed"].includes(s.program) && s.error === null, `${program.name}_stopped`);
|
||||
}
|
||||
return { source_hash: hash, preview_count: Number(loaded.previewCount), running_line: running.line, stopped_program: stopped.program };
|
||||
}));
|
||||
}
|
||||
results.push(await record("round_trip_A", async () => {
|
||||
const input = main.locator('input[type="file"][aria-label="Open G-code file"]');
|
||||
await input.setInputFiles({ name: programs[0].name, mimeType: "text/plain", buffer: Buffer.from(programs[0].source) });
|
||||
const loaded = await wait((s) => s.program === "loaded" && !s.loading && s.error === null, "roundtrip_loaded");
|
||||
if (!loaded.sourceHash.includes(evidence[0].source_hash.slice(0, 12))) throw new Error("roundtrip_hash_mismatch");
|
||||
if (Number(loaded.previewCount) <= 0) throw new Error("roundtrip_preview_empty");
|
||||
}));
|
||||
} finally {
|
||||
await page.screenshot({ path: "/tmp/xyzac-multi-program-load-browser.png", fullPage: true });
|
||||
if (dialogs.length) failures.push({ name: "automatic_dialog", result: "failed", reason: JSON.stringify(dialogs) });
|
||||
console.log(JSON.stringify({ schema: "xyzac-multi-program-load-browser.v1", status: failures.length === 0 && browserErrors.length === 0 ? "verified" : "failed", url, results, evidence, dialogs, failures, browser_errors: browserErrors, final_state: await state(), screenshot: "/tmp/xyzac-multi-program-load-browser.png" }));
|
||||
await browser.close();
|
||||
}
|
||||
Reference in New Issue
Block a user