103 lines
7.1 KiB
JavaScript
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(); }
|