You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 

78 lines
7.1 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 } from '../../unreal_tran_web/src/features/virtual-training-scripts/definition.js';
  7. import { verifyVisualEffects } from './bridge-three-dimensional-assertions.mjs';
  8. const dir='reports/bridge-three-dimensional-20260913';
  9. const root=await login('root');
  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 roles=await api('auth/v1/roles/all'),role=(roles.records||roles).find(r=>r.code==='student');
  12. const password='Br!'+crypto.randomBytes(8).toString('hex');
  13. const created=await api('auth/v1/users',{username:'bridge3d_'+Date.now().toString(36),password,displayName:'三维流程验收',departmentId:'5',roleIds:[role.id],defaultRoleId:role.id,enabled:true,remark:'临时验收账号'});
  14. const userId=(created.user||created).id;
  15. const signed=await call('auth/v1/auth/login',null,{userId,password,roleCode:'student',rememberMe:false});assert.equal(signed.status,200);
  16. 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;};
  17. const file=fs.readdirSync(dir).filter(n=>/^fixtures-\d+\.json$/.test(n)).sort().at(-1);
  18. const fixtures=JSON.parse(fs.readFileSync(`${dir}/${file}`,'utf8'));
  19. const checks=[],runs=[],errors=[];
  20. const browser=await chromium.launch({channel:'msedge',headless:true,args:['--enable-unsafe-swiftshader']});
  21. const page=await browser.newPage({viewport:{width:1600,height:1000}});
  22. await page.addInitScript(s=>{sessionStorage.setItem('unreal-tran:web:access-token:v1',s.accessToken);localStorage.setItem('unreal-tran:web:preferences:v1',JSON.stringify({themeMode:'military'}));},signed.data);
  23. page.on('pageerror',e=>errors.push(e.message));
  24. const ready=async()=>{await page.waitForFunction(()=>globalThis.__trainingSceneDiagnostics?.()?.bridgeWorkshop?.targets.length>0,{},{timeout:120000});await page.waitForTimeout(1200);};
  25. const diag=()=>page.evaluate(()=>globalThis.__trainingSceneDiagnostics());
  26. try{
  27. for(const fixture of fixtures.filter(f=>!process.env.BRIDGE_CODE||process.env.BRIDGE_CODE===f.code)){
  28. const task=await learner(`teaching/assignments/${fixture.taskId}`);
  29. const script=scripts.find(s=>s.code===fixture.code);
  30. assert.deepEqual(task.definitionSnapshot.steps.map(s=>s.action.mechanism.steps),script.definition.steps.map(s=>s.action.mechanism.steps),'Learner projection must retain visual actions');
  31. let run=await learner(`teaching/assignments/${task.id}/accept`,{assignmentVersion:task.version,teamCode:'NEUTRAL',positionCode:'LEARNER'});
  32. runs.push({taskId:task.id,runId:run.id,code:fixture.code});fs.writeFileSync(`${dir}/run-fixtures.json`,JSON.stringify(runs,null,2));
  33. await page.goto(`http://127.0.0.1:6180/teaching/virtual-training/runs/${run.id}`);await ready();
  34. for(let index=0;index<task.definitionSnapshot.steps.length;index++){
  35. const step=task.definitionSnapshot.steps[index],ops=step.action.mechanism.steps;
  36. await expect(page.locator('[data-role="step-count"]')).toHaveText(`${index+1} / ${task.definitionSnapshot.steps.length}`);
  37. if(step.mode==='measure'){
  38. await page.locator('[data-open-preparation]').click();await page.locator('[data-measurement]').fill('根据安装段连接管路,原稿矛盾描述待教员核对。');await page.locator('[data-close-preparation]').click();
  39. }
  40. for(let k=0;k<ops.length;k++){
  41. await expect(page.locator('[data-action="next"]')).toBeDisabled();
  42. await expect.poll(async()=>(await diag()).bridgeWorkshop.operation.targetId).toBe(ops[k].targetId);
  43. if(index===0&&k===ops.length-1&&fixture.code==='TASK-VIRTUAL-006'){
  44. await page.route(`**/teaching/runs/${run.id}/progress`,route=>route.fulfill({status:503,contentType:'application/json',body:JSON.stringify({code:'SERVICE_UNAVAILABLE',message:'验收模拟保存失败'})}),{times:1});
  45. const response=page.waitForResponse(r=>r.url().includes(`/runs/${run.id}/progress`));
  46. await page.locator('[data-role="part-label"]').click();assert.equal((await response).status(),503);
  47. await expect.poll(async()=>(await diag()).bridgeWorkshop.pending).toBe(false);
  48. assert.equal((await learner(`teaching/runs/${run.id}`)).progressPercent,0);
  49. await expect(page.locator('[data-action="next"]')).toBeDisabled();checks.push('保存失败保留当前步骤与未完成状态,允许重试');
  50. }
  51. if(k===ops.length-1){const r=page.waitForResponse(r=>r.url().includes(`/runs/${run.id}/progress`));await page.locator('[data-role="part-label"]').click();assert.equal((await r).status(),200);}
  52. else{await page.locator('[data-role="part-label"]').click();await expect.poll(async()=>(await diag()).bridgeWorkshop.operationIndex).toBe(k+1);}
  53. await verifyVisualEffects(page,ops[k]);
  54. }
  55. await expect(page.locator('[data-action="next"]')).toBeEnabled();
  56. const saved=await learner(`teaching/runs/${run.id}/steps`);assert.equal(saved.filter(s=>s.statusCode==='COMPLETED').length,index+1);
  57. if(index===3){
  58. const before=(await diag()).bridgeWorkshop.targets;
  59. await page.reload();await ready();
  60. const after=(await diag()).bridgeWorkshop.targets;
  61. for(const b of before){const a=after.find(v=>v.id===b.id);assert.equal(a.visible,b.visible);assert.ok(a.position.every((v,i)=>Math.abs(v-b.position[i])<0.001),`Restore ${b.id}`);}
  62. checks.push(`${fixture.code} 刷新恢复模型位置、封堵显示状态与步骤进度`);
  63. }else if(index<task.definitionSnapshot.steps.length-1)await page.locator('[data-action="next"]').click();
  64. console.log(fixture.code,index+1,'saved');
  65. }
  66. await page.locator('[data-action="next"]').click();await expect.poll(async()=>(await learner(`teaching/runs/${run.id}`)).status).toBe('SUBMITTED');
  67. run=await learner(`teaching/runs/${run.id}`);assert.equal(run.score,null);
  68. run=await api(`tran/v1/teaching/runs/${run.id}/review`,{version:run.version,commandId:crypto.randomUUID(),score:85,passed:true,feedback:'自动化验收示例评分',rubric:{testFixture:true}});
  69. assert.equal((await learner(`teaching/runs/${run.id}`)).score,85);
  70. await page.reload();await ready();await expect(page.locator('.virtual-server-status')).toContainText('85');
  71. await page.screenshot({path:`${dir}/${fixture.code}-reviewed.png`});
  72. checks.push(`${fixture.code} 三维操作、提交与教员评定通过`);
  73. }
  74. assert.deepEqual(errors,[]);fs.writeFileSync(`${dir}/run-acceptance.json`,JSON.stringify({pass:true,checks,runs,errors},null,2));
  75. }catch(error){await page.screenshot({path:`${dir}/run-failure.png`}).catch(()=>{});fs.writeFileSync(`${dir}/run-diagnostic.json`,JSON.stringify(await diag().catch(()=>null),null,2));fs.writeFileSync(`${dir}/run-acceptance.json`,JSON.stringify({pass:false,error:error.message,checks,runs,errors},null,2));throw error;}
  76. finally{fs.appendFileSync(`${dir}/run-attempts.jsonl`,JSON.stringify({runs,userId})+'\n');await browser.close();const user=await api(`auth/v1/users/${userId}`);await api(`auth/v1/users/${userId}`,{version:user.version},'DELETE');}