No puede seleccionar más de 25 temas Los temas deben comenzar con una letra o número, pueden incluir guiones ('-') y pueden tener hasta 35 caracteres de largo.
 
 
 
 

88 líneas
11 KiB

  1. import fs from 'node:fs';import crypto from 'node:crypto';import assert from 'node:assert/strict';
  2. import {chromium,expect} from '@playwright/test';import {login,call} from './role-permissions-live.mjs';
  3. const report='reports/virtual-html-full-closure-20260912';fs.mkdirSync(report,{recursive:true});
  4. const admin=await login('root'),checks=[],errors=[];
  5. 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};
  6. const suffix=Date.now().toString(36),password='Ex!'+crypto.randomBytes(8).toString('hex');
  7. const roles=await api('auth/v1/roles?size=100'),role=(roles.records||roles).find(r=>r.code==='student');
  8. const user=await api('auth/v1/users',admin,{username:'ex_'+suffix,displayName:'HTML考试验收-'+suffix,departmentId:'5',roleIds:[role.id],defaultRoleId:role.id,enabled:true,password});
  9. const student=await api('auth/v1/auth/login',null,{userId:(user.user||user).id,roleCode:'student',password,rememberMe:false});
  10. const source=(await api('tran/v1/content/projects/161')).project,now=Math.floor(Date.now()/1000);
  11. const task=await api('tran/v1/teaching/assignments',admin,{code:'HTML-EX-'+suffix,name:'HTML正式考试-'+suffix,description:'正式考试迁移与评定统计验收',channel:'VIRTUAL',assignmentKind:'FORMAL_EXAM',executionMode:'EXAM',audienceType:'ASSIGNED',collaborationMode:'INDIVIDUAL',digitalHumanAllowed:false,contentProjectId:source.id,contentVersionId:source.publishedVersionId,scheduleStartAt:now-60,dueAt:now+3600,members:[{userId:(user.user||user).id,memberType:'LEARNER',teamCode:'NEUTRAL',positionCode:'LEARNER',sortOrder:0}]});
  12. const published=await api(`tran/v1/teaching/assignments/${task.id}/publish`,admin,{version:task.version});
  13. const run=await api(`tran/v1/teaching/assignments/${task.id}/accept`,student,{assignmentVersion:published.version,teamCode:'NEUTRAL',positionCode:'LEARNER'});
  14. fs.writeFileSync(report+'/exam-fixture.json',JSON.stringify({userId:(user.user||user).id,assignmentId:task.id,runId:run.id},null,2));
  15. const browser=await chromium.launch({channel:'msedge',headless:true,args:['--enable-unsafe-swiftshader']});
  16. const context=await browser.newContext({viewport:{width:1920,height:1080}});
  17. await context.addInitScript(s=>{sessionStorage.setItem('unreal-tran:web:access-token:v1',s.accessToken);sessionStorage.setItem('unreal-tran:web:refresh-token:v1',s.refreshToken)},student);
  18. const page=await context.newPage();page.on('pageerror',e=>errors.push(e.message));
  19. const pass=(name,details)=>{checks.push({name,pass:true,details});console.log('PASS',name)};
  20. try{
  21. await page.goto(`http://127.0.0.1:6180/teaching/exams/runs/${run.id}`);
  22. await page.locator('.training-shell[data-formal-exam="true"]').waitFor({timeout:120000});
  23. const details=await api(`tran/v1/teaching/assignments/${task.id}`,student);
  24. assert.ok(!details.definitionSnapshot?.steps?.length,'Exam assignment must not disclose steps');
  25. const execution=await api(`tran/v1/teaching/runs/${run.id}/execution`,student);
  26. assert.equal(execution.currentStep.targetId,undefined);assert.ok(execution.deadlineAt>execution.serverNow);
  27. await expect(page.locator('[data-role="teacher-panel"]')).toBeHidden();
  28. await expect(page.locator('[data-action="toggle-issue-highlight"]')).toBeHidden();
  29. pass('正式考试进入 JS+HTML,仅读取服务端当前步骤,关闭数字人和目标提示');
  30. for(let index=0;index<execution.totalSteps;index++){
  31. const currentProjection=await api(`tran/v1/teaching/runs/${run.id}/execution`,student);
  32. await expect(page.locator('.training-step-copy strong')).toHaveText(currentProjection.currentStep.title);
  33. await expect.poll(async()=>Boolean(await page.evaluate(()=>globalThis.__trainingSceneDiagnostics?.()?.publishedContentMounted)),{timeout:90000}).toBe(true);
  34. const button=page.locator('[data-exam-confirm]'),canvas=page.locator('canvas.training-canvas');
  35. await expect(button).toBeDisabled();const rect=await canvas.boundingBox();
  36. for(const [x,y] of [[.29,.4],[.3,.44],[.27,.42],[.5,.55],[.55,.6],[.5,.7],[.65,.65],[.45,.5]]){const px=rect.x+rect.width*x,py=rect.y+rect.height*y;await page.mouse.click(px,py);if(await button.isEnabled())break;await page.waitForTimeout(150);}
  37. await expect(button).toBeEnabled();
  38. if(!index)await page.screenshot({path:report+'/01-exam-current-step.png',fullPage:true});
  39. await button.click();
  40. await expect.poll(async()=>(await api(`tran/v1/teaching/runs/${run.id}`,student)).progressPercent,{timeout:20000}).toBe(Math.round((index+1)*100/execution.totalSteps));
  41. if(index+1<execution.totalSteps)await expect.poll(async()=>(await api(`tran/v1/teaching/runs/${run.id}/execution`,student)).currentStepIndex).toBe(index+1);
  42. }
  43. await expect.poll(async()=>(await api(`tran/v1/teaching/runs/${run.id}`,student)).status,{timeout:20000}).toBe('SUBMITTED');
  44. pass('实际三维选择、逐步提交、重新获取下一安全投影、最终交卷');
  45. await page.screenshot({path:report+'/02-exam-submitted.png',fullPage:true});
  46. const submitted=await api(`tran/v1/teaching/runs/${run.id}`,admin);
  47. const denied=await call(`tran/v1/teaching/runs/${run.id}/review`,student.accessToken,{version:submitted.version,score:100,passed:true,feedback:'越权测试',commandId:crypto.randomUUID()});assert.equal(denied.status,403);
  48. assert.equal(submitted.checkpoint.examObservations.length,execution.totalSteps,'All step observations retained for teacher');
  49. const teacherContext=await browser.newContext({viewport:{width:1920,height:1080}});await teacherContext.addInitScript(s=>{sessionStorage.setItem('unreal-tran:web:access-token:v1',s.accessToken);sessionStorage.setItem('unreal-tran:web:refresh-token:v1',s.refreshToken)},admin);
  50. const teacher=await teacherContext.newPage();await teacher.goto('http://127.0.0.1:6180/teaching/exams');
  51. await teacher.getByRole('button',{name:'查看',exact:true}).first().waitFor({timeout:60000});
  52. await teacher.getByPlaceholder('请输入任务名称或编码').fill(task.name);await teacher.getByPlaceholder('请输入任务名称或编码').press('Enter');
  53. await teacher.locator('article').filter({has:teacher.getByRole('heading',{name:task.name,exact:true})}).getByRole('button',{name:'查看',exact:true}).click();
  54. await teacher.locator('.el-drawer').getByRole('button',{name:'评定',exact:true}).click();
  55. const dialog=teacher.getByRole('dialog',{name:'教学任务评定'});await expect(dialog).toContainText('考核操作记录');
  56. await dialog.getByText('已核对本次运行的评定依据',{exact:true}).click();await dialog.getByRole('spinbutton').fill('86');
  57. await dialog.getByText('通过',{exact:true}).click();await dialog.getByPlaceholder('请输入评定意见').fill('HTML正式考试操作已核验');
  58. await teacher.screenshot({path:report+'/06-teacher-exam-review.png',fullPage:true});await dialog.getByRole('button',{name:'提交评定'}).click();
  59. await expect.poll(async()=>(await api(`tran/v1/teaching/runs/${run.id}`,admin)).status).toBe('REVIEWED');
  60. const reviewed=await api(`tran/v1/teaching/runs/${run.id}`,admin);
  61. assert.equal(reviewed.status,'REVIEWED');assert.equal(reviewed.passed,true);assert.equal(reviewed.score,86);
  62. const records=await api(`tran/v1/teaching/analytics/records?keyword=${encodeURIComponent(task.name)}&assignmentKind=FORMAL_EXAM&page=1&size=20`,admin);
  63. assert.ok(records.records.some(r=>String(r.runId||r.id)===String(run.id)));
  64. const learnerResult=await api(`tran/v1/teaching/runs/${run.id}`,student);assert.equal(learnerResult.status,'REVIEWED');assert.equal(learnerResult.checkpoint.examObservations,undefined);
  65. pass('学员不能自评;教员评分后学员可见,统计明细包含本次运行',{runId:run.id,score:86});
  66. await teacher.goto('http://127.0.0.1:6180/teaching/data-center');
  67. await teacher.getByTestId('analytics-tab-exam').click();await teacher.getByPlaceholder('搜索学员、编号或任务名称').fill(task.name);
  68. await teacher.getByPlaceholder('搜索学员、编号或任务名称').press('Enter');
  69. const table=teacher.getByTestId('analytics-record-table');await expect(table).toContainText(task.name,{timeout:30000});await expect(table).toContainText('86 分');
  70. await table.getByRole('button',{name:'详情',exact:true}).first().click();await teacher.screenshot({path:report+'/07-exam-analytics.png',fullPage:true});
  71. pass('数据中心页面查询正式考核成绩与记录详情');
  72. const history=await call(`tran/v1/teaching/runs/${run.id}/execution`,student.accessToken);assert.notEqual(history.status,200);
  73. const deadlineNow=Math.floor(Date.now()/1000);
  74. const timedTask=await api('tran/v1/teaching/assignments',admin,{code:'HTML-TIME-'+suffix,name:'HTML考试截止验收-'+suffix,description:'安全边界验收',channel:'VIRTUAL',assignmentKind:'FORMAL_EXAM',executionMode:'EXAM',audienceType:'ASSIGNED',collaborationMode:'INDIVIDUAL',digitalHumanAllowed:false,contentProjectId:source.id,contentVersionId:source.publishedVersionId,scheduleStartAt:deadlineNow-5,dueAt:deadlineNow+25,members:[{userId:(user.user||user).id,memberType:'LEARNER',teamCode:'NEUTRAL',positionCode:'LEARNER',sortOrder:0}]});
  75. const timedPublished=await api(`tran/v1/teaching/assignments/${timedTask.id}/publish`,admin,{version:timedTask.version});
  76. const timedRun=await api(`tran/v1/teaching/assignments/${timedTask.id}/accept`,student,{assignmentVersion:timedPublished.version,teamCode:'NEUTRAL',positionCode:'LEARNER'});
  77. const fixture=JSON.parse(fs.readFileSync(report+'/exam-fixture.json','utf8'));Object.assign(fixture,{deadlineAssignmentId:timedTask.id,deadlineRunId:timedRun.id});fs.writeFileSync(report+'/exam-fixture.json',JSON.stringify(fixture,null,2));
  78. await page.goto(`http://127.0.0.1:6180/teaching/exams/runs/${timedRun.id}`);await page.locator('.training-shell').waitFor({timeout:20000});
  79. await expect(page.getByRole('button',{name:'返回考试测评',exact:true})).toBeVisible({timeout:35000});
  80. const expiredProjection=await call(`tran/v1/teaching/runs/${timedRun.id}/execution`,student.accessToken);assert.notEqual(expiredProjection.status,200);
  81. const expiredAssets=await call(`tran/v1/teaching/runs/${timedRun.id}/assets`,student.accessToken);assert.notEqual(expiredAssets.status,200);
  82. await page.screenshot({path:report+'/08-exam-deadline.png',fullPage:true});
  83. const expiredRun=await api(`tran/v1/teaching/runs/${timedRun.id}`,admin);await api(`tran/v1/teaching/runs/${timedRun.id}/timeout`,admin,{version:expiredRun.version});
  84. pass('考试到期自动关闭工作区;服务端拒绝继续获取执行投影和资产,教员完成超时结算');
  85. assert.equal(errors.length,0,errors.join('\n'));
  86. }catch(error){checks.push({name:'考试、评分和统计验收',pass:false,error:error.message});console.error(error);process.exitCode=1;await page.screenshot({path:report+'/exam-failure.png',fullPage:true})}
  87. finally{fs.writeFileSync(report+'/exam-checks.json',JSON.stringify({checks,errors},null,2));await browser.close()}