import fs from 'node:fs'; import assert from 'node:assert/strict'; import { chromium, expect } from '@playwright/test'; import { login } from './role-permissions-live.mjs'; const baseUrl=process.env.BASE_URL||'http://127.0.0.1:6180',viewport={width:Number(process.env.WIDTH||1920),height:Number(process.env.HEIGHT||945)}; const dir='reports/bridge-introduction-3d-20260925'+(process.env.REPORT_SUFFIX||'');fs.mkdirSync(dir,{recursive:true}); const session=await login('root'),checks=[],errors=[]; const browser=await chromium.launch({channel:'msedge',headless:true,args:['--enable-unsafe-swiftshader','--autoplay-policy=no-user-gesture-required']}); const page=await browser.newPage({viewport}); 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:'light'})); },session); 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.__introSceneState=(withBounds=false)=>{ const e=activeTraining;if(!e?.sceneReady)return null; const p=e.introductionScene,v=e.viewport.getBoundingClientRect(); const rect=selector=>{const node=e.viewport.querySelector(selector);if(!node?.getClientRects().length||getComputedStyle(node).visibility==='hidden')return null;const r=node.getBoundingClientRect();return{x:r.x-v.x,y:r.y-v.y,w:r.width,h:r.height};}; const objects=withBounds?(e.viewerEquipmentRoots||[]).map(root=>{ let x=Infinity,y=Infinity,right=-Infinity,bottom=-Infinity;const point=new THREE.Vector3();root.updateWorldMatrix(true,true); root.traverse(m=>{if(!m.isMesh||m.userData.trainingPickProxy)return;for(let n=m;n;n=n.parent)if(!n.visible)return; const position=m.geometry.attributes.position;for(let i=0;ierrors.push(error.message)); const snapshot=(bounds=false)=>page.evaluate(b=>globalThis.__introSceneState?.(b),bounds); const settled=async()=>{await page.waitForTimeout(800);await expect.poll(async()=>(await snapshot())?.tween,{timeout:20000}).toBe(false);}; const overlap=(a,b)=>b&&a.xb.x&&a.yb.y; try { for(const id of (process.env.TASK_IDS||'86,87,88,89').split(',')){ await page.goto(baseUrl+'/teaching/virtual-training/preview/'+id); await expect.poll(async()=>!!(await snapshot())?.previewCamera,{timeout:120000}).toBe(true); const overlay=page.locator('.training-introduction'),canvas=page.locator('.training-canvas'); await expect(canvas).toBeVisible();assert.equal(await canvas.count(),1);assert.equal(await page.locator('.introduction-background').count(),0); const initial=await snapshot();assert.equal(initial.sameRenderer,true);assert.equal(initial.controlsEnabled,false); await page.waitForTimeout(1500);const rotated=await snapshot();assert.ok(rotated.angle-initial.angle>.015&&rotated.angle-initial.angle<.2); assert.deepEqual(rotated.camera,initial.camera);assert.deepEqual(rotated.completed,[]); assert.equal(await overlay.locator('video').evaluate(v=>v.paused&&v.currentTime===0),true); await overlay.locator('[data-intro-scene-rotation]').click();const paused=await snapshot();await page.waitForTimeout(500); assert.equal((await snapshot()).angle,paused.angle);await page.screenshot({path:`${dir}/task-${id}-background.png`}); await overlay.locator('[data-intro-scene-rotation]').click(); if(process.env.CHECK_LAYOUT==='1'){ await page.locator('[data-viewer-action="fullscreen"]').click();await page.waitForTimeout(700); await expect(overlay.locator('canvas')).toBeVisible();await page.screenshot({path:`${dir}/task-${id}-fullscreen.png`}); await page.locator('[data-viewer-action="fullscreen"]').click();await page.waitForTimeout(700); await page.setViewportSize({width:1366,height:768});await page.waitForTimeout(700);await expect(overlay.locator('canvas')).toBeVisible(); await page.setViewportSize(viewport);await page.waitForTimeout(700); } const count=await page.locator('[data-intro-page]').count(); for(let i=1;istart.angle); assert.equal(await overlay.locator('video').evaluate(v=>v.paused),true); } if(count>1)await page.screenshot({path:`${dir}/task-${id}-lecture.png`}); await overlay.locator('[data-intro-media="toggle"]').click();await expect.poll(()=>overlay.locator('video').evaluate(v=>v.currentTime),{timeout:30000}).toBeGreaterThan(.1); await overlay.locator('[data-intro-command="skip"]').click();await settled(); const operation=await snapshot(true);assert.equal(operation.canvasInPreview,false);assert.equal(operation.controlsEnabled,true);assert.deepEqual(operation.completed,[]); assert.equal(await page.locator('.training-server-video').evaluate(v=>v.paused),true); if(id==='87')for(const object of operation.objects.slice(0,2)){assert.equal(!!overlap(object,operation.tools),false,'vehicle avoids the tools panel');assert.equal(!!overlap(object,operation.teacher),false,'vehicle avoids the teacher');} await page.screenshot({path:`${dir}/task-${id}-operation.png`}); if(id==='87'&&process.env.CHECK_LAYOUT==='1')for(let toggle=0;toggle<2;toggle++){ await page.locator('[data-viewer-action="tools"]').click();await settled();const layout=await snapshot(true); for(const object of layout.objects.slice(0,2)){assert.equal(!!overlap(object,layout.tools),false,'panel toggle keeps both vehicles clear');assert.equal(!!overlap(object,layout.teacher),false);} } await page.locator('[data-role="part-label"]').click();await settled();const focused=await snapshot(); await page.locator('[data-intro-page="0"]').click();await expect(overlay.locator('canvas')).toBeVisible();await page.waitForTimeout(500); assert.deepEqual((await snapshot()).camera,focused.camera,'preview rotation cannot move the operation camera'); await overlay.locator('[data-intro-command="skip"]').click();await settled(); assert.deepEqual((await snapshot()).camera,focused.camera,'return restores focused operation camera'); await page.locator('[data-action="reset-view"]').click();await settled();const reset=await snapshot(true); assert.ok(Math.abs(reset.objects[0].w-operation.objects[0].w)<2,'reset returns to the same overview'); await page.screenshot({path:`${dir}/task-${id}-reset.png`}); checks.push({task:id,introductionPages:count,rotationRadians:rotated.angle-initial.angle,initial,operation,reset});console.log('PASS 3D introduction',id,count,'pages'); } assert.deepEqual(errors,[]);fs.writeFileSync(dir+'/result.json',JSON.stringify({pass:true,baseUrl,viewport,accountRole:'root',checks,errors},null,2)); }catch(error){await page.screenshot({path:dir+'/failure.png'}).catch(()=>{});fs.writeFileSync(dir+'/result.json',JSON.stringify({pass:false,error:error.stack,snapshot:await snapshot(true).catch(()=>null),checks,errors},null,2));throw error;}finally{await browser.close();}