|
- 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.left<a.right&&v.right>a.left&&v.top<a.bottom&&v.bottom>a.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.left<f.right-1&&r.right>f.left+1&&r.top<f.bottom-1&&r.bottom>f.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();}
|