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.
 
 
 
 

84 lignes
8.1 KiB

  1. import fs from 'node:fs';
  2. import assert from 'node:assert/strict';
  3. import {chromium,expect} from '@playwright/test';
  4. import {login,call} from './role-permissions-live.mjs';
  5. const dir='reports/bridge-introduction-narration-20260924',fixture=process.env.INTRO_FIXTURE==='1',authoringOnly=process.env.INTRO_AUTHORING_ONLY==='1';
  6. const generated=JSON.parse(fs.readFileSync(dir+'/generation.json','utf8'));
  7. const session=await login('root'),checks=[],errors=[],writes=[],premature=[];
  8. const browser=await chromium.launch({channel:'msedge',headless:true,args:['--enable-unsafe-swiftshader']});
  9. const page=await browser.newPage({viewport:{width:1366,height:768}});
  10. await page.addInitScript(s=>{
  11. sessionStorage.setItem('unreal-tran:web:access-token:v1',s.accessToken);sessionStorage.setItem('unreal-tran:web:refresh-token:v1',s.refreshToken);
  12. localStorage.setItem('unreal-tran:web:preferences:v1',JSON.stringify({themeMode:'light'}));
  13. 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);
  14. },session);
  15. await page.route(/\/src\/features\/virtual-training-legacy\/upstream\/views\/training\.js(?:\?|$)/,async route=>{
  16. 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;`});
  17. });
  18. if(fixture)await page.route(/\/api\/tran\/v1\/teaching\/assignments\/86$/,async route=>{
  19. const response=await route.fetch(),body=await response.json();body.data.definitionSnapshot.introduction.background.videoUrl=generated.clips[0].videoUrl;
  20. await route.fulfill({response,json:body});
  21. });
  22. 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);});
  23. const overlay=page.locator('.training-introduction'),video=()=>overlay.locator('video'),toggle=()=>overlay.locator('[data-intro-media="toggle"]');
  24. const state=()=>page.evaluate(()=>globalThis.__introState?.());
  25. const ready=async()=>{await expect(overlay).toBeVisible({timeout:120000});await expect.poll(async()=>(await state())?.ready,{timeout:120000}).toBe(true);};
  26. 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);};
  27. const retained=()=>page.evaluate(()=>globalThis.__previousIntro=document.querySelector('.training-introduction video'));
  28. const stopped=async()=>assert.equal(await page.evaluate(()=>globalThis.__previousIntro.paused&&!globalThis.__previousIntro.getAttribute('src')),true);
  29. try{
  30. for(const id of authoringOnly?[]:fixture?[86]:[86,87,88,89]){
  31. await page.goto('http://127.0.0.1:6180/teaching/virtual-training/preview/'+id);await ready();
  32. const original=await state(),writeCount=writes.length,count=await page.locator('[data-intro-page]').count();
  33. for(let i=0;i<(fixture?1:count);i++){
  34. await expect(video()).toBeVisible();await play();
  35. await toggle().click();await expect.poll(()=>video().evaluate(v=>v.paused)).toBe(true);
  36. 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;});}))};});
  37. assert.equal(layout.overflow,false);assert.equal(layout.overlap,false);assert.equal(layout.imageTextOverlap,false);assert.ok(Math.abs(layout.ratio-.75)<.03);
  38. await page.screenshot({path:`${dir}/${fixture?'fixture-':''}${id}-page-${i}.png`});
  39. await overlay.locator('[data-intro-media="replay"]').click();await play();await retained();
  40. await overlay.locator('[data-intro-command="next"]').click();await stopped();
  41. assert.deepEqual(await state(),original);checks.push({id,page:i,...layout});
  42. }
  43. if(fixture){await overlay.locator('[data-intro-command="skip"]').click();}else await expect(overlay).toBeHidden();
  44. await page.locator('[data-intro-page="0"]').click();await play();await retained();await overlay.locator('[data-intro-command="skip"]').click();await stopped();
  45. assert.deepEqual(await state(),original);assert.equal(writes.length,writeCount);
  46. premature.push(...await page.evaluate(()=>globalThis.__prematureIntro));
  47. console.log('PASS introduction',id);
  48. }
  49. // Media failure stays local to the lesson; retry requires an explicit user click.
  50. if(!authoringOnly){
  51. let fail=true;
  52. await page.route('**/narration-revisions/intro-20260924/*.mp4',route=>fail?route.abort():route.continue());
  53. await page.goto('http://127.0.0.1:6180/teaching/virtual-training/preview/86');await ready();
  54. await expect(toggle()).toHaveText('重试播放',{timeout:30000});fail=false;await toggle().click();await play();
  55. await video().evaluate(v=>v.currentTime=v.duration-.15);await expect.poll(()=>video().evaluate(v=>v.ended),{timeout:15000}).toBe(true);
  56. await expect(page.locator('[data-intro-page="0"]')).toHaveAttribute('aria-current','page');checks.push({failedMediaRetry:true,endedDoesNotAdvance:true});
  57. await page.unroute('**/narration-revisions/intro-20260924/*.mp4');
  58. await page.evaluate(()=>{document.documentElement.classList.add('dark');document.documentElement.dataset.theme='dark';});
  59. await page.screenshot({path:dir+'/theme-dark.png'});
  60. await page.evaluate(()=>{document.documentElement.classList.remove('dark');document.documentElement.dataset.theme='light';});
  61. await page.setViewportSize({width:1920,height:1080});await page.screenshot({path:dir+'/desktop.png'});
  62. }
  63. if(!fixture){
  64. if(!authoringOnly){
  65. await page.goto('http://192.168.31.168:6180/teaching/virtual-training/preview/86');await ready();await play();
  66. 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});
  67. }
  68. await page.setViewportSize({width:1920,height:1080});
  69. for(const projectId of ['438','439','440','441']){
  70. await page.goto('http://127.0.0.1:6180/content/training-projects/'+projectId+'/production');
  71. await expect(page.getByRole('button',{name:'播放动作',exact:true})).toBeEnabled({timeout:120000});
  72. await page.getByRole('tab',{name:'背景与讲解'}).click();
  73. await expect(page.getByText('数字教员视频静态路径(可选)',{exact:true}).first()).toBeVisible();
  74. await expect(page.locator('.production-narration:visible').first()).toBeVisible();
  75. await page.screenshot({path:`${dir}/authoring-${projectId}.png`});
  76. const project=(await call('tran/v1/content/projects/'+projectId,session.accessToken)).data;
  77. const a=project.currentVersion.content.introduction,b=project.currentVersion.content.productionDefinition.introduction;
  78. assert.deepEqual(a,b);assert.ok(a.background.videoUrl.includes('/intro-20260924/'));checks.push({projectId,authoringPersisted:true});
  79. }
  80. }
  81. assert.deepEqual(errors,[]);assert.deepEqual(premature,[]);
  82. 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));
  83. }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();}