Du kannst nicht mehr als 25 Themen auswählen Themen müssen entweder mit einem Buchstaben oder einer Ziffer beginnen. Sie können Bindestriche („-“) enthalten und bis zu 35 Zeichen lang sein.
 
 
 
 

99 Zeilen
8.9 KiB

  1. import {chromium,expect} from '@playwright/test';
  2. import assert from 'node:assert/strict';
  3. import crypto from 'node:crypto';
  4. import fs from 'node:fs';
  5. import {login,call} from './role-permissions-live.mjs';
  6. import {scripts,scriptDefinition} from '../../unreal_tran_web/src/features/virtual-training-scripts/definition.js';
  7. import {verifyVisualEffects} from './bridge-model-match-assertions.mjs';
  8. const dir='reports/bridge-3d-only-20260914';fs.mkdirSync(dir,{recursive:true});
  9. const root=await login('root'),checks=[],runs=[],errors=[];
  10. const api=async(path,data,method)=>{const r=await call(path,root.accessToken,data,method);assert.equal(r.status,200,`${path}: ${r.message}`);return r.data;};
  11. const listed=await api('tran/v1/teaching/assignments?channel=VIRTUAL&assignmentKind=TRAINING&size=100');
  12. assert.deepEqual(listed.records.map(t=>t.code).sort(),scripts.map(s=>s.code).sort());
  13. const tasks=scripts.map(script=>listed.records.find(t=>t.code===script.code));
  14. for(const [i,task] of tasks.entries()){
  15. assert.equal(task.definitionSnapshot.runtime.scriptVersion,scripts[i].version);
  16. assert.equal(task.executionMode,'PRACTICE');assert.equal(task.status,'PUBLISHED');
  17. assert.deepEqual(task.definitionSnapshot.steps.map(s=>s.action.mechanism.steps),scriptDefinition(scripts[i]).steps.map(s=>s.action.mechanism.steps));
  18. }
  19. checks.push('正式列表仅四条原任务,005 v2 三维 / 006–008 v3 三维,均为已发布普通训练');
  20. const invalid=scriptDefinition(scripts[1]);invalid.runtime.scriptVersion='2';
  21. const rejected=await call('tran/v1/content/projects',root.accessToken,{type:'TRAINING',name:'拒绝文字版验收',categoryCode:'MAINTENANCE',trainingMode:'VIRTUAL',content:invalid,dependencies:[],assets:[]});
  22. assert.equal(rejected.status,400,'Backend must reject obsolete text version');checks.push('真实内容接口拒绝旧文字版本,返回 400');
  23. const browser=await chromium.launch({channel:'msedge',headless:true,args:['--enable-unsafe-swiftshader']});
  24. let userId,page;
  25. const ready=async()=>{await expect(page.locator('[data-role="loading"]')).toHaveClass(/is-hidden/,{timeout:120000});await page.waitForTimeout(1500);await expect(page.locator('.bridge-procedure')).toHaveCount(0);};
  26. const diag=()=>page.evaluate(()=>globalThis.__trainingSceneDiagnostics());
  27. const saveRuns=()=>fs.writeFileSync(`${dir}/run-fixtures.json`,JSON.stringify({userId,runs},null,2));
  28. try{
  29. page=await browser.newPage({viewport:{width:1600,height:1000}});
  30. await page.addInitScript(s=>sessionStorage.setItem('unreal-tran:web:access-token:v1',s.accessToken),root);
  31. page.on('pageerror',e=>errors.push(e.message));
  32. await page.goto('http://127.0.0.1:6180/teaching/virtual-training');
  33. await expect(page.getByPlaceholder('请输入任务名称或编码')).toBeVisible();
  34. await page.screenshot({path:`${dir}/task-list.png`});
  35. for(const task of tasks){
  36. await page.goto(`http://192.168.31.168:6180/teaching/virtual-training/preview/${task.id}`);await ready();
  37. const bar=page.locator('[data-role="model-commandbar"]');
  38. 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(bar.locator(`[data-action="${action}"]`)).toBeVisible();
  39. await page.screenshot({path:`${dir}/${task.code}-original-preview.png`});
  40. checks.push(`${task.code} 原任务 ${task.id} 局域网预览加载三维模型与完整工具栏`);
  41. }
  42. await page.close();
  43. const roles=await api('auth/v1/roles/all'),role=(roles.records||roles).find(r=>r.code==='student');
  44. const password='Br!'+crypto.randomBytes(8).toString('hex');
  45. const created=await api('auth/v1/users',{username:'bridge_final_'+Date.now().toString(36),password,displayName:'桥梁三维迁移验收',departmentId:'5',roleIds:[role.id],defaultRoleId:role.id,enabled:true,remark:'临时验收账号'});
  46. userId=(created.user||created).id;saveRuns();
  47. const signed=await call('auth/v1/auth/login',null,{userId,password,roleCode:'student',rememberMe:false});assert.equal(signed.status,200);
  48. const learner=async(path,data)=>{const r=await call('tran/v1/'+path,signed.data.accessToken,data);assert.equal(r.status,200,`${path}: ${r.message}`);return r.data;};
  49. page=await browser.newPage({viewport:{width:1600,height:1000}});
  50. await page.addInitScript(s=>sessionStorage.setItem('unreal-tran:web:access-token:v1',s.accessToken),signed.data);
  51. page.on('pageerror',e=>errors.push(e.message));
  52. for(const task of tasks){
  53. let run=await learner(`teaching/assignments/${task.id}/accept`,{assignmentVersion:task.version,teamCode:'NEUTRAL',positionCode:'LEARNER'});
  54. runs.push({taskId:task.id,runId:run.id,code:task.code});saveRuns();
  55. await page.goto(`http://127.0.0.1:6180/teaching/virtual-training/runs/${run.id}`);await ready();
  56. const workshop=task.code!=='TASK-VIRTUAL-005';
  57. for(let i=0;i<task.definitionSnapshot.steps.length;i++){
  58. const step=task.definitionSnapshot.steps[i],ops=step.action.mechanism.steps;
  59. await expect(page.locator('[data-role="step-count"]')).toHaveText(`${i+1} / ${task.definitionSnapshot.steps.length}`);
  60. if(step.mode==='measure'){
  61. await page.locator('[data-open-preparation]').click();await page.locator('[data-measurement]').fill('按安装段连接管路;原稿矛盾已记录,交教员核对。');await page.locator('[data-close-preparation]').click();
  62. }
  63. for(let k=0;k<ops.length;k++){
  64. await expect(page.locator('[data-action="next"]')).toBeDisabled();
  65. if(workshop)await expect.poll(async()=>(await diag()).bridgeWorkshop.operation.targetId).toBe(ops[k].targetId);
  66. if(i===0&&k===ops.length-1&&task.code==='TASK-VIRTUAL-006'){
  67. await page.route(`**/teaching/runs/${run.id}/progress`,r=>r.fulfill({status:503,contentType:'application/json',body:JSON.stringify({code:'SERVICE_UNAVAILABLE',message:'验收模拟保存失败'})}),{times:1});
  68. const response=page.waitForResponse(r=>r.url().includes(`/runs/${run.id}/progress`));await page.locator('[data-role="part-label"]').click();assert.equal((await response).status(),503);
  69. await expect.poll(async()=>(await diag()).bridgeWorkshop.pending).toBe(false);assert.equal((await learner(`teaching/runs/${run.id}`)).progressPercent,0);
  70. await expect(page.locator('[data-action="next"]')).toBeDisabled();checks.push('正式 006 保存失败不推进,重试后才保存');
  71. }
  72. if(k===ops.length-1){const response=page.waitForResponse(r=>r.url().includes(`/runs/${run.id}/progress`));await page.locator('[data-role="part-label"]').click();assert.equal((await response).status(),200);}
  73. else {await page.locator('[data-role="part-label"]').click();if(workshop)await expect.poll(async()=>(await diag()).bridgeWorkshop.operationIndex).toBe(k+1);else await page.waitForTimeout(1200);}
  74. if(workshop)await verifyVisualEffects(page,ops[k]);
  75. }
  76. await expect(page.locator('[data-action="next"]')).toBeEnabled();
  77. assert.equal((await learner(`teaching/runs/${run.id}/steps`)).filter(s=>s.statusCode==='COMPLETED').length,i+1);
  78. if(i===3){
  79. const before=workshop?(await diag()).bridgeWorkshop.targets:null;
  80. await page.reload();await ready();
  81. if(before){const after=(await diag()).bridgeWorkshop.targets;for(const b of before){const a=after.find(t=>t.id===b.id);assert.equal(a.visible,b.visible);assert.ok(a.position.every((v,k)=>Math.abs(v-b.position[k])<.001),`Restore ${b.id}`);}}
  82. checks.push(`${task.code} 刷新恢复服务端步骤及拆装状态`);
  83. }else if(i<task.definitionSnapshot.steps.length-1)await page.locator('[data-action="next"]').click();
  84. console.log(task.code,i+1,'saved');
  85. }
  86. await page.locator('[data-action="next"]').click();await expect.poll(async()=>(await learner(`teaching/runs/${run.id}`)).status).toBe('SUBMITTED');
  87. run=await learner(`teaching/runs/${run.id}`);assert.equal(run.score,null);
  88. run=await api(`tran/v1/teaching/runs/${run.id}/review`,{version:run.version,commandId:crypto.randomUUID(),score:85,passed:true,feedback:'临时自动化验收评分',rubric:{testFixture:true}});
  89. assert.equal((await learner(`teaching/runs/${run.id}`)).score,85);
  90. const records=await api(`tran/v1/teaching/analytics/records?userId=${userId}&channel=VIRTUAL&assignmentKind=TRAINING&size=100`);
  91. assert.ok(records.records.some(r=>String(r.runId||r.id)===run.id));
  92. await page.reload();await ready();await expect(page.locator('.virtual-server-status')).toContainText('85');await page.screenshot({path:`${dir}/${task.code}-reviewed.png`});
  93. checks.push(`${task.code} ${task.definitionSnapshot.steps.length} 步完成、提交、评定 85 分与统计通过`);
  94. }
  95. assert.deepEqual(errors,[]);fs.writeFileSync(`${dir}/acceptance.json`,JSON.stringify({pass:true,checks,errors,runs},null,2));
  96. }catch(e){await page?.screenshot({path:`${dir}/failure.png`}).catch(()=>{});fs.writeFileSync(`${dir}/acceptance.json`,JSON.stringify({pass:false,error:e.message,checks,errors,runs},null,2));throw e;}
  97. finally{await browser.close();saveRuns();if(userId){const user=await api(`auth/v1/users/${userId}`);await api(`auth/v1/users/${userId}`,{version:user.version},'DELETE');}}