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.
 
 
 
 

81 lignes
6.6 KiB

  1. import fs from 'node:fs';
  2. import assert from 'node:assert/strict';
  3. import {expect} from '@playwright/test';
  4. import {call} from './role-permissions-live.mjs';
  5. // Test-only observer: no direct changes to session, part poses or operation state.
  6. export async function finalAcceptance({page,dir,checks,snap,settle,session}){
  7. const writes=[],validations=[],tasks=[],premature=[];
  8. await page.addInitScript(()=>{
  9. globalThis.__prematureIntro=[];
  10. document.addEventListener('play',event=>{
  11. if(event.target.matches?.('.training-introduction video')&&document.querySelector('[data-role="loading"]:not(.is-hidden)'))globalThis.__prematureIntro.push(event.target.currentSrc);
  12. },true);
  13. });
  14. page.on('request',r=>{if(r.url().includes('/api/tran/')&&!['GET','HEAD'].includes(r.method()))writes.push({path:new URL(r.url()).pathname,method:r.method()});});
  15. page.on('response',r=>{if(r.url().includes('/preview/validate'))validations.push({path:new URL(r.url()).pathname,status:r.status()});});
  16. const overlay=page.locator('.training-introduction');
  17. const video=()=>overlay.locator('video'),toggle=()=>overlay.locator('[data-intro-media="toggle"]');
  18. const play=async()=>{
  19. await expect(video()).toBeVisible();await expect(toggle()).toBeEnabled();
  20. if(await toggle().getAttribute('aria-pressed')!=='true')await toggle().click();
  21. await expect.poll(()=>video().evaluate(v=>v.currentTime),{timeout:30000}).toBeGreaterThan(.15);
  22. };
  23. const retain=()=>page.evaluate(()=>{globalThis.__previousIntro=document.querySelector('.training-introduction video');});
  24. const stopped=async()=>assert.equal(await page.evaluate(()=>globalThis.__previousIntro.paused&&!globalThis.__previousIntro.getAttribute('src')),true,'previous introduction is disposed');
  25. const state=d=>({step:d.step,operation:d.operation,id:d.id,complete:d.complete,completed:d.completed,pose:d.pose,tray:d.tray});
  26. const withoutClock=value=>{const {serverNow,...business}=value;return business;};
  27. const persist=()=>fs.writeFileSync(dir+'/acceptance.json',JSON.stringify({tasks,writes,validations,premature,environment:{baseUrl:process.env.BASE_URL||'http://127.0.0.1:6180',width:Number(process.env.WIDTH)||1440,height:Number(process.env.HEIGHT)||900},account:'internal maintenance / instructor preview',learnerResultsCreated:false},null,2));
  28. return {
  29. async introduce(id,task){
  30. tasks.push({id,code:task.code||task.taskCode,contentVersionId:task.contentVersionId,before:withoutClock(task)});persist();
  31. const original=state(await snap()),count=await page.locator('[data-intro-page]').count(),writeCount=writes.length;
  32. assert.equal(count,{'86':6,'87':3,'88':1,'89':1}[id]);
  33. for(let i=0;i<count;i++){
  34. await play();await toggle().click();await expect.poll(()=>video().evaluate(v=>v.paused)).toBe(true);
  35. await page.screenshot({path:`${dir}/task-${id}-intro-${i}.png`});
  36. await overlay.locator('[data-intro-media="replay"]').click();await play();await retain();
  37. await overlay.locator('[data-intro-command="next"]').click();await stopped();
  38. assert.deepEqual(state(await snap()),original,'introduction does not mutate operation state');
  39. checks.push({task:id,introduction:i+1,check:'play, pause, replay, next, stop; no operation changes'});
  40. }
  41. await expect(overlay).toBeHidden();assert.equal(writes.length,writeCount,'introduction never submits training actions');
  42. premature.push(...await page.evaluate(()=>globalThis.__prematureIntro));
  43. console.log('PASS full introduction',id,count);persist();
  44. },
  45. async revisit(id,step,operation){
  46. await settle();const original=state(await snap()),writeCount=writes.length;
  47. await page.locator('[data-intro-page="0"]').click();await play();
  48. assert.equal(await page.locator('.training-teacher video').evaluateAll(nodes=>nodes.every(v=>v.paused)),true,'operation narrator pauses during introduction');
  49. assert.deepEqual(state(await snap()),original,'opening introduction preserves partially completed operations');
  50. await page.screenshot({path:`${dir}/task-${id}-mid-operation-intro.png`});await retain();
  51. await overlay.locator('[data-intro-command="skip"]').click();await stopped();await settle();
  52. assert.deepEqual(state(await snap()),original,'returning from introduction preserves poses, progress and stored parts');
  53. assert.equal(writes.length,writeCount,'revisiting introduction does not submit operations');
  54. checks.push({task:id,step:step+1,operation:operation+1,check:'mid-operation background and skip preserve progress, cover/hoses/bolts and workbench'});
  55. console.log('PASS mid-operation revisit',id,step+1,operation+1);persist();
  56. },
  57. async finish(id,task){
  58. await settle();const d=await snap(),count=task.definitionSnapshot.steps.length;
  59. assert.equal(d.completed.length,count);assert.equal(d.step,count-1);
  60. const accepted=validations.filter(v=>v.path.endsWith('/'+id+'/preview/validate')&&v.status===200);
  61. assert.equal(accepted.length,count,'each step accepted by actual preview validation endpoint');
  62. await page.locator('[data-action="next"]').click();await expect(page.getByText('预览校验完成',{exact:true})).toBeVisible();
  63. await expect(page.getByText('本次预览的操作已经通过服务端校验,不计入学员训练成绩。',{exact:true})).toBeVisible();
  64. assert.equal(await page.locator('video').evaluateAll(nodes=>nodes.every(v=>v.paused)),true,'completion stops all videos');
  65. if(id==='87')await expect(page.getByText('应急替代操作训练已完成,后续可按正常作业规程使用架桥车。',{exact:true})).toBeVisible();
  66. await page.screenshot({path:`${dir}/task-${id}-complete.png`});
  67. const current=await call('tran/v1/teaching/assignments/'+id,session.accessToken);assert.equal(current.status,200);
  68. assert.deepEqual(withoutClock(current.data),withoutClock(task),'published task and pinned version stay unchanged');
  69. const record=tasks.find(t=>t.id===id);record.finished=true;record.steps=count;record.operations=task.definitionSnapshot.steps.reduce((n,s)=>n+s.action.mechanism.steps.length,0);record.storedParts=d.tray?.items.filter(item=>item.stored).map(item=>item.id)||[];delete record.before;
  70. checks.push({task:id,check:'final completion dialog, server validation for all steps, unchanged task and pinned version',steps:count,operations:record.operations});
  71. console.log('PASS training completed',id,count,record.operations);persist();
  72. },
  73. async verify(){
  74. assert.deepEqual(premature,[],'no introduction playback during scene loading');
  75. assert.ok(writes.every(w=>w.method==='POST'&&/^\/api\/tran\/v1\/teaching\/assignments\/\d+\/preview\/validate$/.test(w.path)),'only preview validation writes are permitted');
  76. assert.ok(tasks.every(t=>t.finished));persist();
  77. },
  78. };
  79. }