Vous ne pouvez pas sélectionner plus de 25 sujets Les noms de sujets doivent commencer par une lettre ou un nombre, peuvent contenir des tirets ('-') et peuvent comporter jusqu'à 35 caractères.
 
 
 
 

115 lignes
11 KiB

  1. import fs from 'node:fs';
  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. const report=path.resolve('reports/animation-capabilities-20260912'),file=path.join(report,'fixture.json');
  7. const fixture=JSON.parse(fs.readFileSync(file,'utf8')),session=await login('root');
  8. const save=()=>fs.writeFileSync(file,JSON.stringify(fixture,null,2));
  9. const api=async(url,body,method)=>{const r=await call('tran/v1/'+url,session.accessToken,body,method);assert.equal(r.status,200,`${url}: ${r.message}`);return r.data;};
  10. const browser=await chromium.launch({channel:'msedge',headless:true,args:['--enable-unsafe-swiftshader']});
  11. const context=await browser.newContext({viewport:{width:1920,height:1080}});
  12. await context.addInitScript(s=>{sessionStorage.setItem('unreal-tran:web:access-token:v1',s.accessToken);sessionStorage.setItem('unreal-tran:web:refresh-token:v1',s.refreshToken)},session);
  13. const page=await context.newPage(),checks=[],errors=[];page.on('pageerror',e=>errors.push(e.message));
  14. try {
  15. await page.goto('http://127.0.0.1:6180/content/scenes');
  16. if(!fixture.animationSceneId){
  17. const content=await page.evaluate(async f=>{
  18. const {createEmptySceneDocument,createObjectDocument}=await import('/src/features/editors/scene/presets.ts');
  19. const scene=createEmptySceneDocument(),object=createObjectDocument('box');
  20. scene.name='动画能力验收场景-20260912';
  21. Object.assign(object,{id:'animation-truck',type:'model',name:'组合动作牛奶卡车',targetProjectId:f.modelId,targetVersionId:f.modelVersionId,assetCode:f.savedDocument.modelResource.assetCode,assetId:f.savedDocument.modelId});
  22. object.semantic.modelId=f.savedDocument.modelId;object.semantic.deviceId='ANIM-TRUCK';object.semantic.interactionId='animation-truck';object.semantic.operable=true;
  23. scene.objects=[object];scene.rendering.cameraPosition=[6,4,7];scene.rendering.cameraTarget=[0,1,0];
  24. return scene;
  25. },fixture);
  26. const created=await api('content/projects',{type:'SCENE',name:content.name,categoryCode:'WORKSHOP',content,dependencies:[{targetProjectId:fixture.modelId,targetVersionId:fixture.modelVersionId,relationType:'SCENE_MODEL',required:true}],assets:[]});
  27. fixture.animationSceneId=created.project.id;save();
  28. }
  29. let scene=await api(`content/projects/${fixture.animationSceneId}`);
  30. if(scene.project.status!=='PUBLISHED'&&!scene.currentVersion.content.objects[0].semantic.deviceId){
  31. scene.currentVersion.content.objects[0].semantic.deviceId='ANIM-TRUCK';
  32. scene=await api(`content/projects/${fixture.animationSceneId}`,{type:'SCENE',name:scene.project.name,categoryCode:'WORKSHOP',content:scene.currentVersion.content,version:scene.project.version,dependencies:scene.dependencies,assets:[]},'PUT');
  33. }
  34. if(scene.project.status!=='PUBLISHED')await api(`content/projects/${fixture.animationSceneId}/publish`,{version:scene.project.version});
  35. const publishedScene=await api(`content/projects/${fixture.animationSceneId}`);fixture.animationSceneVersionId=publishedScene.project.publishedVersionId;save();
  36. if(!fixture.animationTrainingId){
  37. const content=await page.evaluate(async({f,scene})=>{
  38. const {createTrainingProject}=await import('/src/features/guide-legacy/demo-src/training-project-io.js');
  39. const doc=createTrainingProject();doc.name='动画能力验收训练-20260912';doc.task.title=doc.name;
  40. const step=doc.steps[0];step.id='animation-step';step.code='animation-step';step.title='旋出、清洁与回路流向';step.targetId='animation-truck';step.targetName='组合动作牛奶卡车';step.duration=4;
  41. step.animationBinding={assetId:`animation-${f.modelId}@${f.modelVersionId}-work`,modelAssetId:`${f.modelId}@${f.modelVersionId}`,clipName:'旋出与清洁演示',sourceStart:0,finish:'hold',duration:4};
  42. doc.steps=[step,{...structuredClone(step),id:'reset-step',code:'reset-step',title:'播放后回到起点',animationBinding:{...step.animationBinding,clipName:'阀门复位演示',finish:'reset'}}];
  43. doc.steps.forEach(step=>{step.stepCode=step.code;step.actionCode='CHECK-'+step.code;});
  44. doc.scenario.sceneResource={id:`${f.animationSceneId}@${f.animationSceneVersionId}`,projectId:f.animationSceneId,versionId:f.animationSceneVersionId,name:scene.name,snapshot:scene};
  45. doc.scenario.visibleParts=['animation-truck'];doc.scenario.operableParts=['animation-truck'];
  46. doc.resources.models=[{id:`${f.modelId}@${f.modelVersionId}`,modelId:f.savedDocument.modelId,targetProjectId:f.modelId,targetVersionId:f.modelVersionId,timeline:f.savedDocument.timeline}];
  47. return doc;
  48. },{f:fixture,scene:publishedScene.currentVersion.content});
  49. const created=await api('content/projects',{type:'TRAINING',name:content.name,categoryCode:'MAINTENANCE',trainingMode:'VIRTUAL',content,dependencies:[{targetProjectId:fixture.animationSceneId,targetVersionId:fixture.animationSceneVersionId,relationType:'TRAINING_SCENE',required:true}],assets:[]});
  50. fixture.animationTrainingId=created.project.id;save();
  51. }
  52. let training=await api(`content/projects/${fixture.animationTrainingId}`);
  53. if(training.project.status!=='PUBLISHED'&&training.currentVersion.content.steps[0].actionCode==='CHECK'){
  54. training.currentVersion.content.steps.forEach(step=>{step.stepCode=step.code;step.actionCode='CHECK-'+step.code;});
  55. training=await api(`content/projects/${fixture.animationTrainingId}`,{type:'TRAINING',name:training.project.name,categoryCode:'MAINTENANCE',trainingMode:'VIRTUAL',content:training.currentVersion.content,version:training.project.version,dependencies:training.dependencies,assets:[]},'PUT');
  56. }
  57. if(training.project.status!=='PUBLISHED')await api(`content/projects/${fixture.animationTrainingId}/publish`,{version:training.project.version});
  58. const published=await api(`content/projects/${fixture.animationTrainingId}`);fixture.animationTrainingVersionId=published.project.publishedVersionId;save();
  59. if(!fixture.animationAssignmentId){
  60. const a=await api('teaching/assignments',{code:'ANIM-CAP-'+Date.now(),name:'动画能力验收任务-20260912',description:'开发环境动画播放验证',channel:'VIRTUAL',assignmentKind:'TRAINING',executionMode:'DEMO',audienceType:'COMMON',collaborationMode:'INDIVIDUAL',digitalHumanAllowed:false,contentProjectId:fixture.animationTrainingId,contentVersionId:fixture.animationTrainingVersionId,members:[]});
  61. fixture.animationAssignmentId=a.id;save();
  62. }
  63. const assignment=await api(`teaching/assignments/${fixture.animationAssignmentId}`);
  64. fixture.animationAssignmentContentVersionId=assignment.contentVersionId;save();
  65. const object=assignment.definitionSnapshot.teachingScene.objects.find(o=>o.targetProjectId===fixture.modelId);
  66. assert.equal(object.modelTimeline.clips.length,2);assert.equal(object.modelTimeline.tracks.length,4);assert.ok(Object.keys(object.modelOverrides).length>0);
  67. assert.equal(object.targetVersionId,fixture.modelVersionId);
  68. checks.push({name:'真实教学任务固定模型版本、动作、材质、枢轴和流向配置',pass:true,modelVersionId:object.targetVersionId,assignmentId:assignment.id});
  69. await page.goto(`http://127.0.0.1:6180/teaching/virtual-training/preview/${assignment.id}`);
  70. await expect(page.locator('.teaching-three-scene canvas')).toBeVisible({timeout:60000});
  71. await page.getByRole('button',{name:'适应',exact:true}).click();
  72. await page.waitForTimeout(6500);
  73. // Inspect the real component's runtime, not a second test renderer.
  74. const inspect=()=>page.evaluate(()=>{
  75. const host=document.querySelector('.teaching-three-scene');let instance=host?.__vueParentComponent;
  76. while(instance&&!instance.type.__name?.includes('TeachingRunScene'))instance=instance.parent;
  77. const state=instance?.devtoolsRawSetupState;
  78. if(!state)throw new Error('Teaching scene setup unavailable');
  79. const entry=state.timelineRuntimes[0],runtime=entry?.runtime;
  80. const wheel=runtime?.bindings.find(b=>b.track.id==='move')?.object;
  81. const material=runtime?.materials.values().next().value;
  82. const mesh=runtime?.materials.keys().next().value;
  83. return {status:state.sceneStatus?.value || state.sceneStatus,time:runtime?.time,bindings:runtime?.bindings.length,unresolved:runtime?.unresolved,
  84. wheel:wheel?.position.toArray(),base:wheel&&runtime.bases.get(wheel).position,roughness:material?.clones[0].roughness,
  85. actualRoughness:mesh?.material.roughness,flowVisible:runtime?.flows[0]?.dots.some(dot=>dot.visible),step:state.props.currentStepCode};
  86. });
  87. const state=await inspect();assert.doesNotMatch(String(state.status),/失败/);
  88. assert.equal(state.bindings,4);assert.equal(state.unresolved,0);assert.equal(state.time,4);assert.equal(state.roughness,.2);assert.equal(state.actualRoughness,.2);assert.equal(state.flowVisible,true);assert.notDeepEqual(state.wheel,state.base);
  89. await page.screenshot({path:path.join(report,'09-teaching-animation.png')});
  90. checks.push({name:'正式教学预览实际播放指定片段并保持末帧,四条轨道全部绑定',pass:true,detail:state});
  91. await page.getByRole('button',{name:'引导完成',exact:true}).click();
  92. await page.getByRole('button',{name:'下一步',exact:true}).click();
  93. await page.waitForTimeout(6500);
  94. const reset=await inspect();assert.equal(reset.step,'reset-step');assert.equal(reset.time,0);assert.equal(reset.roughness,.95);assert.equal(reset.flowVisible,false);assert.deepEqual(reset.wheel,reset.base);
  95. await page.screenshot({path:path.join(report,'10-teaching-reset.png')});
  96. checks.push({name:'切换步骤后播放复位片段,结束恢复起点并关闭流向',pass:true,detail:reset});
  97. const native=await page.evaluate(async()=>{
  98. let instance=document.querySelector('.teaching-three-scene').__vueParentComponent;
  99. while(!instance.type.__name?.includes('TeachingRunScene'))instance=instance.parent;
  100. const s=instance.devtoolsRawSetupState,entry=s.nativePlayers[0];
  101. if(!entry?.clips.length)throw new Error('Fixture native GLB clip missing');
  102. const clip=entry.clips[0],step=s.definitionSteps().find(step=>s.stepCodeOf(step)===s.props.currentStepCode);
  103. const binding=step.animationBinding,previous=binding.clipName;
  104. binding.clipName=clip.name;s.syncStepTimelines();
  105. await new Promise(resolve=>setTimeout(resolve,900));
  106. const time=entry.mixer.clipAction(clip).time;
  107. binding.clipName=previous;s.syncStepTimelines();
  108. return {name:clip.name,time};
  109. });
  110. assert.ok(native.time>0);checks.push({name:'同一教学视口原生 GLB 动画与自编片段切换兼容(浏览器运行时检查)',pass:true,detail:native});
  111. fs.writeFileSync(path.join(report,'teaching-inspection.json'),JSON.stringify(state,null,2));
  112. assert.equal(errors.length,0,errors.join('\n'));
  113. }catch(error){checks.push({name:'教学动画端到端',pass:false,error:error.message});process.exitCode=1;console.error(error.message);await page.screenshot({path:path.join(report,'teaching-failure.png')}).catch(()=>{});}
  114. finally{fs.writeFileSync(path.join(report,'teaching-checks.json'),JSON.stringify({checks,errors},null,2));await browser.close();}