import assert from 'node:assert/strict'; import { readFile, writeFile, mkdir } from 'node:fs/promises'; import { pathToFileURL } from 'node:url'; const module = await import(pathToFileURL(process.env.PLAYWRIGHT_MODULE ?? '/home/mes006/.npm/_npx/e41f203b7505f1fb/node_modules/playwright/index.js')); const browser = await (module.default ?? module).chromium.launch({ headless: true, executablePath: '/usr/bin/google-chrome', args: ['--no-sandbox', '--use-angle=swiftshader', '--enable-unsafe-swiftshader'] }); const page = await browser.newPage({ viewport: { width: 1440, height: 900 } }); const output = new URL('../coverage/glb-machine-manager/workbench/', import.meta.url); await mkdir(output, { recursive: true }); const report = { report_id: 'GLB-MGR-E14', status: 'running', url: process.env.MACHINE_MANAGER_URL ?? 'http://127.0.0.1:5180/', checks: [], errors: [] }; page.on('pageerror', (e) => report.errors.push(e.message)); page.on('console', (m) => { if (['error','warning'].includes(m.type())) report.errors.push(m.text()); }); const expectValue = async (locator, value) => { await locator.evaluate((el, expected) => new Promise((resolve, reject) => { const end = Date.now() + 5000; const check = () => Number(el.value) === expected ? resolve(true) : Date.now() > end ? reject(new Error(`value ${el.value} != ${expected}`)) : setTimeout(check, 20); check(); }), value); }; const c = (id) => page.locator(`[data-axis-control="${id}"]`); const preview = page.locator('[data-machine-manager-preview]'); const choose = async (name) => { await c('machine-manager-tab-structure').click(); await c('machine-tree-search').fill(name); const n = c('machine-tree-node').filter({ has: page.locator('span').filter({ hasText: new RegExp(`^${name}$`) }) }).first(); await n.click(); }; try { await page.goto(report.url); await page.waitForFunction(() => document.querySelector('main.axis-app')?.dataset.axisProgramLoading === 'false' && document.querySelector('main.axis-app')?.dataset.axisStateSource === 'linuxcnc-wasm'); await c('machine-manager-settings').click(); assert.equal(await page.locator('.machine-preview-empty').isVisible(), true); const fileDialog = page.locator('.machine-manager-dialog'); const chooser = page.waitForEvent('filechooser'); await c('machine-manager-import').click(); await chooser; await fileDialog.dispatchEvent('cancel'); assert.equal(await fileDialog.isVisible(), true); await page.evaluate(() => window.dispatchEvent(new Event('focus'))); await page.waitForTimeout(20); report.checks.push('Open Files Cancel leaves the machine manager dialog open'); const bytes = await readFile(new URL('../MILL700-U_1.glb', import.meta.url)); const upload = { name: 'MILL700-U_1.glb', mimeType: 'model/gltf-binary', buffer: bytes }; await c('machine-manager-files').setInputFiles(upload); await page.waitForFunction(() => document.querySelectorAll('[data-axis-control="machine-tree-node"]').length >= 132); await choose('MILL700-A-主轴-JT3'); await c('machine-tree-axis-role').selectOption('Z'); await c('machine-manager-undo').click(); assert.equal(await c('machine-tree-axis-role').inputValue(), ''); await c('machine-manager-redo').click(); assert.equal(await c('machine-tree-axis-role').inputValue(), 'Z'); await c('machine-axis-locate-z').click(); assert.equal(await c('machine-tree-search').inputValue(), ''); assert.match(await page.locator('.machine-part-editor h3').textContent(), /主轴-JT3/); assert.equal(await c('machine-tree-node').filter({ hasText:'MILL700-A-主轴-JT3' }).first().isVisible(), true); report.checks.push('search expands real nested nodes; axis overview locates selection; binding undo/redo'); await c('machine-joint-value').fill('12.5'); const local = JSON.parse(await preview.getAttribute('data-machine-manager-selected-transform')).local; await c('machine-joint-value').fill(''); assert.equal(await c('machine-joint-value').getAttribute('aria-invalid'), 'true'); assert.match(await c('machine-manager-validation').textContent(), /修正/); assert.deepEqual(JSON.parse(await preview.getAttribute('data-machine-manager-selected-transform')).local, local); await c('machine-joint-value').fill('0'); assert.equal(await c('machine-joint-value').getAttribute('aria-invalid'), 'false'); for (let i=0; i<4; i++) { await choose('MILL700-A-主轴-JT2'); await choose('MILL700-A-主轴-JT3'); await c('machine-joint-value').fill('30'); await expectValue(c('machine-joint-value'),30); const v=JSON.parse(await preview.getAttribute('data-machine-manager-selected-transform')).local; assert.ok(Math.abs(v[2] - .3989253330230713)<1e-7, 'switching selection and repeated fill must not concatenate input'); } await c('machine-joint-step-plus').click(); await expectValue(c('machine-joint-value'),31); await c('machine-joint-step-minus').click(); await expectValue(c('machine-joint-value'),30); await c('machine-joint-reset').click(); await expectValue(c('machine-joint-value'),0); report.checks.push('empty input preserves last pose; repeated selection/input does not concatenate; single-axis step and reset'); for (const [axis, name] of [['X','MILL700-A-主轴-JT1'],['Y','MILL700-A-主轴-JT2'],['A','MILL700-A-工作台-JT1'],['C','MILL700-A-工作台-JT2']]) { await choose(name); await c('machine-tree-axis-role').selectOption(axis); } assert.match(await c('machine-manager-validation').textContent(), /检查通过/); assert.equal(await c('machine-manager-apply').isEnabled(), true); await c('machine-manager-tab-calibration').click(); const off = page.locator('fieldset.machine-vector').filter({ hasText:'A 到 C 轴偏置' }); await off.getByLabel('X', { exact:true }).fill('5'); assert.match(await c('machine-manager-validation').textContent(), /必须为 0/); assert.equal(await c('machine-manager-export').isDisabled(), true); await c('machine-manager-undo').click(); await expectValue(off.getByLabel('X', { exact:true }), 0); await off.getByLabel('Y', { exact:true }).fill('18'); await c('machine-manager-undo').click(); await expectValue(off.getByLabel('Y', { exact:true }), 0); await c('machine-manager-redo').click(); await expectValue(off.getByLabel('Y', { exact:true }), 18); report.checks.push('TRT forbidden offset blocks export before build; calibration undo/redo'); await c('machine-manager-tab-structure').click(); await c('machine-axis-locate-z').click(); for (const action of ['fit','selected','front','top']) { await c(`machine-view-${action}`).click(); assert.equal(await preview.getAttribute('data-machine-manager-view'),action); } await c('machine-view-fit').click(); await c('machine-manager-show-origin').uncheck(); await c('machine-manager-show-origin').check(); await c('machine-manager-delete').click(); await page.waitForFunction(() => document.querySelector('[data-machine-manager-preview]')?.dataset.machineManagerPartCount === '0'); await c('machine-manager-undo').click(); await page.waitForFunction(() => document.querySelector('[data-machine-manager-preview]')?.dataset.machineManagerPartCount === '1'); assert.equal(await c('machine-manager-apply').isEnabled(), true); await c('machine-axis-locate-z').click(); assert.equal(await c('machine-tree-axis-role').inputValue(), 'Z'); report.checks.push('view presets and selected fitting; deletion undo restores model with all axis bindings'); const validationBefore = await c('machine-manager-validation').textContent(); await c('machine-manager-files').setInputFiles([upload,{ name:'bad.glb',mimeType:'model/gltf-binary',buffer:Buffer.from('bad') }]); await page.waitForFunction(() => document.querySelector('.machine-manager-workbench')?.getAttribute('aria-busy') === 'false'); assert.equal(await preview.getAttribute('data-machine-manager-part-count'),'1'); assert.equal(await c('machine-manager-validation').textContent(),validationBefore); report.checks.push('failed batch import leaves existing assembly intact'); await c('machine-manager-tab-motion').click(); await c('machine-joint-follow-live').check(); assert.equal(await c('machine-manager-undo').isDisabled(),true); await c('machine-manager-tab-structure').click(); assert.equal(await c('machine-tree-axis-role').isDisabled(),true); assert.equal(await c('machine-manager-delete').isDisabled(),true); await c('machine-manager-tab-motion').click(); await c('machine-joint-follow-live').uncheck(); await c('machine-manager-tab-structure').click(); await c('machine-view-fit').click(); await page.screenshot({ path:new URL('desktop.png',output).pathname }); for (const [width,height] of [[1154,520],[720,900]]) { await page.setViewportSize({width,height}); await page.waitForTimeout(300); const bounds = await page.locator('.machine-manager-dialog').boundingBox(); assert.ok(bounds.x >= 0 && bounds.y >= 0 && bounds.x+bounds.width <= width+1 && bounds.y+bounds.height <=height+1); const footer = await page.locator('.machine-manager-footer').boundingBox(); assert.ok(footer.y+footer.height <=height); const buffer = (await preview.getAttribute('data-machine-manager-buffer-size')).split('x').map(Number); assert.ok(buffer.every((n)=>n>0 && n<=4096)); assert.equal(await page.locator('.machine-manager-dialog').evaluate((el)=>el.scrollWidth<=el.clientWidth+1),true); await page.screenshot({ path:new URL(`viewport-${width}.png`,output).pathname }); } report.checks.push('live mode locks edits; desktop, short viewport and narrow viewport keep footer visible without overflow'); await page.setViewportSize({width:1440,height:900}); await page.keyboard.press('Escape'); await c('machine-manager-settings').click(); await c('machine-axis-locate-z').click(); assert.equal(await c('machine-tree-axis-role').inputValue(),'Z'); assert.deepEqual(report.errors, []); report.status='verified'; } catch(e) { report.status='failed'; report.error=e.stack; await page.screenshot({path:new URL('failure.png',output).pathname}).catch(()=>{}); throw e; } finally { await writeFile(new URL('browser.report.json',output),JSON.stringify(report,null,2)+'\n'); await browser.close(); } console.log(JSON.stringify(report));