|
- import fs from 'node:fs/promises';
- import path from 'node:path';
- import assert from 'node:assert/strict';
- import {chromium,expect} from '@playwright/test';
- import {login,call} from './role-permissions-live.mjs';
-
- const dir=path.resolve('reports/'+(process.env.REPORT_NAME||'bridge-inspection-20260915'));await fs.mkdir(dir,{recursive:true});
- const resultFile=process.env.RESULT_FILE||'result.json';
- const signed=await login('root'),checks=[],errors=[],writes=[];
- const pending=new Map(),networkErrors=[];
- const browser=await chromium.launch({channel:'msedge',headless:true,args:['--enable-unsafe-swiftshader']});
- const page=await browser.newPage({viewport:{width:Number(process.env.WIDTH)||1366,height:Number(process.env.HEIGHT)||768}});
- await page.addInitScript(s=>{sessionStorage.setItem('unreal-tran:web:access-token:v1',s.accessToken);sessionStorage.setItem('unreal-tran:web:refresh-token:v1',s.refreshToken);localStorage.setItem('unreal-tran:web:preferences:v1',JSON.stringify({themeMode:'military'}));},signed);
- await page.route(/\/src\/features\/virtual-training-legacy\/upstream\/views\/training\.js(?:\?|$)/,async route=>{
- const response=await route.fetch();await route.fulfill({response,body:(await response.text())+`
- globalThis.__inspectionSnapshot=(options={})=>{
- const e=activeTraining;if(!e?.sceneReady)return null;
- const id=e.getActiveTargetId(),object=e.parts.get(id),box=object?physicalBounds(object):null;
- const screen=object?e.worldToScreen(box.getCenter(new THREE.Vector3())):null;
- const materials=[];e.publishedContentRoot.traverse(o=>{if(o.isMesh&&!o.userData.trainingPickProxy)for(const m of Array.isArray(o.material)?o.material:[o.material])materials.push({opacity:m.opacity,baseline:e.viewerMaterialBaseline.get(m)?.opacity,side:m.side});});
- const labels=[...e.viewport.querySelectorAll('.training-part-label.is-visible')].map(n=>{const r=n.getBoundingClientRect();return {text:n.textContent.trim(),x:r.x,y:r.y,w:r.width,h:r.height,id:n.dataset.labelTarget||id};});
- const op=e.getActiveMechanismOperation()?.operation;
- const projected=object?e.inspection.projectBounds(object):null;
- const canvas=e.canvas.getBoundingClientRect();
- const pickPoints=[];
- if(projected){const ray=new THREE.Raycaster();for(const fy of [0.5,0.35,0.65,0.2,0.8])for(const fx of [0.5,0.35,0.65,0.2,0.8]){
- const x=projected.x+projected.w*fx,y=projected.y+projected.h*fy;
- ray.setFromCamera(new THREE.Vector2(x/canvas.width*2-1,1-y/canvas.height*2),e.camera);
- const hit=ray.intersectObjects(e.pickables,false).find(h=>!h.object.userData.trainingPickProxy&&effectivelyVisible(h.object));
- if(hit&&e.isObjectInsideTarget(hit.object,object))pickPoints.push({x:x+canvas.left,y:y+canvas.top});
- }}
- const pivot=op&&object?e.getMechanismPivot(object,op).toArray():null;
- const freePoints=[];
- if(options.free){const ray=new THREE.Raycaster();for(let y=70;y<canvas.height-80;y+=40)for(let x=30;x<canvas.width-30;x+=40){
- if(document.elementFromPoint(canvas.left+x,canvas.top+y)!==e.canvas)continue;
- ray.setFromCamera(new THREE.Vector2(x/canvas.width*2-1,1-y/canvas.height*2),e.camera);
- const hit=ray.intersectObjects(e.pickables,false).find(h=>!h.object.userData.trainingPickProxy&&effectivelyVisible(h.object));
- if(hit?.object.userData.partId==='scene:front-pump-rescue-vehicle')freePoints.push({x:canvas.left+x,y:canvas.top+y});
- }}
- return {id,step:e.session.stepIndex,operation:e.getActiveMechanismOperation()?.state.index,errors:e.errorCount,complete:e.session.isComplete(),pending:!!e.bridgeOperationPending||!!e.serverCommitPending,
- hdr:e.scene.environment?.name,distance:e.camera.position.distanceTo(e.controls.target),zoom:e.camera.zoom,exposure:e.renderer.toneMappingExposure,light:[e.ambient.intensity,e.sun.intensity],
- screen,bounds:box?box.getSize(new THREE.Vector3()).toArray():null,projected,pivot,pickPoints,freePoints,
- camera:e.camera.position.toArray(),target:e.controls.target.toArray(),outline:e.inspection.outline.selectedObjects.length,cutaway:e.inspection.cutaway.size,materials,labels,
- operationData:op,position:object?.position.toArray(),quaternion:(object?.userData.bridgeRotatingPart||object)?.quaternion.toArray()};
- };`});
- });
- page.on('pageerror',error=>errors.push(error.message));
- page.on('request',r=>pending.set(r,new URL(r.url()).pathname));
- page.on('requestfinished',r=>pending.delete(r));
- page.on('requestfailed',r=>{networkErrors.push({path:new URL(r.url()).pathname,error:r.failure()?.errorText});pending.delete(r);});
- page.on('request',request=>{if(request.url().includes('/api/tran/')&&!['GET','HEAD'].includes(request.method()))writes.push(request.url());});
- const snapshot=()=>page.evaluate(()=>globalThis.__inspectionSnapshot?.());
- const ready=async()=>{await expect(page.locator('[data-role="loading"]')).toHaveClass(/is-hidden/,{timeout:120000});await expect.poll(snapshot,{timeout:30000}).not.toBeNull();await page.waitForTimeout(1800);};
- try {
- for(const id of (process.env.TASK_IDS||'86,87,88,89').split(',')){
- console.log('OPEN',id);
- await page.goto((process.env.BASE_URL||'http://127.0.0.1:6180')+'/teaching/virtual-training/preview/'+id);await ready();
- const state=await snapshot();
- await page.screenshot({path:path.join(dir,`task-${id}-initial.png`)});
- assert.equal(state.hdr,'venice_sunset_1k');assert.ok(state.outline>0);
- assert.ok(state.materials.every(m=>m.side===2),'DoubleSide physical materials');
- assert.ok(state.materials.every(m=>m.baseline==null||m.opacity===m.baseline),'original opacity');
- assert.ok(state.projected.w>25&&state.projected.h>15,'target is readable');
- assert.ok(state.labels.some(l=>l.id===state.id),'active target label visible');
- checks.push({name:'task '+id+' first view',state});console.log('VIEW',id,JSON.stringify({projected:state.projected,labels:state.labels.map(l=>l.text),cutaway:state.cutaway}));
- if(process.env.EXTRA_CONTROLS==='1'){
- await page.locator('[data-viewer-action="tools"]').click();
- await page.locator('[data-action="viewer-reset-view"]').click();await page.waitForTimeout(1000);
- const free=await page.evaluate(()=>globalThis.__inspectionSnapshot({free:true}));assert.ok(free.freePoints.length,'vehicle body can be selected');
- const point=free.freePoints[0],beforeWrites=writes.length;
- await page.mouse.click(point.x,point.y);await page.waitForTimeout(900);
- const selected=await snapshot();assert.equal(selected.errors,free.errors);assert.equal(selected.step,free.step);assert.equal(selected.operation,free.operation);assert.equal(writes.length,beforeWrites);
- for(let i=0;i<4;i++)await page.locator('[data-action="toggle-issue-highlight"]').click();
- assert.ok((await snapshot()).materials.every(m=>m.baseline==null||m.opacity===m.baseline));
- await page.locator('[data-action="viewer-reset-view"]').click();await page.waitForTimeout(1000);
- const orbit=await page.evaluate(()=>globalThis.__inspectionSnapshot({free:true}));assert.ok(orbit.freePoints.length);
- const dragPoint=orbit.freePoints[0];await page.mouse.move(dragPoint.x,dragPoint.y);await page.mouse.down();await page.mouse.move(dragPoint.x+45,dragPoint.y+20,{steps:8});await page.mouse.up();await page.waitForTimeout(900);
- const moved=await snapshot();assert.equal(moved.errors,0);assert.notDeepEqual(moved.camera,orbit.camera);assert.equal(writes.length,beforeWrites);
- await page.locator('[data-viewer-action="tools"]').click();
- await page.locator('[data-viewer-action="fullscreen"]').click();await page.waitForTimeout(400);await page.keyboard.press('Escape');await page.waitForTimeout(500);
- await expect(page.locator('[data-viewer-action="fullscreen"]')).toHaveText('全屏操作');await expect(page.locator('[data-role="modal"]')).toBeHidden();
- assert.ok(await page.evaluate(()=>document.documentElement.scrollWidth<=innerWidth),'no page horizontal overflow');
- await page.screenshot({path:path.join(dir,'free-inspection-controls.png')});checks.push({name:'non-workflow body click/orbit, selection-highlight restoration, Escape fullscreen exit',pass:true});
- }
- if(process.env.EXECUTE==='1'){
- // Labels only focus: no operation, no validation request, no score/error change.
- const beforeWrites=writes.length;
- await page.locator('[data-role="part-label"]').click();await page.waitForTimeout(900);
- const enlarged=await snapshot();assert.equal(enlarged.step,state.step);assert.equal(enlarged.operation,state.operation);assert.equal(writes.length,beforeWrites);assert.ok(enlarged.zoom>state.zoom);
- // Native fullscreen and both collapsible panels remain reversible on laptops.
- await page.locator('[data-viewer-action="steps"]').click();await expect(page.locator('.training-steps-panel')).toBeVisible();
- await page.locator('[data-viewer-action="steps"]').click();await expect(page.locator('.training-steps-panel')).toBeHidden();
- await page.locator('[data-viewer-action="tools"]').click();await expect(page.locator('[data-role="model-commandbar"]')).toBeVisible();
- await page.locator('[data-action="toggle-part-labels"]').click();
- for(let n=0;n<2;n++){await page.locator('[data-action="toggle-issue-highlight"]').click();await page.waitForTimeout(100);}
- assert.ok((await snapshot()).materials.every(m=>m.baseline==null||m.opacity===m.baseline));
- await page.locator('[data-action="toggle-hide-others"]').click();await page.waitForTimeout(300);
- const isolated=await snapshot();
- assert.ok(isolated.labels.length>0 && isolated.labels.every(l=>l.id===enlarged.id),'hide others keeps the focused label and hides all others');
- await page.locator('[data-action="toggle-hide-others"]').click();
- await page.locator('[data-viewer-action="tools"]').click();
- await page.locator('[data-viewer-action="fullscreen"]').click();await page.waitForTimeout(500);
- await expect(page.locator('[data-viewer-action="fullscreen"]')).toHaveText('退出全屏');
- await page.screenshot({path:path.join(dir,`task-${id}-fullscreen.png`)});
- await page.locator('[data-viewer-action="fullscreen"]').click();await page.waitForTimeout(500);
- await expect(page.locator('[data-viewer-action="fullscreen"]')).toHaveText('全屏操作');
- const assignment=await call('tran/v1/teaching/assignments/'+id,signed.accessToken);
- const total=assignment.data.definitionSnapshot.steps.length;
- const prepared=new Set();
- let gestures=0;
- while(true){
- const start=await snapshot();
- if(assignment.data.definitionSnapshot.steps[start.step].mode==='measure' && !prepared.has(start.step)){
- await page.locator('[data-open-preparation]').click();
- await page.locator('[data-measurement]').fill('本次教员预览按脚本演示连接管路;管路处理方向与依据需由教员核对,此记录仅用于验证交互,不作实装质量判定。');
- await page.locator('[data-save-preparation]').click();await expect(page.locator('[data-save-status]')).toContainText('已保留');
- await page.locator('[data-close-preparation]').click();prepared.add(start.step);
- checks.push({name:'task '+id+' step '+(start.step+1)+' observation retained for instructor review',pass:true});
- }
- assert.ok(start.pickPoints.length,'visible target has a real mesh hit: '+start.id);
- assert.ok(Math.abs(start.distance-state.distance)<0.015,'consistent stand-off '+start.id);
- assert.equal(start.exposure,state.exposure);assert.deepEqual(start.light,state.light);
- for(let i=0;i<start.labels.length;i++)for(let j=i+1;j<start.labels.length;j++){const a=start.labels[i],b=start.labels[j];assert.ok(a.x+a.w<=b.x||b.x+b.w<=a.x||a.y+a.h<=b.y||b.y+b.h<=a.y,'labels do not overlap');}
- const point=start.pickPoints[0],operation=start.operationData;
- await page.mouse.move(point.x,point.y);await page.mouse.down();
- const draggable=process.env.CLICK_ONLY!=='1' && (operation.kind==='rotate'||operation.effects?.some(f=>f.offset||f.scale));
- if(draggable){
- await page.mouse.move(point.x-68,point.y,{steps:6});await page.waitForTimeout(100);
- const mid=await snapshot();
- if(operation.kind==='rotate'){
- assert.notDeepEqual(mid.quaternion,start.quaternion,'rotation follows held pointer '+start.id);
- if(!operation.effects?.some(effect=>effect.offset))assert.ok(Math.hypot(...mid.pivot.map((v,i)=>v-start.pivot[i]))<0.0001,'hinge remains anchored '+start.id);
- }
- await page.mouse.move(point.x-175,point.y,{steps:8});
- }
- await page.mouse.up();
- await expect.poll(async()=>{const s=await snapshot();return !s.pending&&(s.complete||s.operation!==start.operation);},{timeout:20000}).toBe(true);
- await page.waitForTimeout(850);const done=await snapshot();assert.equal(done.errors,0,'no false operation error');
- checks.push({name:'task '+id+' step '+(start.step+1)+' operation '+(start.operation+1),target:start.id,drag:!!draggable,distance:start.distance,projected:start.projected,labels:start.labels.length});
- console.log('ACTION',id,start.step+1,start.operation+1,start.id);
- if(++gestures>120)throw new Error('unexpected operation loop');
- if(done.complete){
- await page.screenshot({path:path.join(dir,`task-${id}-step-${start.step+1}.png`)});
- if(start.step+1>=total)break;
- await page.locator('[data-action="next"]').click();await page.waitForTimeout(1200);
- }
- }
- const after=await call('tran/v1/teaching/assignments/'+id,signed.accessToken);
- assert.deepEqual(after.data.definitionSnapshot,assignment.data.definitionSnapshot);assert.equal(after.data.version,assignment.data.version);
- }
- }
- assert.deepEqual(errors,[]);
- await fs.writeFile(path.join(dir,resultFile),JSON.stringify({pass:true,checks,errors,writes},null,2));
- console.log('PASS',checks.length);
- }catch(error){await page.screenshot({path:path.join(dir,'failure.png')}).catch(()=>{});await fs.writeFile(path.join(dir,resultFile),JSON.stringify({pass:false,error:error.message,checks,state:await snapshot(),errors,writes,networkErrors,pending:[...pending.values()]},null,2));throw error;}
- finally{await browser.close();}
|