|
- 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';
-
- // Capture actual published previews. This hook is injected only into this browser's
- // development-module response; no application code, task definition or score is changed.
- const studioHook = `
- globalThis.__bridgeCoverStudio = {
- prepare() {
- const r = activeTraining;
- r.stopTeacherSpeech(); r.teacherVoiceEnabled = false;
- if (r.targetMarker3D) r.targetMarker3D.visible = false;
- const bounds = object => {
- if (!object) return null;
- const box = new THREE.Box3().setFromObject(object, true);
- return { min:box.min.toArray(), max:box.max.toArray(), center:box.getCenter(new THREE.Vector3()).toArray(), size:box.getSize(new THREE.Vector3()).toArray() };
- };
- return { vehicle:bounds(r.viewerPrimaryObject), workshop:bounds(r.bridgeWorkshop),
- parts:[...r.parts].filter(([id])=>id.startsWith('bridge-') || id.startsWith('front-pump-')).map(([id,o])=>({id,name:o.name,bounds:bounds(o)})),
- camera:r.camera.position.toArray(), target:r.controls.target.toArray() };
- },
- camera(position, target) {
- const r = activeTraining;
- r.cameraAnimation = null;
- r.viewerCameraTween = null;
- r.inspection?.reset();
- if (r.inspection) r.inspection.outline.enabled = false;
- r.controls.minDistance = 0.05; r.controls.maxDistance = 200;
- r.camera.position.fromArray(position); r.controls.target.fromArray(target);
- r.camera.near=0.01; r.camera.updateProjectionMatrix(); r.controls.update();
- },
- stage(lastStep) {
- const r = activeTraining;
- // Published animation poses only: no session completion or API write.
- r.applyCompletedMechanismTransforms(lastStep,true);
- r.bridgeWorkshop?.traverse(o=>{if(o.isSprite)o.visible=false;});
- if(r.targetMarker3D)r.targetMarker3D.visible=false;
- },
- };`;
-
- const compositions = {
- 'TASK-VIRTUAL-005': {step:4,position:[-8.6,4.7,7.6],target:[0,1.05,0.1],subject:'整车侧面、辅助控制盒打开、互救开关切换'},
- 'TASK-VIRTUAL-006': {step:5,position:[6.4,6.2,13.5],target:[0,1.0,4.5],subject:'架桥车与供回油快速接头,已连接的供回油软管'},
- 'TASK-VIRTUAL-007': {step:-1,position:[8.2,4.6,6.4],target:[0,1.05,0.8],subject:'前泵换件场景整车外观,保留正常材质与完整环境'},
- 'TASK-VIRTUAL-008': {step:2,position:[-5.8,5.3,12.8],target:[0,1.02,4.8],subject:'大小腔平衡阀,拆开的阀体及油缸接口'},
- };
-
- const dir = path.resolve(process.env.REPORT_DIR || 'reports/bridge-task-covers-20260915');
- await fs.mkdir(dir, { recursive:true });
- const signed = await login('root');
- const result = await call('tran/v1/teaching/assignments?channel=VIRTUAL&assignmentKind=TRAINING&size=100',signed.accessToken);
- assert.equal(result.status,200);
- const tasks = result.data.records.filter(t=>/^TASK-VIRTUAL-00[5-8]$/.test(t.code)).sort((a,b)=>a.code.localeCompare(b.code));
- assert.equal(tasks.length,4);
- const browser = await chromium.launch({channel:'msedge',headless:true,args:['--enable-unsafe-swiftshader']});
- const page = await browser.newPage({viewport:{width:1200,height:1000},deviceScaleFactor:1});
- 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())+studioHook});
- });
- const requestedCodes=new Set((process.env.TASK_CODES || '').split(',').filter(Boolean));
- const previousFrames=requestedCodes.size ? JSON.parse(await fs.readFile(path.join(dir,'frames.json'),'utf8').catch(()=>'[]')) : [];
- const frames=previousFrames.filter(f=>!requestedCodes.has(f.code));
- try {
- for(const task of tasks) {
- if(requestedCodes.size && !requestedCodes.has(task.code))continue;
- await page.goto(`http://127.0.0.1:6180/teaching/virtual-training/preview/${task.id}`);
- await expect(page.locator('[data-role="loading"]')).toHaveClass(/is-hidden/,{timeout:120000});
- await page.waitForFunction(()=>globalThis.__bridgeCoverStudio && globalThis.__trainingSceneDiagnostics?.()?.publishedContentMounted);
- await page.screenshot({path:path.join(dir,`${task.code}-workspace.png`)});
- const info=await page.evaluate(()=>globalThis.__bridgeCoverStudio.prepare());
- frames.push({code:task.code,name:task.name,taskId:task.id,contentVersionId:task.contentVersionId,...info});
- 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;}'});
- const newModels={
- 'TASK-VIRTUAL-006':{step:5,position:[-7,5.8,11],target:[1.2,1,1.1],subject:'新版架桥车、保障车与两车连接油管'},
- 'TASK-VIRTUAL-007':{step:-1,position:[-6.5,4,8],target:[0,1,1],subject:'新版架桥车及原位前泵换件部件'},
- 'TASK-VIRTUAL-008':{step:2,position:[-3.8,3.8,5],target:[-.05,1.3,.75],subject:'新版架桥车大小腔平衡阀与原位拆装区域'},
- };
- const composition=task.definitionSnapshot.runtime?.scriptVersion==='4'?newModels[task.code]:compositions[task.code];
- await page.evaluate(c=>{globalThis.__bridgeCoverStudio.stage(c.step);globalThis.__bridgeCoverStudio.camera(c.position,c.target);},composition);
- frames.at(-1).composition=composition;
- await page.waitForTimeout(600);
- await page.locator('canvas.training-canvas').screenshot({path:path.join(dir,`${task.code}-scene.jpg`),type:'jpeg',quality:88});
- console.log(`Captured ${task.code}`);
- }
- await fs.writeFile(path.join(dir,'frames.json'),JSON.stringify(frames.sort((a,b)=>a.code.localeCompare(b.code)),null,2));
- } finally { await browser.close(); }
|