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/switching/', 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 } }); const errors = []; page.on('pageerror', (e) => errors.push(e.message)); const control = (id) => page.locator(`[data-axis-control="${id}"]`); const rendered = () => page.waitForFunction(() => { const e = document.querySelector('.xyzac-machine-view'); return e?.dataset.xyzacTick === e?.dataset.xyzacRenderedTick; }); 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 close = (a, b, label) => a.forEach((v, i) => assert.ok(Math.abs(v - b[i]) < 1e-8, `${label}[${i}] ${v} != ${b[i]}`)); async function mdi(block) { await control('tab-mdi').click(); await control('mdi-command').fill(block); await control('mdi-submit').click(); await page.waitForFunction(() => document.querySelector('.command-status')?.textContent?.includes('MDI_EXECUTE acknowledged')); } const report = { report_id: 'trt-stationary-switch-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('tab-manual').click(); await control('manual-home-all').click(); await page.waitForFunction(() => !document.querySelector('[data-axis-control="switchkins-1"]').disabled); const tilt = id[0].toUpperCase(); await mdi(`G21 G90 G0 X0 Y0 Z44.5858 ${tilt}4.8675 C179.88`); await page.waitForFunction(() => Math.abs(JSON.parse(document.querySelector('.xyzac-machine-view').dataset.xyzacJoints)[4] - 179.88) < 1e-7); await control('tab-manual').click(); await page.waitForFunction(() => !document.querySelector('[data-axis-control="switchkins-1"]').disabled); await rendered(); const initial = await sample(), rows = []; await page.locator('.axis-preview-machine').screenshot({ path: new URL(`${id}-identity.png`, output).pathname }); for (const type of [1, 0, 1, 0, 2, 1, 0]) { await control(`switchkins-${type}`).click(); await page.waitForFunction((type) => document.querySelector(`[data-axis-control="switchkins-${type}"]`)?.getAttribute('aria-pressed') === 'true', type); await rendered(); const after = await sample(); close(after.joints, initial.joints, `${id} button joints`); close(after.tip, initial.tip, `${id} button tip`); await page.waitForTimeout(100); const idle = await sample(); close(idle.tip, initial.tip, `${id} idle tip`); rows.push({ type, after }); if (type === 1) await page.locator('.axis-preview-machine').screenshot({ path: new URL(`${id}-tcp.png`, output).pathname }); } for (const type of [1, 0, 1]) { await mdi(type === 1 ? 'M428' : 'M429'); await rendered(); const after = await sample(); close(after.joints, initial.joints, `${id} MDI joints`); close(after.tip, initial.tip, `${id} MDI tip`); rows.push({ mdi: true, type, after }); } // A subsequent rotary jog must still produce actual XYZ compensation. await control('tab-manual').click(); await control(`manual-axis-${id[0]}`).check(); const beforeJog = await sample(), box = await control('manual-jog-positive').boundingBox(); await page.mouse.move(box.x + box.width / 2, box.y + box.height / 2); await page.mouse.down(); try { await page.waitForFunction((angle) => JSON.parse(document.querySelector('.xyzac-machine-view').dataset.xyzacJoints)[3] > angle + 1, beforeJog.joints[3]); } finally { await page.mouse.up(); } await page.waitForFunction(() => document.querySelector('.command-status')?.textContent?.includes('JOG_STOP acknowledged')); await rendered(); const afterJog = await sample(); assert.ok(afterJog.joints.slice(0, 3).some((v, i) => Math.abs(v - beforeJog.joints[i]) > 1e-5)); const compensation = await page.evaluate(async ({ id, beforeJog, afterJog }) => { const api = await import('/src/imported-machine.ts'); const asset = await api.loadImportedMachine(await (await fetch(`/machines/demo-${id}-xyz-head.glb`)).arrayBuffer(), `${id}.glb`); const model = api.buildImportedMachine(asset), offsets = { ...asset.structure.offsets, tool_offset: 0 }; model.update({ ...beforeJog, offsets }); const target = model.workOrigin.worldToLocal(model.toolTip.getWorldPosition(model.toolTip.position.clone())); model.update({ ...afterJog, offsets }); const tip = model.toolTip.getWorldPosition(target.clone()), point = model.workOrigin.localToWorld(target.clone()); const error = point.distanceTo(tip); model.dispose(); api.disposeMachineScene(asset.scene); return error; }, { id, beforeJog, afterJog }); assert.ok(compensation < 1e-8); await control('switchkins-0').click(); await rendered(); close((await sample()).joints, afterJog.joints, 'switch after jog'); await control('toolbar-power').click(); await page.waitForFunction(() => !document.querySelector('[data-axis-control="machine-glb-import"]').disabled); report.machines.push({ id, initial, rows, afterJog, tcp_jog_error: compensation }); } assert.deepEqual(errors, []); report.status = 'verified'; } catch (error) { report.status = 'failed'; report.error = String(error.stack ?? error); await page.screenshot({ path: new URL('failure.png', output).pathname }); throw error; } finally { report.browser_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, switches: report.machines.reduce((n, m) => n + m.rows.length + 1, 0) }));