Save named machine documents and restore editor configuration from GLB or local library
This commit is contained in:
@@ -13,7 +13,7 @@ const [contract, app, panel, preview] = await Promise.all([
|
||||
const controlIds = [
|
||||
"menu-file", "menu-file-edit-tool-data", "menu-file-quit", "menu-help", "menu-help-about", "about-close",
|
||||
"toolbar-estop", "toolbar-power", "toolbar-open", "toolbar-load-case", "toolbar-reload", "toolbar-run", "toolbar-step", "toolbar-pause-resume", "toolbar-stop", "toolbar-block-delete", "toolbar-optional-pause", "toolbar-zoom-in", "toolbar-zoom-out", "toolbar-view-z", "toolbar-view-z2", "toolbar-view-x", "toolbar-view-y", "toolbar-view-y2", "toolbar-view-p", "toolbar-rotate", "toolbar-clear-live-plot",
|
||||
"workspace-case-select", "workspace-load-3d-chips", "workspace-rtcp-programs", "rtcp-program-scope", "rtcp-program-select", "rtcp-program-source", "rtcp-program-load", "rtcp-program-download", "machine-manager-settings", "machine-manager-type", "machine-manager-type-note", "machine-manager-import", "machine-manager-files", "machine-manager-delete", "machine-manager-role", "machine-manager-parent", "machine-manager-show-origin", "machine-tree-node", "machine-tree-origin", "machine-tree-axis-role", "machine-joint-direction", "machine-joint-sign", "machine-joint-value", "machine-joint-step-minus", "machine-joint-step-plus", "machine-joint-reset", "machine-joint-follow-live", "machine-joint-live-state", "machine-manager-export-json", "machine-manager-undo", "machine-manager-redo", "machine-manager-draft-state", "machine-tree-search", "machine-tree-expand", "machine-tree-collapse", "machine-manager-validation", "machine-test-x", "machine-test-y", "machine-test-z", "machine-test-a", "machine-test-b", "machine-test-c", "machine-test-reset", "machine-test-state", "machine-manager-apply", "machine-manager-export", "machine-manager-tab-structure", "machine-manager-tab-motion", "machine-manager-tab-calibration", "machine-view-fit", "machine-view-selected", "machine-view-front", "machine-view-top", "manual-home-all", "manual-home-joint-select",
|
||||
"workspace-case-select", "workspace-load-3d-chips", "workspace-rtcp-programs", "rtcp-program-scope", "rtcp-program-select", "rtcp-program-source", "rtcp-program-load", "rtcp-program-download", "machine-manager-settings", "machine-document-new", "machine-document-open", "machine-document-open-file", "machine-document-from-controller", "machine-document-name", "machine-document-description", "machine-document-location", "machine-library-search", "machine-library-open", "machine-library-download", "machine-library-delete", "machine-manager-tab-files", "machine-manager-type", "machine-manager-type-note", "machine-manager-import", "machine-manager-files", "machine-manager-delete", "machine-manager-role", "machine-manager-parent", "machine-manager-show-origin", "machine-tree-node", "machine-tree-origin", "machine-tree-axis-role", "machine-joint-direction", "machine-joint-sign", "machine-joint-value", "machine-joint-step-minus", "machine-joint-step-plus", "machine-joint-reset", "machine-joint-follow-live", "machine-joint-live-state", "machine-manager-export-json", "machine-manager-undo", "machine-manager-redo", "machine-manager-draft-state", "machine-tree-search", "machine-tree-expand", "machine-tree-collapse", "machine-manager-validation", "machine-test-x", "machine-test-y", "machine-test-z", "machine-test-a", "machine-test-b", "machine-test-c", "machine-test-reset", "machine-test-state", "machine-manager-apply", "machine-manager-export", "machine-manager-tab-structure", "machine-manager-tab-motion", "machine-manager-tab-calibration", "machine-view-fit", "machine-view-selected", "machine-view-front", "machine-view-top", "manual-home-all", "manual-home-joint-select",
|
||||
"switchkins-0", "switchkins-1", "switchkins-2",
|
||||
"tab-manual", "tab-mdi", "tab-diagnostics", "tab-preview", "tab-dro",
|
||||
"manual-axis-x", "manual-axis-y", "manual-axis-z", "manual-axis-a", "manual-axis-c",
|
||||
|
||||
59
scripts/test_machine_document_browser.mjs
Normal file
59
scripts/test_machine_document_browser.mjs
Normal file
@@ -0,0 +1,59 @@
|
||||
import assert from 'node:assert/strict';
|
||||
import { readFile,writeFile,mkdir } from 'node:fs/promises';
|
||||
import { pathToFileURL } from 'node:url';
|
||||
import * as THREE from '../web/react/node_modules/three/build/three.module.js';
|
||||
import { GLTFExporter } from '../web/react/node_modules/three/examples/jsm/exporters/GLTFExporter.js';
|
||||
globalThis.FileReader=class{readAsArrayBuffer(blob){blob.arrayBuffer().then(v=>{this.result=v;this.onloadend?.();});}readAsDataURL(blob){blob.arrayBuffer().then(v=>{this.result=`data:${blob.type};base64,${Buffer.from(v).toString('base64')}`;this.onloadend?.();});}};
|
||||
const fixtures=await Promise.all(Array.from({length:8},async(_,i)=>{const s=new THREE.Scene();s.name=`part_${i}`;const mesh=new THREE.Mesh(new THREE.BoxGeometry(.05,.05,.05),new THREE.MeshStandardMaterial({color:0x458e83}));mesh.name=`mesh_${i}`;mesh.position.set(.01,.02,.03);s.add(mesh);const extra=mesh.clone();extra.name=`extra_${i}`;extra.position.x=.07;s.add(extra);const b=await new GLTFExporter().parseAsync(s,{binary:true,trs:true});return{name:`part-${i}.glb`,mimeType:'model/gltf-binary',buffer:Buffer.from(b)};}));
|
||||
const pw=await import(pathToFileURL(process.env.PLAYWRIGHT_MODULE??'/home/mes006/.npm/_npx/e41f203b7505f1fb/node_modules/playwright/index.js'));
|
||||
const browser=await(pw.default??pw).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:1000}});
|
||||
const out=new URL('../coverage/glb-machine-manager/document/',import.meta.url);await mkdir(out,{recursive:true});
|
||||
const report={report_id:'GLB-MGR-E17',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 c=id=>page.locator(`[data-axis-control="${id}"]`),preview=page.locator('[data-machine-manager-preview]');
|
||||
const idle=()=>page.waitForFunction(()=>document.querySelector('main.axis-app')?.dataset.axisProgramLoading==='false'&&document.querySelector('main.axis-app')?.dataset.axisStateSource==='linuxcnc-wasm',null,{timeout:60000});
|
||||
const choose=async(name)=>{await c('machine-manager-tab-structure').click();await c('machine-tree-search').fill(name);await c('machine-tree-node').filter({has:page.locator('span').filter({hasText:new RegExp(`^${name}$`)})}).first().click();};
|
||||
const save=async()=>{const p=page.waitForEvent('download');await c('machine-manager-export').click();const d=await p;const bytes=await readFile(await d.path());return{bytes,filename:d.suggestedFilename(),json:JSON.parse(bytes.toString('utf8',20,20+bytes.readUInt32LE(12)))}};
|
||||
const editor=saved=>saved.json.scenes[0].extras.linuxcncMachineEditor;
|
||||
try{
|
||||
await page.goto(report.url);await idle();await c('machine-manager-settings').click();await c('machine-manager-files').setInputFiles(fixtures);await page.waitForFunction(()=>document.querySelector('[data-machine-manager-preview]')?.dataset.machineManagerPartCount==='8');
|
||||
for(const[i,a]of[[1,'X'],[2,'Y'],[3,'Z'],[5,'A'],[6,'C']]){await choose(`part_${i}`);await c('machine-tree-axis-role').selectOption(a);}
|
||||
await choose('part_3');await c('machine-joint-sign').selectOption('-1');await c('machine-manager-show-origin').uncheck();
|
||||
await choose('extra_3');const extra=c('machine-tree-node').filter({has:page.locator('span').filter({hasText:/^extra_3$/})}).first();const sourcePath=await extra.locator('xpath=ancestor::li[1]').getAttribute('data-machine-tree-path');const partId=await extra.locator('xpath=ancestor::li[1]').getAttribute('data-machine-tree-part');
|
||||
await c('machine-tree-search').fill('');const target=c('machine-tree-node').filter({has:page.locator('span').filter({hasText:/^mesh_3$/})}).first();const targetPath=await target.locator('xpath=ancestor::li[1]').getAttribute('data-machine-tree-path');
|
||||
await target.evaluate((el,p)=>{const d=new DataTransfer();d.setData('application/x-linuxcnc-machine-tree',JSON.stringify(p));el.dispatchEvent(new DragEvent('drop',{bubbles:true,cancelable:true,dataTransfer:d}));},{partId,path:sourcePath});
|
||||
await page.waitForFunction(({partId,sourcePath,targetPath})=>document.querySelector(`[data-machine-tree-part="${partId}"][data-machine-tree-path="${sourcePath}"]`)?.dataset.machineTreeParent===targetPath,{partId,sourcePath,targetPath});
|
||||
await choose('part_7');await page.locator('.machine-part-flags input').first().uncheck();
|
||||
await c('machine-manager-tab-calibration').click();const field=t=>page.locator('fieldset.machine-vector').filter({hasText:t});await field('C 轴原点').getByLabel('X',{exact:true}).fill('24');await field('A 到 C 轴偏置').getByLabel('Y',{exact:true}).fill('18');await page.locator('fieldset.machine-zero').getByLabel('A',{exact:true}).fill('12');
|
||||
await c('machine-manager-tab-files').click();await c('machine-document-name').fill('Workshop Mill');await c('machine-document-description').fill('Revision 2: calibrated A pivot');const first=await save();assert.equal(first.filename,'Workshop Mill-xyzac.glb');assert.equal(editor(first).parts.length,8);assert.equal(editor(first).info.description,'Revision 2: calibrated A pivot');
|
||||
await page.waitForFunction(()=>document.querySelectorAll('[data-machine-saved-id]').length===1);assert.match(await c('machine-document-location').textContent(),/下载.*本浏览器机床库/s);
|
||||
const id=await page.locator('[data-machine-saved-id]').first().getAttribute('data-machine-saved-id');await writeFile(new URL('saved-eight-parts.glb',out),first.bytes);
|
||||
await choose('part_3');await c('machine-manager-show-origin').check();assert.doesNotMatch(await c('machine-manager-draft-state').textContent(),/与保存版本一致/);await c('machine-manager-undo').click();assert.match(await c('machine-manager-draft-state').textContent(),/与保存版本一致/);
|
||||
report.checks.push('named GLB downloads and atomic browser-library copy with time/size/checksum; editor display changes mark document dirty');
|
||||
await page.reload();await idle();await c('machine-manager-settings').click();await c('machine-manager-tab-files').click();await page.waitForFunction(()=>document.querySelectorAll('[data-machine-saved-id]').length===1);await c('machine-library-open').click();await page.waitForFunction(()=>document.querySelector('[data-machine-manager-preview]')?.dataset.machineManagerPartCount==='8');
|
||||
await choose('part_3');assert.equal(await c('machine-tree-axis-role').inputValue(),'Z');assert.equal(await c('machine-joint-sign').inputValue(),'-1');assert.equal(await c('machine-manager-show-origin').isChecked(),false);
|
||||
await choose('part_7');assert.equal(await page.locator('.machine-part-flags input').first().isChecked(),false);
|
||||
await c('machine-manager-tab-calibration').click();assert.equal(await field('C 轴原点').getByLabel('X',{exact:true}).inputValue(),'24');assert.equal(await field('A 到 C 轴偏置').getByLabel('Y',{exact:true}).inputValue(),'18');assert.equal(await page.locator('fieldset.machine-zero').getByLabel('A',{exact:true}).inputValue(),'12');
|
||||
await c('machine-manager-tab-files').click();assert.equal(await c('machine-document-name').inputValue(),'Workshop Mill');assert.equal(await c('machine-document-description').inputValue(),'Revision 2: calibrated A pivot');
|
||||
const second=await save();assert.deepEqual(editor(second),editor(first));assert.deepEqual(second.json.scenes[0].extras.linuxcncJointModel,first.json.scenes[0].extras.linuxcncJointModel);assert.deepEqual(second.json.scenes[0].extras.linuxcncMachineGeometry,first.json.scenes[0].extras.linuxcncMachineGeometry);
|
||||
report.checks.push('reload and local-library open restore eight source files, exact edited hierarchy, axis/sign, visibility, origins and calibration; second save has identical metadata');
|
||||
await c('machine-document-new').click();assert.equal(await preview.getAttribute('data-machine-manager-part-count'),'0');await c('machine-document-open-file').setInputFiles({name:first.filename,mimeType:'model/gltf-binary',buffer:first.bytes});await page.waitForFunction(()=>document.querySelector('[data-machine-manager-preview]')?.dataset.machineManagerPartCount==='8');
|
||||
await c('machine-manager-tab-files').click();const dl=page.waitForEvent('download');await page.locator(`[data-machine-saved-id="${id}"] [data-axis-control="machine-library-download"]`).click();assert.deepEqual(await readFile(await(await dl).path()),first.bytes);
|
||||
page.once('dialog',d=>d.accept());await page.locator(`[data-machine-saved-id="${id}"] [data-axis-control="machine-library-delete"]`).click();await page.waitForFunction(id=>!document.querySelector(`[data-machine-saved-id="${id}"]`),id);assert.equal(await preview.getAttribute('data-machine-manager-part-count'),'8');
|
||||
report.checks.push('explicit open-file entry restores document; library redownload is byte identical; delete affects only chosen saved copy');
|
||||
const damaged=structuredClone(first.json);damaged.scenes[0].extras.linuxcncMachineEditor.parts[3].settings.nodeJoints.scene.sign=1;
|
||||
const text=Buffer.from(JSON.stringify(damaged));const pad=(4-text.length%4)%4;const jsonBytes=Buffer.concat([text,Buffer.alloc(pad,32)]);const oldLen=first.bytes.readUInt32LE(12),bin=first.bytes.subarray(20+oldLen),header=Buffer.alloc(20);first.bytes.copy(header,0,0,12);header.writeUInt32LE(20+jsonBytes.length+bin.length,8);header.writeUInt32LE(jsonBytes.length,12);header.writeUInt32LE(0x4e4f534a,16);
|
||||
await c('machine-document-open-file').setInputFiles({name:'damaged.glb',mimeType:'model/gltf-binary',buffer:Buffer.concat([header,jsonBytes,bin])});await page.waitForFunction(()=>document.querySelector('.machine-manager-workbench')?.getAttribute('aria-busy')==='false');assert.equal(await preview.getAttribute('data-machine-manager-part-count'),'8');assert.match(await page.locator('.machine-manager-footer').textContent(),/无法读取/);
|
||||
report.checks.push('inconsistent editor metadata is rejected atomically without replacing the current draft');
|
||||
await page.getByRole('button',{name:'清除操作消息'}).click();await page.screenshot({path:new URL('files-and-recovery.png',out).pathname});
|
||||
await page.keyboard.press('Escape');await c('machine-glb-settings').click();await c('machine-glb-file').setInputFiles({name:first.filename,mimeType:'model/gltf-binary',buffer:first.bytes});await page.waitForFunction(()=>document.querySelector('.xyzac-machine-view')?.dataset.machineJointState);await idle();await page.keyboard.press('Escape');
|
||||
await c('machine-manager-settings').click();await c('machine-document-new').click();await c('machine-document-from-controller').click();await page.waitForFunction(()=>document.querySelector('[data-machine-manager-preview]')?.dataset.machineManagerPartCount==='8');await choose('part_3');assert.equal(await c('machine-joint-sign').inputValue(),'-1');
|
||||
report.checks.push('saved GLB loads through control-panel importer and can be sent back into editor with eight original parts');
|
||||
// The legacy machine remains readable without editor-document metadata.
|
||||
await c('machine-document-open-file').setInputFiles(new URL('../coverage/glb-machine-manager/machine-type/test-xyzbc.glb',import.meta.url).pathname);await page.waitForFunction(()=>document.querySelector('[data-axis-control="machine-manager-type"]')?.value==='XYZBC');assert.equal(await preview.getAttribute('data-machine-manager-part-count'),'1');report.checks.push('legacy GLB remains editable via single-assembly fallback');
|
||||
// A quota/IDB failure must still allow download and report exact persistence result.
|
||||
await c('machine-manager-tab-files').click();await page.evaluate(()=>{indexedDB.open=()=>{throw new Error('quota');};});const fallback=await save();assert.ok(fallback.bytes.length>0);assert.match(await c('machine-document-location').textContent(),/未保存成功/);
|
||||
report.checks.push('local storage failure does not prevent GLB download and is visibly reported');
|
||||
assert.deepEqual(report.errors,[]);report.status='verified';
|
||||
}catch(e){report.status='failed';report.error=e.stack;report.text=await page.locator('body').innerText().catch(()=> '');await page.screenshot({path:new URL('failure.png',out).pathname}).catch(()=>{});throw e;}finally{await writeFile(new URL('browser.report.json',out),JSON.stringify(report,null,2)+'\n');await browser.close();}
|
||||
console.log(JSON.stringify(report));
|
||||
Reference in New Issue
Block a user