import fs from 'node:fs'; import assert from 'node:assert/strict'; import {expect} from '@playwright/test'; import {call} from './role-permissions-live.mjs'; // Test-only observer: no direct changes to session, part poses or operation state. export async function finalAcceptance({page,dir,checks,snap,settle,session}){ const writes=[],validations=[],tasks=[],premature=[]; await page.addInitScript(()=>{ globalThis.__prematureIntro=[]; document.addEventListener('play',event=>{ if(event.target.matches?.('.training-introduction video')&&document.querySelector('[data-role="loading"]:not(.is-hidden)'))globalThis.__prematureIntro.push(event.target.currentSrc); },true); }); 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()});}); page.on('response',r=>{if(r.url().includes('/preview/validate'))validations.push({path:new URL(r.url()).pathname,status:r.status()});}); const overlay=page.locator('.training-introduction'); const video=()=>overlay.locator('video'),toggle=()=>overlay.locator('[data-intro-media="toggle"]'); const play=async()=>{ await expect(video()).toBeVisible();await expect(toggle()).toBeEnabled(); if(await toggle().getAttribute('aria-pressed')!=='true')await toggle().click(); await expect.poll(()=>video().evaluate(v=>v.currentTime),{timeout:30000}).toBeGreaterThan(.15); }; const retain=()=>page.evaluate(()=>{globalThis.__previousIntro=document.querySelector('.training-introduction video');}); const stopped=async()=>assert.equal(await page.evaluate(()=>globalThis.__previousIntro.paused&&!globalThis.__previousIntro.getAttribute('src')),true,'previous introduction is disposed'); const state=d=>({step:d.step,operation:d.operation,id:d.id,complete:d.complete,completed:d.completed,pose:d.pose,tray:d.tray}); const withoutClock=value=>{const {serverNow,...business}=value;return business;}; 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)); return { async introduce(id,task){ tasks.push({id,code:task.code||task.taskCode,contentVersionId:task.contentVersionId,before:withoutClock(task)});persist(); const original=state(await snap()),count=await page.locator('[data-intro-page]').count(),writeCount=writes.length; assert.equal(count,{'86':6,'87':3,'88':1,'89':1}[id]); for(let i=0;ivideo().evaluate(v=>v.paused)).toBe(true); await page.screenshot({path:`${dir}/task-${id}-intro-${i}.png`}); await overlay.locator('[data-intro-media="replay"]').click();await play();await retain(); await overlay.locator('[data-intro-command="next"]').click();await stopped(); assert.deepEqual(state(await snap()),original,'introduction does not mutate operation state'); checks.push({task:id,introduction:i+1,check:'play, pause, replay, next, stop; no operation changes'}); } await expect(overlay).toBeHidden();assert.equal(writes.length,writeCount,'introduction never submits training actions'); premature.push(...await page.evaluate(()=>globalThis.__prematureIntro)); console.log('PASS full introduction',id,count);persist(); }, async revisit(id,step,operation){ await settle();const original=state(await snap()),writeCount=writes.length; await page.locator('[data-intro-page="0"]').click();await play(); assert.equal(await page.locator('.training-teacher video').evaluateAll(nodes=>nodes.every(v=>v.paused)),true,'operation narrator pauses during introduction'); assert.deepEqual(state(await snap()),original,'opening introduction preserves partially completed operations'); await page.screenshot({path:`${dir}/task-${id}-mid-operation-intro.png`});await retain(); await overlay.locator('[data-intro-command="skip"]').click();await stopped();await settle(); assert.deepEqual(state(await snap()),original,'returning from introduction preserves poses, progress and stored parts'); assert.equal(writes.length,writeCount,'revisiting introduction does not submit operations'); checks.push({task:id,step:step+1,operation:operation+1,check:'mid-operation background and skip preserve progress, cover/hoses/bolts and workbench'}); console.log('PASS mid-operation revisit',id,step+1,operation+1);persist(); }, async finish(id,task){ await settle();const d=await snap(),count=task.definitionSnapshot.steps.length; assert.equal(d.completed.length,count);assert.equal(d.step,count-1); const accepted=validations.filter(v=>v.path.endsWith('/'+id+'/preview/validate')&&v.status===200); assert.equal(accepted.length,count,'each step accepted by actual preview validation endpoint'); await page.locator('[data-action="next"]').click();await expect(page.getByText('预览校验完成',{exact:true})).toBeVisible(); await expect(page.getByText('本次预览的操作已经通过服务端校验,不计入学员训练成绩。',{exact:true})).toBeVisible(); assert.equal(await page.locator('video').evaluateAll(nodes=>nodes.every(v=>v.paused)),true,'completion stops all videos'); if(id==='87')await expect(page.getByText('应急替代操作训练已完成,后续可按正常作业规程使用架桥车。',{exact:true})).toBeVisible(); await page.screenshot({path:`${dir}/task-${id}-complete.png`}); const current=await call('tran/v1/teaching/assignments/'+id,session.accessToken);assert.equal(current.status,200); assert.deepEqual(withoutClock(current.data),withoutClock(task),'published task and pinned version stay unchanged'); 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; checks.push({task:id,check:'final completion dialog, server validation for all steps, unchanged task and pinned version',steps:count,operations:record.operations}); console.log('PASS training completed',id,count,record.operations);persist(); }, async verify(){ assert.deepEqual(premature,[],'no introduction playback during scene loading'); 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'); assert.ok(tasks.every(t=>t.finished));persist(); }, }; }