Ви не можете вибрати більше 25 тем Теми мають розпочинатися з літери або цифри, можуть містити дефіси (-) і не повинні перевищувати 35 символів.
 
 
 
 

88 рядки
8.0 KiB

  1. import fs from 'node:fs';
  2. import assert from 'node:assert/strict';
  3. import { chromium, expect } from '@playwright/test';
  4. import { login } from './role-permissions-live.mjs';
  5. 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)};
  6. const dir='reports/bridge-introduction-3d-20260925'+(process.env.REPORT_SUFFIX||'');fs.mkdirSync(dir,{recursive:true});
  7. const session=await login('root'),checks=[],errors=[];
  8. const browser=await chromium.launch({channel:'msedge',headless:true,args:['--enable-unsafe-swiftshader','--autoplay-policy=no-user-gesture-required']});
  9. const page=await browser.newPage({viewport});
  10. await page.addInitScript(s=>{
  11. sessionStorage.setItem('unreal-tran:web:access-token:v1',s.accessToken);sessionStorage.setItem('unreal-tran:web:refresh-token:v1',s.refreshToken);
  12. localStorage.setItem('unreal-tran:web:preferences:v1',JSON.stringify({themeMode:'light'}));
  13. },session);
  14. await page.route(/\/src\/features\/virtual-training-legacy\/upstream\/views\/training\.js(?:\?|$)/,async route=>{
  15. const response=await route.fetch();await route.fulfill({response,body:await response.text()+`
  16. globalThis.__introSceneState=(withBounds=false)=>{
  17. const e=activeTraining;if(!e?.sceneReady)return null;
  18. const p=e.introductionScene,v=e.viewport.getBoundingClientRect();
  19. 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};};
  20. const objects=withBounds?(e.viewerEquipmentRoots||[]).map(root=>{
  21. let x=Infinity,y=Infinity,right=-Infinity,bottom=-Infinity;const point=new THREE.Vector3();root.updateWorldMatrix(true,true);
  22. root.traverse(m=>{if(!m.isMesh||m.userData.trainingPickProxy)return;for(let n=m;n;n=n.parent)if(!n.visible)return;
  23. const position=m.geometry.attributes.position;for(let i=0;i<position.count;i++){
  24. point.fromBufferAttribute(position,i).applyMatrix4(m.matrixWorld).project(e.camera);
  25. const px=(point.x+1)*v.width/2,py=(1-point.y)*v.height/2;x=Math.min(x,px);y=Math.min(y,py);right=Math.max(right,px);bottom=Math.max(bottom,py);
  26. }
  27. });return{name:root.name,x,y,w:right-x,h:bottom-y};
  28. }):undefined;
  29. return{intro:e.introductionActive,step:e.session.stepIndex,completed:[...e.session.completed],camera:e.camera.position.toArray(),target:e.controls.target.toArray(),zoom:e.camera.zoom,
  30. angle:p?.angle,paused:p?.paused,previewCamera:p?.camera?.position.toArray(),canvasInPreview:!!e.canvas.closest('[data-intro-scene]'),sameRenderer:e.renderer.domElement===e.canvas,
  31. controlsEnabled:e.controls.enabled,tween:!!e.viewerCameraTween,tools:rect('.training-scene-tools'),teacher:rect('.training-teacher.is-open'),objects};
  32. };`});
  33. });
  34. page.on('pageerror',error=>errors.push(error.message));
  35. const snapshot=(bounds=false)=>page.evaluate(b=>globalThis.__introSceneState?.(b),bounds);
  36. const settled=async()=>{await page.waitForTimeout(800);await expect.poll(async()=>(await snapshot())?.tween,{timeout:20000}).toBe(false);};
  37. const overlap=(a,b)=>b&&a.x<b.x+b.w&&a.x+a.w>b.x&&a.y<b.y+b.h&&a.y+a.h>b.y;
  38. try {
  39. for(const id of (process.env.TASK_IDS||'86,87,88,89').split(',')){
  40. await page.goto(baseUrl+'/teaching/virtual-training/preview/'+id);
  41. await expect.poll(async()=>!!(await snapshot())?.previewCamera,{timeout:120000}).toBe(true);
  42. const overlay=page.locator('.training-introduction'),canvas=page.locator('.training-canvas');
  43. await expect(canvas).toBeVisible();assert.equal(await canvas.count(),1);assert.equal(await page.locator('.introduction-background').count(),0);
  44. const initial=await snapshot();assert.equal(initial.sameRenderer,true);assert.equal(initial.controlsEnabled,false);
  45. await page.waitForTimeout(1500);const rotated=await snapshot();assert.ok(rotated.angle-initial.angle>.015&&rotated.angle-initial.angle<.2);
  46. assert.deepEqual(rotated.camera,initial.camera);assert.deepEqual(rotated.completed,[]);
  47. assert.equal(await overlay.locator('video').evaluate(v=>v.paused&&v.currentTime===0),true);
  48. await overlay.locator('[data-intro-scene-rotation]').click();const paused=await snapshot();await page.waitForTimeout(500);
  49. assert.equal((await snapshot()).angle,paused.angle);await page.screenshot({path:`${dir}/task-${id}-background.png`});
  50. await overlay.locator('[data-intro-scene-rotation]').click();
  51. if(process.env.CHECK_LAYOUT==='1'){
  52. await page.locator('[data-viewer-action="fullscreen"]').click();await page.waitForTimeout(700);
  53. await expect(overlay.locator('canvas')).toBeVisible();await page.screenshot({path:`${dir}/task-${id}-fullscreen.png`});
  54. await page.locator('[data-viewer-action="fullscreen"]').click();await page.waitForTimeout(700);
  55. await page.setViewportSize({width:1366,height:768});await page.waitForTimeout(700);await expect(overlay.locator('canvas')).toBeVisible();
  56. await page.setViewportSize(viewport);await page.waitForTimeout(700);
  57. }
  58. const count=await page.locator('[data-intro-page]').count();
  59. for(let i=1;i<count;i++){
  60. await page.locator(`[data-intro-page="${i}"]`).click();await expect(overlay.locator('canvas')).toBeVisible();
  61. const start=await snapshot();await page.waitForTimeout(400);assert.ok((await snapshot()).angle>start.angle);
  62. assert.equal(await overlay.locator('video').evaluate(v=>v.paused),true);
  63. }
  64. if(count>1)await page.screenshot({path:`${dir}/task-${id}-lecture.png`});
  65. await overlay.locator('[data-intro-media="toggle"]').click();await expect.poll(()=>overlay.locator('video').evaluate(v=>v.currentTime),{timeout:30000}).toBeGreaterThan(.1);
  66. await overlay.locator('[data-intro-command="skip"]').click();await settled();
  67. const operation=await snapshot(true);assert.equal(operation.canvasInPreview,false);assert.equal(operation.controlsEnabled,true);assert.deepEqual(operation.completed,[]);
  68. assert.equal(await page.locator('.training-server-video').evaluate(v=>v.paused),true);
  69. 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');}
  70. await page.screenshot({path:`${dir}/task-${id}-operation.png`});
  71. if(id==='87'&&process.env.CHECK_LAYOUT==='1')for(let toggle=0;toggle<2;toggle++){
  72. await page.locator('[data-viewer-action="tools"]').click();await settled();const layout=await snapshot(true);
  73. 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);}
  74. }
  75. await page.locator('[data-role="part-label"]').click();await settled();const focused=await snapshot();
  76. await page.locator('[data-intro-page="0"]').click();await expect(overlay.locator('canvas')).toBeVisible();await page.waitForTimeout(500);
  77. assert.deepEqual((await snapshot()).camera,focused.camera,'preview rotation cannot move the operation camera');
  78. await overlay.locator('[data-intro-command="skip"]').click();await settled();
  79. assert.deepEqual((await snapshot()).camera,focused.camera,'return restores focused operation camera');
  80. await page.locator('[data-action="reset-view"]').click();await settled();const reset=await snapshot(true);
  81. assert.ok(Math.abs(reset.objects[0].w-operation.objects[0].w)<2,'reset returns to the same overview');
  82. await page.screenshot({path:`${dir}/task-${id}-reset.png`});
  83. checks.push({task:id,introductionPages:count,rotationRadians:rotated.angle-initial.angle,initial,operation,reset});console.log('PASS 3D introduction',id,count,'pages');
  84. }
  85. assert.deepEqual(errors,[]);fs.writeFileSync(dir+'/result.json',JSON.stringify({pass:true,baseUrl,viewport,accountRole:'root',checks,errors},null,2));
  86. }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();}