Вы не можете выбрать более 25 тем Темы должны начинаться с буквы или цифры, могут содержать дефисы(-) и должны содержать не более 35 символов.
 
 
 
 

103 строки
6.9 KiB

  1. import { chromium, expect } from '@playwright/test';
  2. import assert from 'node:assert/strict';
  3. import fs from 'node:fs';
  4. import { credentials } from './role-permissions-live.mjs';
  5. import videos from '../../unreal_tran_web/src/features/virtual-training-scripts/narration-video-manifest.js';
  6. const origin=process.env.BASE_URL || 'http://127.0.0.1:6180';
  7. const widgetOrigin=process.env.WIDGET_URL || 'http://127.0.0.1:8003';
  8. const dir=process.env.REPORT_DIR || 'reports/bridge-lecture-polish-20260915/local';
  9. fs.mkdirSync(dir,{recursive:true});
  10. const auth=await fetch(`${origin}/api/auth/v1/auth/login`,{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({...credentials('root'),roleCode:'admin',rememberMe:false})}).then(r=>r.json());
  11. assert.ok(auth.data?.accessToken,'Login');
  12. const listing=await fetch(`${origin}/api/tran/v1/teaching/assignments?channel=VIRTUAL&assignmentKind=TRAINING&size=100`,{headers:{Authorization:`Bearer ${auth.data.accessToken}`}}).then(r=>r.json());
  13. const browser=await chromium.launch({channel:'msedge',headless:true,args:['--enable-unsafe-swiftshader','--autoplay-policy=no-user-gesture-required']});
  14. const previous=process.env.WIDGET_ONLY === '1' ? JSON.parse(fs.readFileSync(fs.existsSync(`${dir}/result.json`)?`${dir}/result.json`:`${dir}/failure.json`)) : null;
  15. const checks=previous?.checks.filter(c=>c.task) || [], errors=[];
  16. let page;
  17. try {
  18. page=await browser.newPage({viewport:{width:1920,height:945}});
  19. await page.addInitScript(token=>{
  20. sessionStorage.setItem('unreal-tran:web:access-token:v1',token);
  21. localStorage.setItem('unreal-tran:web:preferences:v1',JSON.stringify({themeMode:'military'}));
  22. window.__lecturePlays=[];
  23. const play=HTMLMediaElement.prototype.play;
  24. HTMLMediaElement.prototype.play=function(){
  25. if(this.classList.contains('training-server-video')) {
  26. const visible=e=>{const s=getComputedStyle(e);return e.getClientRects().length>0&&s.visibility!=='hidden'&&s.display!=='none'&&s.opacity!=='0'};
  27. window.__lecturePlays.push({loading:[...document.querySelectorAll('.training-loading:not(.is-hidden), .el-loading-mask')].some(visible),src:new URL(this.currentSrc||this.src,location.href).pathname});
  28. if(window.__blockNextLecture){window.__blockNextLecture=false;return Promise.reject(new DOMException('User gesture required','NotAllowedError'))}
  29. }
  30. return play.apply(this,arguments);
  31. };
  32. },auth.data.accessToken);
  33. page.on('pageerror',e=>errors.push(e.message));
  34. // Deliberately delay model and decoder responses without replacing their data.
  35. let delayed=previous?.delayed || 0;
  36. await page.route(/(?:\.glb(?:\?|$)|draco.*\.(?:wasm|js)(?:\?|$)|\/assets\/[^/]+\/download)/,async route=>{
  37. delayed++;
  38. await new Promise(resolve=>setTimeout(resolve,1500));
  39. await route.continue();
  40. });
  41. for(const pack of process.env.WIDGET_ONLY === '1' ? [] : videos.scripts){
  42. const task=listing.data.records.find(t=>t.code===pack.code);assert.ok(task,pack.code);
  43. await page.goto(`${origin}/teaching/virtual-training/preview/${task.id}`);
  44. await expect(page.locator('[data-role="loading"]')).toHaveClass(/is-hidden/,{timeout:150000});
  45. const media=page.locator('.training-server-video');
  46. await expect(media).toBeVisible();
  47. await expect(page.locator('[data-action="toggle-teacher-media"]')).toHaveText('切换语音');
  48. await expect.poll(()=>media.evaluate(v=>v.currentTime),{timeout:20000}).toBeGreaterThan(.1);
  49. const attempts=await page.evaluate(()=>window.__lecturePlays);
  50. assert.ok(attempts.length>0);assert.ok(attempts.every(p=>!p.loading),'Narration must wait for visible loading overlays to finish');
  51. assert.equal(attempts[0].src,pack.steps[0].url);
  52. assert.equal(await page.locator('.training-scene-badge').count(),0);
  53. for(const width of [1920,1600,1366]) {
  54. await page.setViewportSize({width,height:945});
  55. const hud=page.locator('[data-role="scenario-hud"]'),tools=page.locator('[data-role="model-commandbar"]');
  56. await expect(tools).toBeVisible();
  57. if(await hud.isVisible()){
  58. const h=await hud.boundingBox(),t=await tools.boundingBox();
  59. assert.ok(t.y>=h.y+h.height+7,`${pack.code} HUD/tool overlap at ${width}`);
  60. }
  61. if(width===1920)await page.screenshot({path:`${dir}/${pack.code}.png`});
  62. }
  63. await page.setViewportSize({width:1920,height:945});
  64. await page.evaluate(()=>window.__blockNextLecture=true);
  65. await page.locator('[data-action="teacher-speak"]').click();
  66. await expect(page.locator('[data-role="teacher-voice-status"]')).toHaveText('点击播放讲解');
  67. await expect(media).toBeVisible();
  68. await expect(page.locator('[data-action="toggle-teacher-media"]')).toHaveText('切换语音');
  69. await page.locator('[data-action="teacher-speak"]').click();
  70. await expect.poll(()=>media.evaluate(v=>v.currentTime)).toBeGreaterThan(.1);
  71. checks.push({task:pack.code,videoDefault:true,noPlaybackWhileLoading:true,autoplayRestrictionRetainsVideo:true,noHudOverlap:[1920,1600,1366],plays:attempts});
  72. console.log('PASS',pack.code);
  73. }
  74. await page.goto(`${origin}/teaching/virtual-training`);
  75. const publicWidget=await page.locator('[data-testid="global-digital-human-widget"]').getAttribute('src');
  76. const slug=new URL(publicWidget).pathname.split('/').at(-1);
  77. const publicProfile=await fetch(`${widgetOrigin}/open/v1/realtime/${slug}`).then(r=>r.json());
  78. const expectedCover=publicProfile.data.avatars[0].coverUrl;
  79. assert.match(expectedCover,/^\/avatars\/builtin\/.+\.png$/);
  80. await page.goto(`${widgetOrigin}/embed/${slug}?label=${encodeURIComponent('问数字教员')}`);
  81. const image=page.locator('.embed-launcher-avatar img');
  82. await expect(image).toBeVisible();
  83. await expect(image).toHaveAttribute('src',expectedCover,{timeout:30000});
  84. await expect.poll(()=>image.evaluate(e=>e.naturalWidth)).toBeGreaterThan(0);
  85. assert.equal(await page.locator('.embed-launcher-avatar i').count(),0);
  86. const launcher=page.locator('.embed-launcher');
  87. assert.equal(await launcher.evaluate(e=>getComputedStyle(e).boxShadow),'none');
  88. await launcher.hover();
  89. assert.equal(await launcher.evaluate(e=>getComputedStyle(e).boxShadow),'none');
  90. await page.screenshot({path:`${dir}/widget.png`});
  91. await launcher.click();
  92. await expect(page.locator('.embed-panel')).toBeVisible();
  93. assert.equal(await page.locator('.embed-panel').evaluate(e=>getComputedStyle(e).boxShadow),'none');
  94. checks.push({widget:widgetOrigin,avatarLoaded:true,avatar:expectedCover,greenDotRemoved:true,launcherAndPanelShadows:'none'});
  95. assert.deepEqual(errors,[]);
  96. fs.writeFileSync(`${dir}/result.json`,JSON.stringify({origin,delayed,checks,errors},null,2));
  97. console.log(JSON.stringify({passed:checks.length,delayed,errors}));
  98. }catch(error){
  99. await page?.screenshot({path:`${dir}/failure.png`}).catch(()=>{});
  100. fs.writeFileSync(`${dir}/failure.json`,JSON.stringify({message:error.message,checks,errors},null,2));
  101. throw error;
  102. }finally{await browser.close()}