import fs from 'node:fs'; import assert from 'node:assert/strict'; import {chromium,expect} from '@playwright/test'; import {login,call} from './role-permissions-live.mjs'; const dir='reports/bridge-introduction-narration-20260924',fixture=process.env.INTRO_FIXTURE==='1',authoringOnly=process.env.INTRO_AUTHORING_ONLY==='1'; const generated=JSON.parse(fs.readFileSync(dir+'/generation.json','utf8')); const session=await login('root'),checks=[],errors=[],writes=[],premature=[]; const browser=await chromium.launch({channel:'msedge',headless:true,args:['--enable-unsafe-swiftshader']}); const page=await browser.newPage({viewport:{width:1366,height:768}}); await page.addInitScript(s=>{ sessionStorage.setItem('unreal-tran:web:access-token:v1',s.accessToken);sessionStorage.setItem('unreal-tran:web:refresh-token:v1',s.refreshToken); localStorage.setItem('unreal-tran:web:preferences:v1',JSON.stringify({themeMode:'light'})); 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); },session); await page.route(/\/src\/features\/virtual-training-legacy\/upstream\/views\/training\.js(?:\?|$)/,async route=>{ const response=await route.fetch();await route.fulfill({response,body:await response.text()+`\nglobalThis.__introState=()=>activeTraining?{ready:activeTraining.sceneReady,step:activeTraining.session.stepIndex,completed:[...activeTraining.session.completed]}:null;`}); }); if(fixture)await page.route(/\/api\/tran\/v1\/teaching\/assignments\/86$/,async route=>{ const response=await route.fetch(),body=await response.json();body.data.definitionSnapshot.introduction.background.videoUrl=generated.clips[0].videoUrl; await route.fulfill({response,json:body}); }); page.on('pageerror',e=>errors.push(e.message));page.on('request',r=>{if(r.url().includes('/api/tran/')&&!['GET','HEAD'].includes(r.method()))writes.push(new URL(r.url()).pathname);}); const overlay=page.locator('.training-introduction'),video=()=>overlay.locator('video'),toggle=()=>overlay.locator('[data-intro-media="toggle"]'); const state=()=>page.evaluate(()=>globalThis.__introState?.()); const ready=async()=>{await expect(overlay).toBeVisible({timeout:120000});await expect.poll(async()=>(await state())?.ready,{timeout:120000}).toBe(true);}; const play=async()=>{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 retained=()=>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); try{ for(const id of authoringOnly?[]:fixture?[86]:[86,87,88,89]){ await page.goto('http://127.0.0.1:6180/teaching/virtual-training/preview/'+id);await ready(); const original=await state(),writeCount=writes.length,count=await page.locator('[data-intro-page]').count(); for(let i=0;i<(fixture?1:count);i++){ await expect(video()).toBeVisible();await play(); await toggle().click();await expect.poll(()=>video().evaluate(v=>v.paused)).toBe(true); const layout=await overlay.evaluate(el=>{const c=el.querySelector('.training-introduction-content'),v=el.querySelector('video').getBoundingClientRect(),a=el.querySelector('.introduction-reading').getBoundingClientRect(),f=el.querySelector('.introduction-visual')?.getBoundingClientRect();return {overflow:c.scrollWidth>c.clientWidth,ratio:v.width/v.height,overlap:v.lefta.left&&v.topa.top,imageTextOverlap:!!f&&[...el.querySelectorAll('.introduction-reading p')].some(p=>[...p.childNodes].filter(n=>n.nodeType===3).some(n=>{const range=document.createRange();return [...n.textContent].some((_,i)=>{range.setStart(n,i);range.setEnd(n,i+1);const r=range.getBoundingClientRect();return r.leftf.left+1&&r.topf.top+1;});}))};}); assert.equal(layout.overflow,false);assert.equal(layout.overlap,false);assert.equal(layout.imageTextOverlap,false);assert.ok(Math.abs(layout.ratio-.75)<.03); await page.screenshot({path:`${dir}/${fixture?'fixture-':''}${id}-page-${i}.png`}); await overlay.locator('[data-intro-media="replay"]').click();await play();await retained(); await overlay.locator('[data-intro-command="next"]').click();await stopped(); assert.deepEqual(await state(),original);checks.push({id,page:i,...layout}); } if(fixture){await overlay.locator('[data-intro-command="skip"]').click();}else await expect(overlay).toBeHidden(); await page.locator('[data-intro-page="0"]').click();await play();await retained();await overlay.locator('[data-intro-command="skip"]').click();await stopped(); assert.deepEqual(await state(),original);assert.equal(writes.length,writeCount); premature.push(...await page.evaluate(()=>globalThis.__prematureIntro)); console.log('PASS introduction',id); } // Media failure stays local to the lesson; retry requires an explicit user click. if(!authoringOnly){ let fail=true; await page.route('**/narration-revisions/intro-20260924/*.mp4',route=>fail?route.abort():route.continue()); await page.goto('http://127.0.0.1:6180/teaching/virtual-training/preview/86');await ready(); await expect(toggle()).toHaveText('重试播放',{timeout:30000});fail=false;await toggle().click();await play(); await video().evaluate(v=>v.currentTime=v.duration-.15);await expect.poll(()=>video().evaluate(v=>v.ended),{timeout:15000}).toBe(true); await expect(page.locator('[data-intro-page="0"]')).toHaveAttribute('aria-current','page');checks.push({failedMediaRetry:true,endedDoesNotAdvance:true}); await page.unroute('**/narration-revisions/intro-20260924/*.mp4'); await page.evaluate(()=>{document.documentElement.classList.add('dark');document.documentElement.dataset.theme='dark';}); await page.screenshot({path:dir+'/theme-dark.png'}); await page.evaluate(()=>{document.documentElement.classList.remove('dark');document.documentElement.dataset.theme='light';}); await page.setViewportSize({width:1920,height:1080});await page.screenshot({path:dir+'/desktop.png'}); } if(!fixture){ if(!authoringOnly){ await page.goto('http://192.168.31.168:6180/teaching/virtual-training/preview/86');await ready();await play(); const response=await page.request.get('http://192.168.31.168:6180/api/auth/v1/system-config/public');assert.equal(response.status(),200);checks.push({lanPlayback:true,gatewayProxy:true}); } await page.setViewportSize({width:1920,height:1080}); for(const projectId of ['438','439','440','441']){ await page.goto('http://127.0.0.1:6180/content/training-projects/'+projectId+'/production'); await expect(page.getByRole('button',{name:'播放动作',exact:true})).toBeEnabled({timeout:120000}); await page.getByRole('tab',{name:'背景与讲解'}).click(); await expect(page.getByText('数字教员视频静态路径(可选)',{exact:true}).first()).toBeVisible(); await expect(page.locator('.production-narration:visible').first()).toBeVisible(); await page.screenshot({path:`${dir}/authoring-${projectId}.png`}); const project=(await call('tran/v1/content/projects/'+projectId,session.accessToken)).data; const a=project.currentVersion.content.introduction,b=project.currentVersion.content.productionDefinition.introduction; assert.deepEqual(a,b);assert.ok(a.background.videoUrl.includes('/intro-20260924/'));checks.push({projectId,authoringPersisted:true}); } } assert.deepEqual(errors,[]);assert.deepEqual(premature,[]); fs.writeFileSync(dir+(authoringOnly?'/authoring-live.json':fixture?'/fixture-live.json':'/live.json'),JSON.stringify({pass:true,checks,errors,premature,writes,account:'internal maintenance preview',learnerResultsCreated:false},null,2)); }catch(error){await page.screenshot({path:dir+'/live-failure.png'}).catch(()=>{});fs.writeFileSync(dir+'/live-failure.json',JSON.stringify({error:error.stack,checks,errors,premature},null,2));throw error;}finally{await browser.close();}