Files
linuxcncWebWASM/scripts/test_axis_panel_source_flow_browser.mjs

237 lines
16 KiB
JavaScript

import assert from "node:assert/strict";
import { pathToFileURL } from "node:url";
const modulePath = process.env.PLAYWRIGHT_MODULE;
assert.ok(modulePath, "PLAYWRIGHT_MODULE is required");
const { chromium } = await import(pathToFileURL(modulePath).href);
const url = process.env.XYZAC_PANEL_URL ?? "http://127.0.0.1:5174/";
const browser = await chromium.launch({ headless: true, executablePath: process.env.BROWSER_EXECUTABLE_PATH ?? "/usr/bin/google-chrome", args: ["--disable-gpu"] });
const page = await browser.newPage({ viewport: { width: 1440, height: 1000 } });
const errors = [];
const checks = [];
let dialogAnswers = [];
page.on("pageerror", (error) => errors.push(error.message));
page.on("dialog", (dialog) => dialogAnswers.length ? dialog.accept(dialogAnswers.shift()) : dialog.dismiss());
await page.addInitScript(() => {
window.axisMessages = [];
window.axisResults = [];
const NativeWorker = window.Worker;
window.Worker = class extends NativeWorker {
constructor(...args) {
super(...args);
this.addEventListener("message", (event) => {
const value = event.data;
window.axisResults.push({ kind: value.kind, seq: value.seq, payload: value.payload });
if (window.axisResults.length > 100) window.axisResults.shift();
});
}
postMessage(value, ...args) {
window.axisMessages.push({ kind: value.kind, seq: value.seq, payload: value.payload });
super.postMessage(value, ...args);
}
};
});
const control = (id) => page.locator(`[data-axis-control="${id}"]`);
const wait = (fn, arg) => page.waitForFunction(fn, arg, { timeout: 45000 });
const ready = () => wait(() => document.querySelector("main")?.dataset.axisProgramLoading === "false");
const queueEmpty = () => wait(() => document.querySelector("main")?.dataset.axisTpQueueDepth === "0");
const messages = (kind) => page.evaluate((kind) => window.axisMessages.filter((entry) => entry.kind === kind), kind);
const commandCount = async (kind) => (await messages(kind)).length;
const ack = (kind, after = 0) => wait(({ kind, after }) => window.axisResults.some((result) => result.seq > after && result.kind === "COMMAND_RESULT" && window.axisMessages.some((message) => message.seq === result.seq && message.kind === kind)), { kind, after });
const latestSeq = () => page.evaluate(() => window.axisMessages.filter((entry) => !entry.kind.startsWith("UI_STATE_")).at(-1)?.seq ?? 0);
const clickAck = async (id, kind) => { const seq = await latestSeq(); await control(id).click(); await ack(kind, seq); };
const pose = () => page.locator('[data-xyzac-preview="toolpath"]').evaluate((node) => JSON.parse(node.dataset.xyzacLivePose));
const check = async (name, run) => { await run(); checks.push(name); process.stderr.write(`[source-flow] ${name}: verified\n`); };
const hold = async (id, release, axis) => {
const starts = await commandCount("JOG_START"), stops = await commandCount("JOG_STOP"), seq = await latestSeq();
const before = await pose();
await control(id).dispatchEvent("pointerdown", { pointerId: 1, button: 0 });
await ack("JOG_START", seq);
await page.waitForTimeout(400);
const during = await pose();
assert.notEqual(during[axis], before[axis], `no ${axis} motion`);
assert.equal(await commandCount("JOG_START"), starts + 1, "sampling must not reissue JOG_START");
await release();
await ack("JOG_STOP", seq);
await queueEmpty();
const stopped = await pose();
await page.waitForTimeout(180);
assert.deepEqual(await pose(), stopped, "motion continued after release");
assert.equal(await commandCount("JOG_STOP"), stops + 1, "release must stop exactly once");
};
const pixels = () => page.locator('[data-xyzac-preview="toolpath"] canvas').evaluate((canvas) => {
const gl = canvas.getContext("webgl2") ?? canvas.getContext("webgl");
if (!gl) return { colors: 0 };
const data = new Uint8Array(canvas.width * canvas.height * 4);
gl.readPixels(0, 0, canvas.width, canvas.height, gl.RGBA, gl.UNSIGNED_BYTE, data);
const colors = new Set(); let hash = 2166136261;
for (let i = 0; i < data.length; i += 16) { const value = (data[i] << 16) | (data[i + 1] << 8) | data[i + 2]; colors.add(value); hash = Math.imul(hash ^ value, 16777619); }
return { colors: colors.size, hash, width: canvas.width, height: canvas.height };
});
try {
await page.goto(url, { waitUntil: "domcontentloaded" });
await ready();
await check("startup-single-load", async () => assert.equal(await commandCount("PROGRAM_LOAD"), 1));
await clickAck("toolbar-power", "POWER");
await clickAck("manual-home-all", "HOME");
await check("mdi-trim-clear-deduplicate-history", async () => {
await control("tab-mdi").click();
const input = control("mdi-command");
const submit = async (text) => { const seq = await latestSeq(); await input.fill(text); await control("mdi-submit").click(); await ack("MDI_EXECUTE", seq); await queueEmpty(); assert.equal(await input.inputValue(), ""); };
await submit(" G21 "); await submit("G21");
assert.equal(await control("mdi-history-select").count(), 1);
await submit("G90");
assert.deepEqual(await control("mdi-history-select").allTextContents(), ["G21", "G90"]);
assert.equal((await messages("MDI_EXECUTE"))[0].payload.block, "G21");
await input.press("ArrowUp"); assert.equal(await input.inputValue(), "G90");
await input.press("ArrowUp"); assert.equal(await input.inputValue(), "G21");
await input.press("ArrowUp"); assert.equal(await input.inputValue(), "");
await input.press("ArrowDown"); assert.equal(await input.inputValue(), "G21");
const count = await commandCount("MDI_EXECUTE");
await control("mdi-history-select").last().click();
assert.equal(await commandCount("MDI_EXECUTE"), count);
const seq = await latestSeq(); await control("mdi-history-select").last().dblclick(); await ack("MDI_EXECUTE", seq);
assert.equal(await commandCount("MDI_EXECUTE"), count + 1);
assert.equal(await control("mdi-history-select").count(), 2);
});
await control("tab-manual").click();
await check("continuous-five-axes-both-directions", async () => {
for (const axis of ["x", "y", "z", "a", "c"]) {
await control(`manual-axis-${axis}`).check();
for (const direction of ["positive", "negative"]) {
const id = `manual-jog-${direction}`;
await hold(id, async () => {
await control(id).dispatchEvent("pointerup", { pointerId: 1, button: 0 });
await control(id).dispatchEvent("pointercancel", { pointerId: 1, button: 0 });
}, axis);
}
}
});
await control("manual-axis-x").check();
await check("jog-blur-and-hidden-tab-stop", async () => {
await hold("manual-jog-positive", () => page.evaluate(() => window.dispatchEvent(new Event("blur"))), "x");
await hold("manual-jog-positive", () => control("tab-diagnostics").click(), "x");
await control("tab-manual").click();
});
await check("jog-fast-release-and-keyboard-hold", async () => {
let starts = await commandCount("JOG_START"), stops = await commandCount("JOG_STOP");
await control("manual-jog-positive").evaluate((button) => {
button.dispatchEvent(new PointerEvent("pointerdown", { bubbles: true, pointerId: 9, button: 0 }));
button.dispatchEvent(new PointerEvent("pointerup", { bubbles: true, pointerId: 9, button: 0 }));
});
await wait((stops) => window.axisMessages.filter((m) => m.kind === "JOG_STOP").length === stops + 1, stops);
await queueEmpty(); assert.equal(await commandCount("JOG_START"), starts + 1);
await control("manual-jog-positive").focus();
starts = await commandCount("JOG_START"); stops = await commandCount("JOG_STOP");
await page.keyboard.down("Space"); await page.keyboard.down("Space"); await page.waitForTimeout(200); await page.keyboard.up("Space");
await wait((stops) => window.axisMessages.filter((m) => m.kind === "JOG_STOP").length === stops + 1, stops);
await queueEmpty(); assert.equal(await commandCount("JOG_START"), starts + 1);
});
await check("increment-completes-after-release", async () => {
await control("manual-jog-increment").selectOption("0.1000");
const before = await pose(), seq = await latestSeq(), stops = await commandCount("JOG_STOP");
await control("manual-jog-positive").press("Space"); await ack("JOG_START", seq); await queueEmpty();
assert.ok(Math.abs((await pose()).x - before.x - 0.1) < 1e-8);
assert.equal(await commandCount("JOG_STOP"), stops);
await control("manual-jog-increment").selectOption("Continuous");
});
await check("touch-off-preview-and-relative-coordinate", async () => {
const before = await pose();
dialogAnswers = ["2", "P1 G54"];
await clickAck("manual-touch-off", "TOUCH_OFF");
assert.deepEqual(await pose(), before);
await control("tab-dro").click(); await control("dro-coordinate").selectOption("relative");
assert.equal(Number(await page.locator(".dro-row").first().locator("output").first().textContent()), 2);
await control("dro-coordinate").selectOption("machine"); await control("tab-preview").click();
dialogAnswers = [String(before.x), "P1 G54"];
await clickAck("manual-touch-off", "TOUCH_OFF");
});
await check("spindle-key-toggle-and-release", async () => {
await clickAck("manual-spindle-ccw", "SPINDLE_COMMAND");
let seq = await latestSeq(); await page.keyboard.press("F9"); await ack("SPINDLE_COMMAND", seq);
assert.equal((await messages("SPINDLE_COMMAND")).at(-1).payload.direction, 0);
await clickAck("manual-spindle-cw", "SPINDLE_COMMAND");
seq = await latestSeq(); await page.keyboard.press("F10"); await ack("SPINDLE_COMMAND", seq);
assert.equal((await messages("SPINDLE_COMMAND")).at(-1).payload.direction, 0);
await clickAck("manual-spindle-cw", "SPINDLE_COMMAND");
seq = await latestSeq(); await control("manual-spindle-increase").dispatchEvent("pointerdown", { pointerId: 2, button: 0 }); await ack("SPINDLE_COMMAND", seq);
await page.evaluate(() => window.dispatchEvent(new Event("blur"))); await ack("SPINDLE_CONSTANT", seq);
await clickAck("manual-spindle-stop", "SPINDLE_COMMAND");
});
await check("paused-open-gate-and-source-key-case", async () => {
await clickAck("toolbar-run", "PROGRAM_RUN"); await clickAck("toolbar-pause-resume", "PROGRAM_PAUSE");
assert.ok(await control("toolbar-open").isDisabled());
assert.ok(await control("toolbar-open").getAttribute("title"));
const count = await commandCount("PROGRAM_LOAD");
await page.keyboard.press("Control+r"); await page.waitForTimeout(100);
assert.equal(await commandCount("PROGRAM_LOAD"), count);
await clickAck("toolbar-stop", "PROGRAM_ABORT");
await wait(() => document.querySelector("main")?.dataset.axisTaskMode === "MANUAL");
const commands = await commandCount("PROGRAM_RUN");
await page.keyboard.press("Control+r"); await ready();
assert.equal(await commandCount("PROGRAM_RUN"), commands, "Ctrl-R must never become Run");
await page.keyboard.press("R"); assert.equal(await commandCount("PROGRAM_RUN"), commands);
});
await check("repeat-step-and-restore-tab-mode", async () => {
const input = control("program-file-input");
const seq = await latestSeq();
await input.setInputFiles({ name: "source-step.ngc", mimeType: "text/plain", buffer: Buffer.from("G21 G90\nG1 X2 F600\nG1 X3\nM2\n") });
await ack("PROGRAM_LOAD", seq); await ready();
const x = (await pose()).x;
await clickAck("toolbar-step", "PROGRAM_STEP");
assert.equal((await pose()).x, x);
await clickAck("toolbar-step", "PROGRAM_STEP");
await clickAck("toolbar-step", "PROGRAM_STEP"); await queueEmpty();
await wait(() => Math.abs(JSON.parse(document.querySelector('[data-xyzac-preview="toolpath"]').dataset.xyzacLivePose).x - 2) < 1e-7, "step_first_motion_pose");
assert.ok(Math.abs((await pose()).x - 2) < 1e-7);
await clickAck("toolbar-step", "PROGRAM_STEP"); await queueEmpty();
await wait(() => Math.abs(JSON.parse(document.querySelector('[data-xyzac-preview="toolpath"]').dataset.xyzacLivePose).x - 3) < 1e-7, "step_second_motion_pose");
assert.ok(Math.abs((await pose()).x - 3) < 1e-7);
await clickAck("toolbar-stop", "PROGRAM_ABORT");
await wait(() => document.querySelector("main")?.dataset.axisTaskMode === "MANUAL");
});
await check("about-modal-and-estop-from-editor", async () => {
await control("menu-help").click(); await control("menu-help-about").click();
assert.equal(await page.evaluate(() => document.activeElement?.getAttribute("data-axis-control")), "about-close");
const runs = await commandCount("PROGRAM_RUN"); await page.keyboard.press("r"); assert.equal(await commandCount("PROGRAM_RUN"), runs);
await page.keyboard.press("Escape"); assert.equal(await control("about-close").count(), 0);
await control("program-editor").focus(); const seq = await latestSeq(); await page.keyboard.press("F1"); await ack("ESTOP", seq);
await wait(() => document.querySelector("[data-axis-state-machine]")?.dataset.axisStateMachine === "ESTOP");
assert.ok(await control("toolbar-power").isDisabled());
await clickAck("toolbar-estop", "ESTOP"); await clickAck("toolbar-power", "POWER");
});
await check("desktop-mobile-canvas-and-layout", async () => {
for (const viewport of [{ width: 1440, height: 1000 }, { width: 390, height: 844 }]) {
await page.setViewportSize(viewport); await control("tab-preview").click(); await control("toolbar-view-p").click(); await page.waitForTimeout(200);
for (const preset of ["z", "z2", "x", "y", "y2", "p"]) {
await control(`toolbar-view-${preset}`).click();
await wait((preset) => document.querySelector("[data-xyzac-preview]")?.dataset.xyzacProjection === (preset === "p" ? "perspective" : "orthographic"), preset);
}
const before = await pixels(); assert.ok(before.colors > 20, JSON.stringify(before));
await control("toolbar-rotate").click(); await page.waitForTimeout(250);
const stationary = await pixels(); await page.waitForTimeout(250);
assert.equal((await pixels()).hash, stationary.hash, "rotate mode must not animate the camera");
await control("preview-layer-velocity").check();
await wait(() => document.querySelector(".axis-preview-hud")?.textContent.includes("mm/min"));
await control("preview-layer-velocity").uncheck();
await control("preview-layer-limits").check();
await wait(() => document.querySelector("[data-xyzac-preview]")?.dataset.xyzacLimitsVisible === "true");
await control("preview-layer-limits").uncheck();
await control("preview-layer-extents").check();
await wait(() => document.querySelector("[data-xyzac-preview]")?.dataset.xyzacExtentsVisible === "true");
await control("preview-layer-extents").uncheck();
await control("toolbar-view-x").click(); await page.waitForTimeout(200);
const after = await pixels(); assert.notEqual(after.hash, before.hash, "view did not change canvas");
assert.equal(await page.evaluate(() => document.documentElement.scrollWidth <= innerWidth + 1), true, "horizontal page overflow");
assert.equal(await page.locator("img").evaluateAll((images) => images.some((image) => !image.complete || image.naturalWidth === 0)), false);
await page.screenshot({ path: `/tmp/axis-source-flow-${viewport.width}.png`, fullPage: true });
}
});
assert.deepEqual(errors, []);
console.log(JSON.stringify({ schema: "axis-panel-source-flow-browser.v1", status: "verified", url, checks, browser_errors: errors }));
} catch (error) {
process.stderr.write(JSON.stringify(await page.evaluate(() => ({ messages: window.axisMessages.slice(-12), results: window.axisResults.slice(-3).map((entry) => ({ kind: entry.kind, seq: entry.seq, status: entry.payload?.status, reason: entry.payload?.gate_reason ?? entry.payload?.message_key })) }))) + "\n");
await page.screenshot({ path: "/tmp/axis-source-flow-failure.png", fullPage: true });
throw error;
} finally { await browser.close(); }