Nelze vybrat více než 25 témat Téma musí začínat písmenem nebo číslem, může obsahovat pomlčky („-“) a může být dlouhé až 35 znaků.
 
 
 
 

90 řádky
6.1 KiB

  1. import fs from 'node:fs/promises';
  2. import path from 'node:path';
  3. import assert from 'node:assert/strict';
  4. import { chromium, expect } from '@playwright/test';
  5. import { login, call } from './role-permissions-live.mjs';
  6. // Capture actual published previews. This hook is injected only into this browser's
  7. // development-module response; no application code, task definition or score is changed.
  8. const studioHook = `
  9. globalThis.__bridgeCoverStudio = {
  10. prepare() {
  11. const r = activeTraining;
  12. r.stopTeacherSpeech(); r.teacherVoiceEnabled = false;
  13. if (r.targetMarker3D) r.targetMarker3D.visible = false;
  14. const bounds = object => {
  15. if (!object) return null;
  16. const box = new THREE.Box3().setFromObject(object, true);
  17. return { min:box.min.toArray(), max:box.max.toArray(), center:box.getCenter(new THREE.Vector3()).toArray(), size:box.getSize(new THREE.Vector3()).toArray() };
  18. };
  19. return { vehicle:bounds(r.viewerPrimaryObject), workshop:bounds(r.bridgeWorkshop),
  20. parts:[...r.parts].filter(([id])=>id.startsWith('bridge-') || id.startsWith('front-pump-')).map(([id,o])=>({id,name:o.name,bounds:bounds(o)})),
  21. camera:r.camera.position.toArray(), target:r.controls.target.toArray() };
  22. },
  23. camera(position, target) {
  24. const r = activeTraining;
  25. r.cameraAnimation = null;
  26. r.viewerCameraTween = null;
  27. r.inspection?.reset();
  28. if (r.inspection) r.inspection.outline.enabled = false;
  29. r.controls.minDistance = 0.05; r.controls.maxDistance = 200;
  30. r.camera.position.fromArray(position); r.controls.target.fromArray(target);
  31. r.camera.near=0.01; r.camera.updateProjectionMatrix(); r.controls.update();
  32. },
  33. stage(lastStep) {
  34. const r = activeTraining;
  35. // Published animation poses only: no session completion or API write.
  36. r.applyCompletedMechanismTransforms(lastStep,true);
  37. r.bridgeWorkshop?.traverse(o=>{if(o.isSprite)o.visible=false;});
  38. if(r.targetMarker3D)r.targetMarker3D.visible=false;
  39. },
  40. };`;
  41. const compositions = {
  42. 'TASK-VIRTUAL-005': {step:4,position:[-8.6,4.7,7.6],target:[0,1.05,0.1],subject:'整车侧面、辅助控制盒打开、互救开关切换'},
  43. 'TASK-VIRTUAL-006': {step:5,position:[6.4,6.2,13.5],target:[0,1.0,4.5],subject:'架桥车与供回油快速接头,已连接的供回油软管'},
  44. 'TASK-VIRTUAL-007': {step:-1,position:[8.2,4.6,6.4],target:[0,1.05,0.8],subject:'前泵换件场景整车外观,保留正常材质与完整环境'},
  45. 'TASK-VIRTUAL-008': {step:2,position:[-5.8,5.3,12.8],target:[0,1.02,4.8],subject:'大小腔平衡阀,拆开的阀体及油缸接口'},
  46. };
  47. const dir = path.resolve(process.env.REPORT_DIR || 'reports/bridge-task-covers-20260915');
  48. await fs.mkdir(dir, { recursive:true });
  49. const signed = await login('root');
  50. const result = await call('tran/v1/teaching/assignments?channel=VIRTUAL&assignmentKind=TRAINING&size=100',signed.accessToken);
  51. assert.equal(result.status,200);
  52. const tasks = result.data.records.filter(t=>/^TASK-VIRTUAL-00[5-8]$/.test(t.code)).sort((a,b)=>a.code.localeCompare(b.code));
  53. assert.equal(tasks.length,4);
  54. const browser = await chromium.launch({channel:'msedge',headless:true,args:['--enable-unsafe-swiftshader']});
  55. const page = await browser.newPage({viewport:{width:1200,height:1000},deviceScaleFactor:1});
  56. 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);
  57. await page.route(/\/src\/features\/virtual-training-legacy\/upstream\/views\/training\.js(?:\?|$)/,async route=>{
  58. const response=await route.fetch(); await route.fulfill({response,body:(await response.text())+studioHook});
  59. });
  60. const requestedCodes=new Set((process.env.TASK_CODES || '').split(',').filter(Boolean));
  61. const previousFrames=requestedCodes.size ? JSON.parse(await fs.readFile(path.join(dir,'frames.json'),'utf8').catch(()=>'[]')) : [];
  62. const frames=previousFrames.filter(f=>!requestedCodes.has(f.code));
  63. try {
  64. for(const task of tasks) {
  65. if(requestedCodes.size && !requestedCodes.has(task.code))continue;
  66. await page.goto(`http://127.0.0.1:6180/teaching/virtual-training/preview/${task.id}`);
  67. await expect(page.locator('[data-role="loading"]')).toHaveClass(/is-hidden/,{timeout:120000});
  68. await page.waitForFunction(()=>globalThis.__bridgeCoverStudio && globalThis.__trainingSceneDiagnostics?.()?.publishedContentMounted);
  69. await page.screenshot({path:path.join(dir,`${task.code}-workspace.png`)});
  70. const info=await page.evaluate(()=>globalThis.__bridgeCoverStudio.prepare());
  71. frames.push({code:task.code,name:task.name,taskId:task.id,contentVersionId:task.contentVersionId,...info});
  72. await page.addStyleTag({content:'.training-viewport {position:fixed!important;inset:0!important;width:1200px!important;height:1000px!important;z-index:2147483647!important;border:0!important;border-radius:0!important;} .training-viewport > :not(canvas) {display:none!important;} .training-canvas {width:100%!important;height:100%!important;}'});
  73. const newModels={
  74. 'TASK-VIRTUAL-006':{step:5,position:[-7,5.8,11],target:[1.2,1,1.1],subject:'新版架桥车、保障车与两车连接油管'},
  75. 'TASK-VIRTUAL-007':{step:-1,position:[-6.5,4,8],target:[0,1,1],subject:'新版架桥车及原位前泵换件部件'},
  76. 'TASK-VIRTUAL-008':{step:2,position:[-3.8,3.8,5],target:[-.05,1.3,.75],subject:'新版架桥车大小腔平衡阀与原位拆装区域'},
  77. };
  78. const composition=task.definitionSnapshot.runtime?.scriptVersion==='4'?newModels[task.code]:compositions[task.code];
  79. await page.evaluate(c=>{globalThis.__bridgeCoverStudio.stage(c.step);globalThis.__bridgeCoverStudio.camera(c.position,c.target);},composition);
  80. frames.at(-1).composition=composition;
  81. await page.waitForTimeout(600);
  82. await page.locator('canvas.training-canvas').screenshot({path:path.join(dir,`${task.code}-scene.jpg`),type:'jpeg',quality:88});
  83. console.log(`Captured ${task.code}`);
  84. }
  85. await fs.writeFile(path.join(dir,'frames.json'),JSON.stringify(frames.sort((a,b)=>a.code.localeCompare(b.code)),null,2));
  86. } finally { await browser.close(); }