25개 이상의 토픽을 선택하실 수 없습니다. Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 

115 lines
11 KiB

  1. import fs from 'node:fs';
  2. import path from 'node:path';
  3. import crypto from 'node:crypto';
  4. import assert from 'node:assert/strict';
  5. import {chromium,expect} from '@playwright/test';
  6. import {login,call} from './role-permissions-live.mjs';
  7. const report=path.resolve('reports/virtual-html-workspace-20260912');fs.mkdirSync(report,{recursive:true});
  8. const checks=[],errors=[],admin=await login('root');
  9. const api=async(endpoint,session=admin,body,method)=>{const r=await call(endpoint,session?.accessToken,body,method);assert.equal(r.status,200,`${endpoint}: ${r.message}`);return r.data;};
  10. const checked=(name,details)=>{checks.push({name,pass:true,details});console.log('PASS',name);};
  11. const browser=await chromium.launch({channel:'msedge',headless:true,args:['--enable-unsafe-swiftshader']});
  12. const createContext=async(session,width=1920)=>{const ctx=await browser.newContext({viewport:{width,height:1080}});await ctx.addInitScript(s=>{
  13. sessionStorage.setItem('unreal-tran:web:access-token:v1',s.accessToken);sessionStorage.setItem('unreal-tran:web:refresh-token:v1',s.refreshToken);
  14. localStorage.setItem('unreal-tran:web:preferences:v1',JSON.stringify({themeMode:'military'}));
  15. },session);return ctx;};
  16. const context=await createContext(admin),page=await context.newPage();page.on('pageerror',e=>errors.push(e.message));
  17. const diagnostic=p=>p.evaluate(()=>globalThis.__trainingSceneDiagnostics?.());
  18. const open=async(p,url)=>{await p.goto('http://127.0.0.1:6180'+url);await p.locator('.training-shell').waitFor({timeout:120000});await expect.poll(async()=>Boolean((await diagnostic(p))?.publishedContentMounted),{timeout:90000}).toBe(true);};
  19. const screenshot=async(p,name)=>{await p.screenshot({path:path.join(report,name+'.png'),fullPage:true});};
  20. let studentPage;
  21. try {
  22. await open(page,'/teaching/virtual-training/preview/54');
  23. await expect(page.locator('[data-role="model-commandbar"]')).toBeVisible();
  24. for(const action of ['toggle-structure-tree','focus-selected-part','viewer-reset-view','toggle-part-labels','toggle-issue-highlight','toggle-hide-others','toggle-explosion'])await expect(page.locator(`[data-role="model-commandbar"] [data-action="${action}"]`)).toBeVisible();
  25. await page.locator('[data-role="model-commandbar"] [data-action="toggle-structure-tree"]').click();
  26. const tree=page.locator('[data-role="structure-tree"]');await expect(tree).toHaveClass(/is-open/);
  27. assert.ok((await tree.locator('[data-action="select-structure-part"]').count())>20);
  28. await tree.locator('[data-action="select-structure-part"]').first().click();
  29. await page.locator('[data-action="focus-selected-part"]').click();
  30. await page.locator('[data-role="model-commandbar"] [data-action="toggle-structure-tree"]').click();
  31. await page.locator('[data-action="toggle-issue-highlight"]').click();
  32. await expect.poll(async()=>(await diagnostic(page)).framing.keptTargets.length).toBeGreaterThan(0);
  33. await page.locator('[data-action="toggle-hide-others"]').click();
  34. assert.equal((await diagnostic(page)).framing.hideOthers,true);
  35. await page.locator('[data-action="toggle-hide-others"]').click();
  36. await page.locator('[data-action="toggle-part-labels"]').click();
  37. assert.equal((await diagnostic(page)).framing.labelsVisible,true);
  38. await screenshot(page,'01-bridge-tools');
  39. await page.locator('[data-action="toggle-explosion"]').click();
  40. await screenshot(page,'02-bridge-explosion');
  41. await page.locator('[data-action="viewer-reset-view"]').click();
  42. await expect(page.locator('[data-role="teacher-image"]')).toBeVisible();
  43. const bridge=await diagnostic(page);assert.equal(bridge.proceduralMachineVisible,false);assert.ok(bridge.server.registeredTargets.includes(bridge.framing.targetOnScreen.targetId));
  44. checked('桥梁:完整结构树、聚焦、高亮、隐藏、标签、爆炸、复位和数字教员',bridge.framing);
  45. const suffix=Date.now().toString(36),password='Vh!'+crypto.randomBytes(8).toString('hex');
  46. const roles=await api('auth/v1/roles?size=100');const role=(roles.records||roles).find(r=>r.code==='student');
  47. const user=await api('auth/v1/users',admin,{username:'vh_'+suffix,displayName:'HTML验收-'+suffix,departmentId:'5',roleIds:[role.id],defaultRoleId:role.id,enabled:true,password,remark:'JS/HTML正式工作区隔离测试账号'});
  48. const learner=await api('auth/v1/auth/login',null,{userId:(user.user||user).id,roleCode:'student',password,rememberMe:false});
  49. const source=await api('tran/v1/content/projects/419');
  50. source.currentVersion.content.steps.forEach((step,index)=>{
  51. step.mode=index===0?'measure':'remove';step.interaction=index===0?'rule-check':'spatial-drag';step.targetNodeId=step.targetId;
  52. step.destination=index===0?'压力测量检查':'编号存放区';
  53. step.action={...step.action,kind:step.interaction,mode:step.mode,targetId:step.targetId,requiresPlacement:index===1,tolerance:.55};
  54. });
  55. const content=await api('tran/v1/content/projects',admin,{type:'TRAINING',name:'HTML闭环验收-'+suffix,categoryCode:'MAINTENANCE',trainingMode:'VIRTUAL',content:source.currentVersion.content,dependencies:source.currentVersion.dependencies,assets:[]});
  56. await api(`tran/v1/content/projects/${content.project.id}/publish`,admin,{version:content.project.version});
  57. const reference=(await api(`tran/v1/content/projects/${content.project.id}`)).project;
  58. const task=await api('tran/v1/teaching/assignments',admin,{code:'HTML-'+suffix,name:'HTML工作区-卡车动画闭环-'+suffix,description:'开发环境真实操作、断点恢复和动画验收',channel:'VIRTUAL',assignmentKind:'TRAINING',executionMode:'PRACTICE',audienceType:'ASSIGNED',collaborationMode:'INDIVIDUAL',digitalHumanAllowed:true,contentProjectId:reference.id,contentVersionId:reference.publishedVersionId,members:[{userId:(user.user||user).id,memberType:'LEARNER',teamCode:'NEUTRAL',positionCode:'LEARNER',sortOrder:0}]});
  59. const published=await api(`tran/v1/teaching/assignments/${task.id}/publish`,admin,{version:task.version});
  60. const run=await api(`tran/v1/teaching/assignments/${task.id}/accept`,learner,{assignmentVersion:published.version,teamCode:'NEUTRAL',positionCode:'LEARNER'});
  61. fs.writeFileSync(path.join(report,'fixture.json'),JSON.stringify({userId:(user.user||user).id,assignmentId:task.id,runId:run.id},null,2));
  62. const ctx=await createContext(learner);studentPage=await ctx.newPage();studentPage.on('pageerror',e=>errors.push(e.message));
  63. await open(studentPage,`/teaching/virtual-training/runs/${run.id}`);
  64. await studentPage.waitForTimeout(5200);
  65. const animation=(await diagnostic(studentPage)).server.animations.find(a=>a.bindings);
  66. assert.equal(animation.bindings,4);assert.equal(animation.unresolved,0);assert.equal(animation.time,4);assert.equal(animation.flowVisible,true);
  67. checked('卡车:正式工作区播放四种已编排动画轨道并保持末帧',animation);
  68. const prepare=async()=>{
  69. const panel=studentPage.locator('.training-server-preparation');if(!(await panel.evaluate(p=>p.open)))await panel.locator('summary').click();
  70. await panel.locator('[data-tool="pressure-gauge"]').check();for(const checkbox of await panel.locator('[data-safety]').all())await checkbox.check();
  71. await panel.locator('textarea').fill('测试测量:压力为零,检查已完成。');
  72. await panel.locator('[data-save-preparation]').click();await expect(panel.locator('[data-save-status]')).toHaveText('已保存');
  73. };
  74. await prepare();
  75. await studentPage.reload();await expect.poll(async()=>Boolean((await diagnostic(studentPage))?.publishedContentMounted),{timeout:90000}).toBe(true);
  76. await studentPage.locator('.training-server-preparation summary').click();
  77. await expect(studentPage.locator('[data-tool="pressure-gauge"]')).toBeChecked();await expect(studentPage.locator('[data-measurement]')).toHaveValue('测试测量:压力为零,检查已完成。');
  78. await screenshot(studentPage,'03-learner-preparation-restored');
  79. checked('学员:工具、安全、测量记录通过真实接口保存并刷新恢复',{runId:run.id});
  80. await studentPage.locator('.training-server-preparation summary').click();
  81. const clickTarget=async()=>{
  82. await studentPage.locator('[data-action="toggle-issue-highlight"]').click();
  83. await studentPage.waitForTimeout(500);
  84. const d=await diagnostic(studentPage),rect=await studentPage.locator('canvas.training-canvas').boundingBox();
  85. assert.ok(d.framing.targetOnScreen?.insideCanvas,'Target must be on screen');
  86. await studentPage.mouse.click(rect.x+d.framing.targetOnScreen.localX,rect.y+d.framing.targetOnScreen.localY+38);
  87. };
  88. // Lose one progress response before it reaches the API; ensure no local advance.
  89. let rejected=false;
  90. await studentPage.route('**/api/tran/v1/teaching/runs/*/progress',async route=>{if(!rejected){rejected=true;await route.abort('failed');}else await route.continue();});
  91. await clickTarget();await studentPage.waitForTimeout(1500);
  92. assert.equal(rejected,true,'An actual progress request must have been attempted');
  93. assert.equal((await api(`tran/v1/teaching/runs/${run.id}`,learner)).progressPercent,0);
  94. assert.equal((await diagnostic(studentPage)).server.completed.length,0);
  95. checked('进度请求失败时不在本地提前完成步骤');
  96. await clickTarget();
  97. await expect.poll(async()=>(await api(`tran/v1/teaching/runs/${run.id}`,learner)).progressPercent,{timeout:20000}).toBe(50);
  98. await expect(studentPage.locator('[data-action="next"]')).toBeEnabled();
  99. await studentPage.locator('[data-action="next"]').click();await studentPage.waitForTimeout(5200);
  100. const reset=(await diagnostic(studentPage)).server.animations.find(a=>a.bindings);assert.equal(reset.time,0);assert.equal(reset.flowVisible,false);
  101. checked('步骤完成服务器确认后推进,下一步动画按发布片段复位',reset);
  102. await prepare();await studentPage.locator('.training-server-preparation summary').click();
  103. const dragState=await diagnostic(studentPage),canvasRect=await studentPage.locator('canvas.training-canvas').boundingBox(),drop=await studentPage.locator('[data-role="dropzone"]').boundingBox();
  104. await studentPage.mouse.move(canvasRect.x+dragState.framing.targetOnScreen.localX,canvasRect.y+dragState.framing.targetOnScreen.localY+38);
  105. await studentPage.mouse.down();await studentPage.mouse.move(drop.x+drop.width/2,drop.y+drop.height/2,{steps:25});await studentPage.mouse.up();
  106. await expect.poll(async()=>(await api(`tran/v1/teaching/runs/${run.id}`,learner)).progressPercent,{timeout:20000}).toBe(100);
  107. await studentPage.locator('[data-action="next"]').click();
  108. await expect.poll(async()=>(await api(`tran/v1/teaching/runs/${run.id}`,learner)).status,{timeout:20000}).toBe('SUBMITTED');
  109. await screenshot(studentPage,'04-submitted');checked('真实两步操作完成、服务端判定、提交结果闭环',{runId:run.id});
  110. await open(page,`/teaching/virtual-training/runs/${run.id}`);assert.equal((await diagnostic(page)).server.readOnly,true);await screenshot(page,'05-observer');
  111. checked('维护账号观察他人运行时只读,不修改学员进度');
  112. assert.equal(errors.length,0,errors.join('\n'));
  113. } catch(error) {checks.push({name:'HTML工作区端到端',pass:false,error:error.message});console.error(error.message);process.exitCode=1;await screenshot(studentPage||page,'failure').catch(()=>{});}
  114. finally {fs.writeFileSync(path.join(report,'checks.json'),JSON.stringify({checks,errors},null,2));await browser.close();}