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(); }