120 lines
16 KiB
JavaScript
120 lines
16 KiB
JavaScript
import assert from 'node:assert/strict';
|
|
import fs from 'node:fs';
|
|
import path from 'node:path';
|
|
import {createHash} from 'node:crypto';
|
|
import {loadWasmProbe} from './wasm-probe-runtime.mjs';
|
|
import {playwright} from './part-primitive-business-cases.mjs';
|
|
|
|
const wizard='PartDesign_WizardShaft',callback='PartDesign_WizardShaftCallBack';
|
|
export const shaftIds=[wizard,callback].map(command=>`PartDesign:desktop:${command}`);
|
|
export async function runShaftCases(ids,loadWorker){
|
|
if(!ids.length)return [];
|
|
const directory=path.resolve('docs/current/partdesign-shaft-business',new Date().toISOString().replace(/[:.]/g,'-'));fs.mkdirSync(directory,{recursive:true});
|
|
const results=ids.map(capabilityId=>({capabilityId,status:'pending',cases:[],evidenceFiles:{},remaining:[]}));
|
|
const record=(command,input,actual,name)=>results.find(r=>r.capabilityId.endsWith(`:${command}`))?.cases.push({input,actual,assertions:[{name,passed:true}]});
|
|
const near=(a,b,tolerance=1e-6)=>assert.ok(Number.isFinite(a)&&Math.abs(a-b)<Math.max(tolerance,Math.abs(b)*1e-7),`${a} != ${b}`);
|
|
const volume=segments=>segments.reduce((sum,s)=>sum+Math.PI/4*(s.diameter**2-s.innerDiameter**2)*s.length,0);
|
|
let kernel,browser;
|
|
try{
|
|
({kernel}=await loadWasmProbe('web/public/kernel','FreeCADWebPartDesignRuntimeProbe'));
|
|
const worker=loadWorker(kernel),document='ShaftBusiness';let sequence=0,selection;
|
|
const router={request:(_module,command,payload,doc)=>worker.send(worker.request(`shaft-${++sequence}`,command,payload,doc))};
|
|
const native=async(action,args={},doc=document)=>{const reply=await router.request('PartDesign','partdesign.document',{schema:'freecad-web-partdesign/document/v1',module:'PartDesign',document:doc,action,...args},doc);assert.equal(reply.kind,'result',JSON.stringify(reply));return reply.result;};
|
|
const desktop=(command,args,doc=document)=>worker.desktop(router,command,{document:doc,selection:[],...args},doc,(_doc,value)=>{selection=value;});
|
|
const accepted=async(args,doc=document)=>{const reply=await desktop(wizard,args,doc);assert.equal(reply.kind,'result',JSON.stringify(reply));return reply.result;};
|
|
await native('create');await native('create',{},'ShaftOther');const other=await native('snapshot',{},'ShaftOther');
|
|
const segments=[{length:30,diameter:20,innerDiameter:0,constraintType:'None'},{length:15,diameter:12,innerDiameter:0,constraintType:'None'}];
|
|
let state=await accepted({segments});near(state.shaft.volume,volume(segments));const handle=state.shaft.objectHandle;
|
|
assert.equal(state.shaft.analysisComputed,false);assert.equal(state.objects.find(o=>o.name===state.shaft.sketchHandle).type,'Sketcher::SketchObject');
|
|
assert.ok(state.objects.find(o=>o.name===handle).mesh.triangleCount>0);assert.equal(selection.length,1);assert.equal(selection[0].documentId,document);assert.equal(selection[0].object,handle);
|
|
record(wizard,{operation:'create',segments},{volume:state.shaft.volume,object:handle},'Desktop ShaftFeature builds a native sketch and revolution with analytic stepped-shaft volume');
|
|
segments[0].length=40;state=await accepted({segments});near(state.shaft.volume,volume(segments));assert.equal(state.shaft.objectHandle,handle);
|
|
await native('undo');state=await accepted({operation:'read'});near(state.shaft.volume,Math.PI*(100*30+36*15));assert.equal(state.shaft.segments[0].length,30);
|
|
await native('redo');state=await accepted({operation:'read'});near(state.shaft.volume,volume(segments));assert.equal(state.shaft.segments[0].length,40);
|
|
segments[0].length=45;state=await accepted({segments});near(state.shaft.volume,volume(segments));
|
|
record(wizard,{operations:['edit','undo','read','redo','edit-again']},{volume:state.shaft.volume,segments:state.shaft.segments},'Wizard editing state follows native history rather than stale Python session memory');
|
|
const stable=await native('snapshot');
|
|
for(const args of [{segments:[{length:-1,diameter:20,innerDiameter:0}]},
|
|
{segments:[{...segments[0],innerDiameter:2},segments[1]]},{segments:[{...segments[0],innerDiameter:20},segments[1]]},
|
|
{segments,activeTask:true},{segments,documentId:'ShaftOther'},{segments,selection:[{documentId:'ShaftOther',object:handle}]}]){
|
|
const reply=await desktop(wizard,args);assert.equal(reply.kind,'error',JSON.stringify(reply));assert.deepEqual(await native('snapshot'),stable);
|
|
record(wizard,args,reply.error,'Invalid dimensions and task inputs preserve geometry and transaction history');
|
|
}
|
|
for(const [constraintType,constraintProperties] of [['Force',{Force:1000}],['Fixed',{}],['Bearing',{AxialFree:true}],['Pulley',{}],['Gear',{}]]){
|
|
const reply=await desktop(wizard,{segments:[{...segments[0],constraintType,constraintProperties},segments[1]]});assert.equal(reply.kind,'result',JSON.stringify(reply));
|
|
const shaft=reply.result.shaft;assert.equal(shaft.segments[0].constraintType,constraintType);assert.ok(shaft.constraints.some(item=>item.segment===0&&item.constraintType===constraintType));
|
|
segments[0]={...segments[0],constraintType,constraintProperties};assert.ok((await native('snapshot')).objects.some(item=>item.type===`Fem::Constraint${constraintType}`));
|
|
record(wizard,{constraintType,constraintProperties},{constraintType,constraintType,object:shaft.constraints[0].objectHandle},'Native shaft constraint metadata is document-owned and survives recompute');
|
|
}
|
|
const callbackReply=await desktop(callback,{editedColumn:0,constraintType:'Force',constraintProperties:{Force:275}});assert.equal(callbackReply.kind,'result',JSON.stringify(callbackReply));
|
|
assert.equal(callbackReply.result.editedColumn,0);assert.equal(callbackReply.result.shaft.segments[0].constraintProperties.Force,275);
|
|
assert.ok(callbackReply.result.constraints.some(item=>item.constraintType==='Force'&&item.properties.Force===275));
|
|
record(callback,{editedColumn:0,Force:275},{constraintType:'Force',Force:callbackReply.result.shaft.segments[0].constraintProperties.Force},'Callback finishes an existing shaft constraint through a native document transaction');
|
|
assert.deepEqual(await native('snapshot',{},'ShaftOther'),other);
|
|
const committed=await native('snapshot');
|
|
const cancelled=await accepted({operation:'cancel',segments:[{...segments[0],length:999},segments[1]]});
|
|
assert.equal(cancelled.shaft.cancelled,true);assert.deepEqual(await native('snapshot'),committed);
|
|
assert.equal(cancelled.shaft.segments[0].length,45);assert.equal(cancelled.shaft.segments[0].constraintType,'Force');
|
|
assert.equal(cancelled.shaft.segments[0].constraintProperties.Force,275);
|
|
segments[0]=cancelled.shaft.segments[0];
|
|
record(wizard,{operation:'cancel',draftLength:999},{length:45,Force:275,undoCount:committed.undoCount},'Cancel discards unapplied dimensions and preserves all committed geometry, constraints and history');
|
|
const saved=await native('save',{path:'/tmp/ShaftBusiness.FCStd'});await native('close');await native('open',{path:'/tmp/ShaftBusiness.FCStd',bytes:saved.bytes});
|
|
state=await accepted({operation:'read'});near(state.shaft.volume,volume(segments));segments[1].length=20;state=await accepted({segments});near(state.shaft.volume,volume(segments));assert.equal(state.shaft.objectHandle,handle);
|
|
await native('undo');near((await accepted({operation:'read'})).shaft.volume,Math.PI*(100*45+36*15));
|
|
record(wizard,{operations:['cancel','save','close','open','edit-restored','undo']},{bytes:saved.bytes.byteLength},'FCStd restores editable wizard geometry and metadata without an old session');
|
|
await native('create',{},'HollowShaft');const hollow=[{length:10,diameter:20,innerDiameter:4},{length:5,diameter:12,innerDiameter:4}];
|
|
const bored=await accepted({segments:hollow},'HollowShaft');near(bored.shaft.volume,volume(hollow));record(wizard,{segments:hollow},{volume:bored.shaft.volume},'Native sketch builder creates a uniform-bore hollow shaft with analytic volume');
|
|
kernel.PThread?.terminateAllThreads();kernel=null;
|
|
browser=await playwright().chromium.launch({executablePath:process.env.FREECAD_CHROME??'/usr/bin/google-chrome',headless:true,args:['--no-sandbox','--disable-dev-shm-usage']});
|
|
const page=await browser.newPage({viewport:{width:1440,height:900},deviceScaleFactor:1}),errors=[];page.on('pageerror',e=>errors.push(String(e)));
|
|
await page.goto(`${process.env.FREECAD_BROWSER_ORIGIN??'http://127.0.0.1:5180'}/workbench.html?module=PartDesign&command=${wizard}&document=BrowserShaft`);
|
|
await page.getByLabel('Shaft document',{exact:true}).fill('BrowserShaft');await page.getByRole('button',{name:'Create shaft document',exact:true}).click();
|
|
await page.getByRole('button',{name:'Add shaft segment',exact:true}).click();await page.getByLabel('Segment 2 length',{exact:true}).fill('15');await page.getByLabel('Segment 2 diameter',{exact:true}).fill('12');
|
|
await page.getByRole('button',{name:'Apply shaft',exact:true}).click();await page.getByLabel('Shaft volume',{exact:true}).waitFor();near(parseFloat(await page.getByLabel('Shaft volume',{exact:true}).innerText()),Math.PI*(100*30+36*15),0.001);
|
|
await page.getByLabel('Segment 1 length',{exact:true}).fill('40');await page.getByRole('button',{name:'Apply shaft',exact:true}).click();await page.waitForFunction(()=>window.__freecadWorkbenchState?.response?.result?.shaft?.segments?.[0]?.length===40);
|
|
await page.getByRole('button',{name:'Shaft undo',exact:true}).click();await page.waitForFunction(()=>document.querySelector('[aria-label="Segment 1 length"]')?.value==='30');
|
|
await page.getByRole('button',{name:'Shaft redo',exact:true}).click();await page.waitForFunction(()=>document.querySelector('[aria-label="Segment 1 length"]')?.value==='40');
|
|
assert.equal(await page.getByLabel('Segment 1 constraint',{exact:true}).locator('option').filter({hasText:/^Force$/}).evaluate(option=>option.disabled),false);
|
|
await page.getByLabel('Segment 1 constraint',{exact:true}).selectOption('Force');await page.getByRole('button',{name:'Apply shaft',exact:true}).click();
|
|
await page.waitForFunction(()=>window.__freecadWorkbenchState?.response?.result?.shaft?.segments?.[0]?.constraintType==='Force');
|
|
const committedVolume=await page.getByLabel('Shaft volume',{exact:true}).innerText();
|
|
await page.getByLabel('Segment 1 length',{exact:true}).fill('999');
|
|
await page.getByRole('button',{name:'Cancel shaft edits',exact:true}).click();
|
|
await page.waitForFunction(()=>document.querySelector('[aria-label="Segment 1 length"]')?.value==='40');
|
|
assert.equal(await page.getByLabel('Segment 1 constraint',{exact:true}).inputValue(),'Force');
|
|
assert.equal(await page.getByLabel('Shaft volume',{exact:true}).innerText(),committedVolume);
|
|
await page.locator('.matrix-list button').filter({has:page.getByText(callback,{exact:true})}).click();
|
|
await page.getByLabel('Shaft force',{exact:true}).fill('275');
|
|
await page.getByRole('button',{name:'Apply force',exact:true}).click();
|
|
await page.waitForFunction(()=>document.querySelector('[aria-label="Shaft constraint force"]')?.textContent.trim()==='275');
|
|
assert.equal(await page.getByLabel('Shaft volume',{exact:true}).innerText(),committedVolume);
|
|
await page.getByLabel('Shaft force',{exact:true}).fill('999');
|
|
await page.getByRole('button',{name:'Cancel shaft edits',exact:true}).click();
|
|
await page.waitForFunction(()=>document.querySelector('[aria-label="Shaft force"]')?.value==='275');
|
|
assert.equal((await page.getByLabel('Shaft constraint force',{exact:true}).innerText()).trim(),'275');
|
|
record(callback,{workflow:['select-callback','apply-force','cancel-draft'],Force:275,draftForce:999},{Force:275,volume:committedVolume},'Mounted callback applies document constraint properties and cancel restores the committed force without changing geometry');
|
|
await page.locator('.matrix-list button').filter({has:page.getByText(wizard,{exact:true})}).click();
|
|
const capture=async name=>{
|
|
await page.waitForFunction(()=>document.querySelector('[aria-label="Shaft task"] fieldset')?.disabled===false);
|
|
await page.evaluate(()=>new Promise(resolve=>requestAnimationFrame(()=>requestAnimationFrame(resolve))));
|
|
await page.locator('[aria-label="Shaft viewport"] canvas').scrollIntoViewIfNeeded();
|
|
const clip=await page.locator('[aria-label="Shaft viewport"] canvas').boundingBox();assert.ok(clip);
|
|
const png=await page.screenshot({path:path.join(directory,name),clip});
|
|
const colors=await page.evaluate(async encoded=>{const image=new Image();image.src=`data:image/png;base64,${encoded}`;await image.decode();const c=document.createElement('canvas');c.width=image.width;c.height=image.height;const ctx=c.getContext('2d');ctx.drawImage(image,0,0);const p=ctx.getImageData(0,0,c.width,c.height).data,colors=new Set;for(let i=0;i<p.length;i+=4)colors.add(`${p[i]},${p[i+1]},${p[i+2]}`);return colors.size;},png.toString('base64'));assert.ok(colors>8);
|
|
const hash=createHash('sha256').update(png).digest('hex');for(const r of results)r.evidenceFiles[path.relative('.',path.join(directory,name))]=hash;return hash;
|
|
};
|
|
const first=await capture('desktop.png'),rect=await page.locator('[aria-label="Shaft viewport"] canvas').boundingBox();await page.mouse.move(rect.x+rect.width/2,rect.y+rect.height/2);await page.mouse.down();await page.mouse.move(rect.x+rect.width/2+65,rect.y+rect.height/2+28,{steps:8});await page.mouse.up();assert.notEqual(await capture('rotated.png'),first);
|
|
await page.getByRole('button',{name:'Save shaft document',exact:true}).click();await page.getByRole('link',{name:'Open shaft in project',exact:true}).waitFor();const bytes=await page.evaluate(async()=>{const {readDocumentFile}=await import('/src/storage.ts');return [...new Uint8Array(await readDocumentFile('Shaft-BrowserShaft.FCStd'))]});
|
|
await page.reload();await page.getByLabel('Shaft FCStd file',{exact:true}).setInputFiles({name:'BrowserShaft.FCStd',mimeType:'application/octet-stream',buffer:Buffer.from(bytes)});
|
|
await page.waitForFunction(()=>document.querySelector('[aria-label="Segment 1 length"]')?.value==='40');
|
|
assert.equal((await page.getByLabel('Shaft constraint force',{exact:true}).innerText()).trim(),'275');
|
|
record(callback,{operations:['save-OPFS','reload','open']},{Force:275},'Browser reopening restores the callback constraint value from native FCStd bytes');
|
|
await page.getByLabel('Segment 1 length',{exact:true}).fill('45');await page.getByRole('button',{name:'Apply shaft',exact:true}).click();await page.waitForFunction(()=>window.__freecadWorkbenchState?.response?.result?.shaft?.segments?.[0]?.length===45);
|
|
await page.setViewportSize({width:390,height:844});await capture('mobile.png');const task=page.getByRole('region',{name:'Shaft task',exact:true});assert.ok(await task.evaluate(e=>e.scrollWidth<=e.clientWidth+1));
|
|
const png=await task.screenshot({path:path.join(directory,'mobile-task.png')});for(const r of results)r.evidenceFiles[path.relative('.',path.join(directory,'mobile-task.png'))]=createHash('sha256').update(png).digest('hex');assert.deepEqual(errors,[]);
|
|
record(wizard,{workflow:['create','two-segments','edit','undo','redo','rotate','save-OPFS','reload','open','edit-restored'],viewports:[[1440,900],[390,844]]},{bytes:bytes.length},'Mounted geometry controls produce visible native shafts with persistent editing and responsive rendering');
|
|
}catch(error){for(const r of results){r.status='failed';r.error=String(error.stack??error);}}
|
|
finally{for(const result of results)if(result.status==='pending')result.status=result.cases.some(item=>item.assertions?.some(assertion=>!assertion.passed))?'failed':'passed';await browser?.close();kernel?.PThread?.terminateAllThreads();fs.writeFileSync(path.join(directory,'result.json'),JSON.stringify(results,null,2));}
|
|
return results;
|
|
}
|