import assert from 'node:assert/strict'; import { writeFile } from 'node:fs/promises'; import { pathToFileURL } from 'node:url'; const { chromium } = await import(pathToFileURL(process.env.PLAYWRIGHT_MODULE).href); const output = new URL('../coverage/glb-machine/jog-limits/', import.meta.url); 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: 1440, height: 1000 } }); await page.addInitScript(() => { window.jogErrors = []; const OriginalWorker = window.Worker; window.Worker = class extends OriginalWorker { constructor(...args) { super(...args); this.addEventListener('message', ({ data }) => { if (data.kind === 'ERROR') window.jogErrors.push({ seq: data.seq, ...data.payload }); window.jogLastResponse = data; }); } }; }); const errors = []; page.on('pageerror', (e) => errors.push(e.message)); const control = (id) => page.locator(`[data-axis-control="${id}"]`); const sample = () => page.locator('.xyzac-machine-view').evaluate((e) => ({ joints: JSON.parse(e.dataset.xyzacJoints), tip: JSON.parse(e.dataset.xyzacToolTip), tick: Number(e.dataset.xyzacTick) })); const queueEmpty = () => page.waitForFunction(() => window.jogLastResponse?.payload?.wasm_motion?.state?.queue_depth === 0 || window.jogLastResponse?.payload?.panel_snapshot?.wasm_motion?.state?.queue_depth === 0); async function seed(block, angle) { await control('tab-mdi').click(); await control('mdi-command').fill(block); await control('mdi-submit').click(); await page.waitForFunction((angle) => Math.abs(JSON.parse(document.querySelector('.xyzac-machine-view').dataset.xyzacJoints)[3] - angle) < 1e-7, angle); await queueEmpty(); await control('tab-manual').click(); } async function hold(axis, direction, until) { await control(`manual-axis-${axis}`).check(); const box = await control(direction > 0 ? 'manual-jog-positive' : 'manual-jog-negative').boundingBox(); await page.mouse.move(box.x + box.width / 2, box.y + box.height / 2); await page.mouse.down(); try { await until(); } finally { await page.mouse.up(); } await page.waitForFunction(() => document.querySelector('.command-status')?.textContent?.includes('JOG_STOP acknowledged')); } const report = { report_id: 'jog-limit-browser-v1', status: 'running', machines: [] }; try { await page.goto('http://127.0.0.1:5174/'); await page.waitForFunction(() => document.querySelector('main')?.dataset.axisProgramLoading === 'false'); for (const id of ['ac', 'bc']) { await control('machine-glb-settings').click(); await control(`machine-glb-${id}`).click(); await page.waitForFunction(() => document.querySelector('main')?.dataset.axisProgramLoading === 'false'); await page.keyboard.press('Escape'); await control('toolbar-power').click(); await control('manual-home-all').click(); await page.waitForFunction(() => !document.querySelector('[data-axis-control="switchkins-1"]').disabled); const axis = id[0]; await seed(`G21 G90 G0 X0 Y0 Z30 ${axis.toUpperCase()}49 C20`, 49); await hold(axis, 1, async () => { await page.waitForFunction(() => Math.abs(JSON.parse(document.querySelector('.xyzac-machine-view').dataset.xyzacJoints)[3] - 50) < 1e-7); await queueEmpty(); }); const atLimit = await sample(); assert.ok(atLimit.joints[3] <= 50 + 1e-8); await hold(axis, -1, () => page.waitForFunction(() => JSON.parse(document.querySelector('.xyzac-machine-view').dataset.xyzacJoints)[3] < 49)); const retreat = await sample(); assert.ok(retreat.joints[3] < atLimit.joints[3]); await seed(`G90 G0 X190 Y0 Z30 ${axis.toUpperCase()}0 C0`, 0); await control('switchkins-1').click(); const count = await page.evaluate(() => window.jogErrors.length); await hold('c', 1, () => page.waitForFunction((count) => window.jogErrors.length > count, count)); const fault = await page.evaluate((count) => window.jogErrors[count], count); assert.equal(fault.code, 'motion_limit'); assert.equal(fault.reason, 'motion_joint_limit'); await queueEmpty(); const stopped = await sample(); assert.ok(stopped.joints[1] <= 100 + 1e-7); await page.waitForTimeout(150); assert.deepEqual((await sample()).joints, stopped.joints); await hold('c', -1, () => page.waitForFunction((c) => JSON.parse(document.querySelector('.xyzac-machine-view').dataset.xyzacJoints)[4] < c - 1, stopped.joints[4])); const resumed = await sample(); assert.ok(resumed.joints[4] < stopped.joints[4]); for (let i = 0; i < 4; i++) { const before = await sample(), dir = i % 2 ? -1 : 1; await hold('c', dir, () => page.waitForFunction(({ c, dir }) => (JSON.parse(document.querySelector('.xyzac-machine-view').dataset.xyzacJoints)[4] - c) * dir > .2, { c: before.joints[4], dir })); } assert.doesNotMatch(await page.locator('.axis-window-header').innerText(), /ERROR/); const pixels = await page.locator('.xyzac-machine-view canvas').evaluate((canvas) => { const gl = canvas.getContext('webgl2'), bytes = new Uint8Array(canvas.width * canvas.height * 4), colors = new Set(); gl.readPixels(0, 0, canvas.width, canvas.height, gl.RGBA, gl.UNSIGNED_BYTE, bytes); for (let i = 0; i < bytes.length; i += 32) colors.add(`${bytes[i]},${bytes[i + 1]},${bytes[i + 2]}`); return colors.size; }); assert.ok(pixels > 100); assert.notDeepEqual(resumed.tip, stopped.tip); await page.screenshot({ path: new URL(`${id}-recovered.png`, output).pathname }); await control('toolbar-power').click(); await page.waitForFunction(() => !document.querySelector('[data-axis-control="machine-glb-import"]').disabled); report.machines.push({ id, atLimit, retreat, stopped, resumed, fault: { code: fault.code, reason: fault.reason }, pixels }); } const faults = await page.evaluate(() => window.jogErrors.map((e) => ({ code: e.code, reason: e.reason }))); assert.ok(faults.every((e) => e.code === 'motion_limit'), JSON.stringify(faults)); assert.deepEqual(errors, []); report.status = 'verified'; } catch (e) { report.status = 'failed'; report.error = String(e.stack ?? e); await page.screenshot({ path: new URL('failure.png', output).pathname }); throw e; } finally { report.page_errors = errors; await writeFile(new URL('browser.report.json', output), JSON.stringify(report, null, 2) + '\n'); await browser.close(); } console.log(JSON.stringify({ report_id: report.report_id, status: report.status, machines: report.machines.map((m) => m.id) }));