import assert from "node:assert/strict"; import { pathToFileURL } from "node:url"; const imported = await import(pathToFileURL(process.env.PLAYWRIGHT_MODULE).href); const { chromium } = imported.default ?? imported; const url = process.env.XYZAC_PANEL_URL ?? "http://127.0.0.1:5175/"; const browser = await chromium.launch({ headless: false, slowMo: 100, executablePath: "/usr/bin/google-chrome", args: ["--disable-gpu"] }); const page = await browser.newPage({ viewport: { width: 1440, height: 1100 } }); const errors = []; let confirmHome = true; page.on("pageerror", (e) => errors.push(e.message)); page.on("dialog", async (d) => { await page.waitForTimeout(600); if (d.type() === "confirm" && confirmHome) await d.accept(); else await d.dismiss(); }); await page.addInitScript(() => { window.homeResults = []; window.homeRequests = []; const Original = window.Worker; window.Worker = class extends Original { constructor(...args) { super(...args); this.addEventListener("message", ({ data }) => { const p = data.payload; window.homeResults.push({ seq: data.seq, kind: data.kind, command: p?.command, home: p?.homing, status: p?.source_status ?? p?.panel_snapshot?.source_status, pose: p?.panel_snapshot?.visual?.pose, epoch: p?.panel_snapshot?.visual?.live_path_epoch }); if (window.homeResults.length > 500) window.homeResults.shift(); }); } postMessage(data, ...args) { window.homeRequests.push({ seq: data.seq, kind: data.kind, payload: data.payload }); super.postMessage(data, ...args); } }; }); const control = (id) => page.locator(`[data-axis-control="${id}"]`); const status = () => page.evaluate(() => window.homeResults.filter((r) => r.status).at(-1).status); const home = async (id) => { const seq = await page.evaluate(() => window.homeRequests.at(-1)?.seq ?? 0); await control(id).click(); await page.waitForFunction((seq) => window.homeResults.some((r) => r.seq > seq && r.home?.phase === "accepted"), seq); await page.waitForFunction((seq) => window.homeResults.some((r) => r.seq > seq && r.status?.homing_history?.length > 1 && !r.status.homing_active), seq); const result = await status(); assert.equal(result.task.mode, "MANUAL"); assert.equal(result.task.interpState, "INTERP_IDLE"); assert.equal(result.traj.mode, result.joint.every((j) => j.homed) ? "TELEOP" : "FREE"); assert.ok(result.homing_history.some((event) => event.joint.some((j) => j.home_state === 21 && j.homing))); return result; }; const jog = async (axis) => { await control(`manual-axis-${axis}`).check(); const button = control("manual-jog-positive"); await button.scrollIntoViewIfNeeded(); const box = await button.boundingBox(); await page.mouse.move(box.x + box.width / 2, box.y + box.height / 2); await page.mouse.down(); await page.waitForTimeout(900); await page.mouse.up(); await page.waitForFunction(() => document.querySelector("main").dataset.axisTpQueueDepth === "0"); await page.waitForTimeout(250); return status(); }; try { await page.goto(url); await page.waitForFunction(() => document.querySelector("main")?.dataset.axisProgramLoading === "false"); await control("toolbar-power").click(); await home("manual-home-all"); for (const [joint, axis] of ["x", "y", "z", "a", "c"].entries()) { process.stderr.write(`Visible UI: Jog ${axis.toUpperCase()}, then Home Joint ${joint}\n`); const before = await jog(axis); assert.ok(before.joint[joint].position > 0); await control("manual-home-joint-select").selectOption(String(joint)); const after = await home("manual-home-axis"); for (let j = 0; j < 5; j++) { assert.ok(Math.abs(after.joint[j].motor_position - before.joint[j].motor_position) < 1e-8); assert.ok(Math.abs(after.joint[j].position - (j === joint ? 0 : before.joint[j].position)) < 1e-8); } } const before = await jog("x"); confirmHome = false; const count = await page.evaluate(() => window.homeRequests.filter((r) => r.kind === "HOME").length); await control("manual-home-all").click(); await page.waitForTimeout(800); assert.equal(await page.evaluate(() => window.homeRequests.filter((r) => r.kind === "HOME").length), count); confirmHome = true; const after = await home("manual-home-all"); assert.deepEqual(after.joint.map((j) => j.motor_position), before.joint.map((j) => j.motor_position)); assert.ok(after.joint.every((j) => j.homed && Math.abs(j.position) < 1e-8)); await control("tab-manual").click(); await page.waitForFunction(() => window.homeResults.filter((r) => r.status).at(-1)?.status.traj.mode === "TELEOP"); await page.locator('[aria-label="LinuxCNC status"]').scrollIntoViewIfNeeded(); await page.screenshot({ path: "/tmp/axis-native-home-desktop.png", fullPage: true }); const pixelCheck = async () => page.locator("canvas[data-engine]").evaluate((canvas) => { const gl = canvas.getContext("webgl2") ?? canvas.getContext("webgl"); 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(); for (let i=0;i 5); await control("toolbar-run").click(); await page.waitForFunction(() => window.homeResults.filter((r) => r.status).at(-1)?.status.task.mode === "AUTO"); assert.equal((await status()).traj.mode, "COORD"); await control("toolbar-pause-resume").click(); await page.waitForFunction(() => window.homeResults.filter((r) => r.status).at(-1)?.status.task.interpState === "INTERP_PAUSED"); await control("tab-manual").click(); assert.equal((await status()).task.mode, "AUTO"); assert.equal(await control("manual-home-all").isDisabled(), true); await control("toolbar-stop").click(); await page.waitForFunction(() => document.querySelector("main").dataset.axisTpQueueDepth === "0"); await page.setViewportSize({ width: 390, height: 844 }); await home("manual-home-all"); await page.locator('[aria-label="LinuxCNC status"]').scrollIntoViewIfNeeded(); await page.screenshot({ path: "/tmp/axis-native-home-mobile.png", fullPage: true }); assert.ok(await pixelCheck() > 5); assert.equal(await page.evaluate(() => document.documentElement.scrollWidth <= innerWidth), true); assert.deepEqual(errors, []); console.log(JSON.stringify({ status: "verified", url, checks: ["visible-five-joint-home", "motor-position-preserved", "native-state-history", "cancel", "auto-paused-mode", "desktop-mobile-canvas"] })); } finally { await browser.close(); }