Files
linuxcncWebWASM/scripts/test_xyzac_machine_model_browser.mjs

103 lines
7.1 KiB
JavaScript

import assert from "node:assert/strict";
import { mkdir, writeFile } from "node:fs/promises";
import { pathToFileURL } from "node:url";
const { chromium } = await import(pathToFileURL(process.env.PLAYWRIGHT_MODULE).href);
const output = process.env.MACHINE_ARTIFACT_DIR ?? "/tmp/xyzac-machine-model";
await mkdir(output, { recursive: true });
const browser = await chromium.launch({ executablePath: "/usr/bin/google-chrome", headless: true, args: ["--no-sandbox", "--use-angle=swiftshader", "--enable-unsafe-swiftshader"] });
const page = await browser.newPage({ viewport: { width: 1600, height: 1100 } });
const errors = [];
page.on("pageerror", (error) => errors.push(error.message));
page.on("dialog", (dialog) => dialog.accept());
const button = (id) => page.locator(`[data-axis-control="${id}"]`);
const machine = page.locator(".xyzac-machine-view");
const canvas = machine.locator("canvas[data-engine]");
const pixels = () => canvas.evaluate((canvas) => {
const gl = canvas.getContext("webgl2");
const bytes = new Uint8Array(canvas.width * canvas.height * 4);
gl.readPixels(0, 0, canvas.width, canvas.height, gl.RGBA, gl.UNSIGNED_BYTE, bytes);
let hash = 2166136261, lit = 0, minX = canvas.width, maxX = 0, minY = canvas.height, maxY = 0;
const colors = new Set();
for (let i = 0; i < bytes.length; i += 16) {
hash = Math.imul(hash ^ bytes[i], 16777619);
colors.add(`${bytes[i]},${bytes[i + 1]},${bytes[i + 2]}`);
if (bytes[i] + bytes[i + 1] + bytes[i + 2] > 330) {
lit++; const x = (i / 4) % canvas.width, y = Math.floor(i / 4 / canvas.width);
minX = Math.min(x, minX); maxX = Math.max(x, maxX); minY = Math.min(y, minY); maxY = Math.max(y, maxY);
}
}
return { hash: hash >>> 0, colors: colors.size, litRatio: lit / (bytes.length / 16), width: canvas.width, height: canvas.height, bounds: [minX, minY, maxX, maxY] };
});
const settled = () => page.waitForTimeout(400);
const report = { viewports: [], controls: [], motion: null };
try {
await page.goto(process.env.XYZAC_PANEL_URL ?? "http://127.0.0.1:5174/");
await page.waitForFunction(() => document.querySelector("main")?.dataset.axisProgramLoading === "false");
await machine.scrollIntoViewIfNeeded(); await settled();
assert.equal(await page.locator(".axis-preview-split canvas[data-engine]").count(), 2);
const initial = await pixels();
const toolBefore = await page.locator(".xyzac-tool-preview canvas[data-engine]").evaluate((canvas) => canvas.toDataURL());
assert.equal(await machine.getAttribute("data-xyzac-mouse-controls"), "left-rotate-middle-zoom-right-pan-wheel-zoom");
const box = await canvas.boundingBox();
assert.ok(box, "machine canvas must have a screen box");
const center = { x: box.x + box.width / 2, y: box.y + box.height / 2 };
const mouseHash = async () => (await pixels()).hash;
const beforeMouseRotate = await mouseHash();
await page.mouse.move(center.x, center.y); await page.mouse.down({ button: "left" });
await page.mouse.move(center.x + 120, center.y - 55, { steps: 8 }); await page.mouse.up({ button: "left" }); await settled();
assert.notEqual(await mouseHash(), beforeMouseRotate, "left drag must rotate the model");
const beforeMousePan = await mouseHash();
await page.mouse.move(center.x, center.y); await page.mouse.down({ button: "right" });
await page.mouse.move(center.x + 55, center.y + 25, { steps: 8 }); await page.mouse.up({ button: "right" }); await settled();
assert.notEqual(await mouseHash(), beforeMousePan, "right drag must pan the model");
const beforeWheel = await mouseHash();
await page.mouse.move(center.x, center.y); await page.mouse.wheel(0, -420); await settled();
assert.notEqual(await mouseHash(), beforeWheel, "wheel must zoom the model");
for (const action of ["zoomIn", "zoomOut", "panLeft", "panRight", "panUp", "panDown", "rotateLeft", "rotateRight", "rotateUp", "rotateDown", "tool", "reset"]) {
const before = await pixels();
await button(`machine-${action}`).click(); await settled();
const after = await pixels(); assert.notEqual(after.hash, before.hash, action);
report.controls.push(action);
if (action === "tool") await machine.screenshot({ path: `${output}/tool-detail.png` });
}
assert.equal(await page.locator(".xyzac-tool-preview canvas[data-engine]").evaluate((canvas) => canvas.toDataURL()), toolBefore, "machine controls must leave the toolpath camera alone");
assert.equal((await pixels()).hash, initial.hash, "reset should recover the fitted camera");
for (const width of [1600, 1024, 768, 390]) {
await page.setViewportSize({ width, height: width > 900 ? 1100 : 844 });
await machine.scrollIntoViewIfNeeded(); await settled();
const sample = await pixels();
assert.ok(sample.height <= 680, "canvas height must not grow with its drawing buffer");
assert.ok(sample.colors > 100 && sample.litRatio > .04, JSON.stringify(sample));
assert.ok(sample.bounds[0] > 0 && sample.bounds[1] > 0 && sample.bounds[2] < sample.width - 1 && sample.bounds[3] < sample.height - 1, `clipped model: ${JSON.stringify(sample)}`);
assert.equal(await page.evaluate(() => document.documentElement.scrollWidth <= innerWidth), true);
const overlap = await machine.evaluate((node) => {
const a = node.querySelector(".xyzac-machine-controller").getBoundingClientRect();
const b = node.querySelector(".xyzac-machine-hud").getBoundingClientRect();
return a.left < b.right && a.right > b.left && a.top < b.bottom && a.bottom > b.top;
});
assert.equal(overlap, false, "controller and HUD must not overlap");
await machine.screenshot({ path: `${output}/machine-${width}.png` });
report.viewports.push({ viewport: width, ...sample });
}
await page.setViewportSize({ width: 1600, height: 1100 });
await button("toolbar-power").click();
await button("manual-home-all").click();
await page.waitForFunction(() => document.querySelector("main")?.dataset.axisRunEnabled === "true");
await machine.scrollIntoViewIfNeeded(); await settled();
const jointsBefore = await machine.getAttribute("data-xyzac-joints");
const beforeMotion = await pixels();
await button("toolbar-run").click();
await page.waitForFunction((joints) => document.querySelector(".xyzac-machine-view")?.dataset.xyzacJoints !== joints, jointsBefore);
await machine.scrollIntoViewIfNeeded(); await settled();
const moving = await pixels(); assert.notEqual(moving.hash, beforeMotion.hash);
report.motion = { before: jointsBefore, after: await machine.getAttribute("data-xyzac-joints"), renderedTick: await machine.getAttribute("data-xyzac-rendered-tick") };
await page.locator(".axis-preview-split").screenshot({ path: `${output}/running-dual-preview.png` });
await button("toolbar-stop").click(); await settled();
await machine.scrollIntoViewIfNeeded(); await settled();
const stopped = await pixels(); await settled(); assert.equal((await pixels()).hash, stopped.hash, "stopped machine must not animate autonomously");
assert.deepEqual(errors, []);
await writeFile(`${output}/report.json`, JSON.stringify({ status: "verified", ...report, errors }, null, 2));
console.log(JSON.stringify({ status: "verified", output, ...report }));
} finally { await browser.close(); }