import { chromium, expect } from '@playwright/test'; import assert from 'node:assert/strict'; import fs from 'node:fs'; import { credentials } from './role-permissions-live.mjs'; import videos from '../../unreal_tran_web/src/features/virtual-training-scripts/narration-video-manifest.js'; const origin=process.env.BASE_URL || 'http://127.0.0.1:6180'; const widgetOrigin=process.env.WIDGET_URL || 'http://127.0.0.1:8003'; const dir=process.env.REPORT_DIR || 'reports/bridge-lecture-polish-20260915/local'; fs.mkdirSync(dir,{recursive:true}); 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()); assert.ok(auth.data?.accessToken,'Login'); 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()); const browser=await chromium.launch({channel:'msedge',headless:true,args:['--enable-unsafe-swiftshader','--autoplay-policy=no-user-gesture-required']}); const previous=process.env.WIDGET_ONLY === '1' ? JSON.parse(fs.readFileSync(fs.existsSync(`${dir}/result.json`)?`${dir}/result.json`:`${dir}/failure.json`)) : null; const checks=previous?.checks.filter(c=>c.task) || [], errors=[]; let page; try { page=await browser.newPage({viewport:{width:1920,height:945}}); await page.addInitScript(token=>{ sessionStorage.setItem('unreal-tran:web:access-token:v1',token); localStorage.setItem('unreal-tran:web:preferences:v1',JSON.stringify({themeMode:'military'})); window.__lecturePlays=[]; const play=HTMLMediaElement.prototype.play; HTMLMediaElement.prototype.play=function(){ if(this.classList.contains('training-server-video')) { const visible=e=>{const s=getComputedStyle(e);return e.getClientRects().length>0&&s.visibility!=='hidden'&&s.display!=='none'&&s.opacity!=='0'}; 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}); if(window.__blockNextLecture){window.__blockNextLecture=false;return Promise.reject(new DOMException('User gesture required','NotAllowedError'))} } return play.apply(this,arguments); }; },auth.data.accessToken); page.on('pageerror',e=>errors.push(e.message)); // Deliberately delay model and decoder responses without replacing their data. let delayed=previous?.delayed || 0; await page.route(/(?:\.glb(?:\?|$)|draco.*\.(?:wasm|js)(?:\?|$)|\/assets\/[^/]+\/download)/,async route=>{ delayed++; await new Promise(resolve=>setTimeout(resolve,1500)); await route.continue(); }); for(const pack of process.env.WIDGET_ONLY === '1' ? [] : videos.scripts){ const task=listing.data.records.find(t=>t.code===pack.code);assert.ok(task,pack.code); await page.goto(`${origin}/teaching/virtual-training/preview/${task.id}`); await expect(page.locator('[data-role="loading"]')).toHaveClass(/is-hidden/,{timeout:150000}); const media=page.locator('.training-server-video'); await expect(media).toBeVisible(); await expect(page.locator('[data-action="toggle-teacher-media"]')).toHaveText('切换语音'); await expect.poll(()=>media.evaluate(v=>v.currentTime),{timeout:20000}).toBeGreaterThan(.1); const attempts=await page.evaluate(()=>window.__lecturePlays); assert.ok(attempts.length>0);assert.ok(attempts.every(p=>!p.loading),'Narration must wait for visible loading overlays to finish'); assert.equal(attempts[0].src,pack.steps[0].url); assert.equal(await page.locator('.training-scene-badge').count(),0); for(const width of [1920,1600,1366]) { await page.setViewportSize({width,height:945}); const hud=page.locator('[data-role="scenario-hud"]'),tools=page.locator('[data-role="model-commandbar"]'); await expect(tools).toBeVisible(); if(await hud.isVisible()){ const h=await hud.boundingBox(),t=await tools.boundingBox(); assert.ok(t.y>=h.y+h.height+7,`${pack.code} HUD/tool overlap at ${width}`); } if(width===1920)await page.screenshot({path:`${dir}/${pack.code}.png`}); } await page.setViewportSize({width:1920,height:945}); await page.evaluate(()=>window.__blockNextLecture=true); await page.locator('[data-action="teacher-speak"]').click(); await expect(page.locator('[data-role="teacher-voice-status"]')).toHaveText('点击播放讲解'); await expect(media).toBeVisible(); await expect(page.locator('[data-action="toggle-teacher-media"]')).toHaveText('切换语音'); await page.locator('[data-action="teacher-speak"]').click(); await expect.poll(()=>media.evaluate(v=>v.currentTime)).toBeGreaterThan(.1); checks.push({task:pack.code,videoDefault:true,noPlaybackWhileLoading:true,autoplayRestrictionRetainsVideo:true,noHudOverlap:[1920,1600,1366],plays:attempts}); console.log('PASS',pack.code); } await page.goto(`${origin}/teaching/virtual-training`); const publicWidget=await page.locator('[data-testid="global-digital-human-widget"]').getAttribute('src'); const slug=new URL(publicWidget).pathname.split('/').at(-1); const publicProfile=await fetch(`${widgetOrigin}/open/v1/realtime/${slug}`).then(r=>r.json()); const expectedCover=publicProfile.data.avatars[0].coverUrl; assert.match(expectedCover,/^\/avatars\/builtin\/.+\.png$/); await page.goto(`${widgetOrigin}/embed/${slug}?label=${encodeURIComponent('问数字教员')}`); const image=page.locator('.embed-launcher-avatar img'); await expect(image).toBeVisible(); await expect(image).toHaveAttribute('src',expectedCover,{timeout:30000}); await expect.poll(()=>image.evaluate(e=>e.naturalWidth)).toBeGreaterThan(0); assert.equal(await page.locator('.embed-launcher-avatar i').count(),0); const launcher=page.locator('.embed-launcher'); assert.equal(await launcher.evaluate(e=>getComputedStyle(e).boxShadow),'none'); await launcher.hover(); assert.equal(await launcher.evaluate(e=>getComputedStyle(e).boxShadow),'none'); await page.screenshot({path:`${dir}/widget.png`}); await launcher.click(); await expect(page.locator('.embed-panel')).toBeVisible(); assert.equal(await page.locator('.embed-panel').evaluate(e=>getComputedStyle(e).boxShadow),'none'); checks.push({widget:widgetOrigin,avatarLoaded:true,avatar:expectedCover,greenDotRemoved:true,launcherAndPanelShadows:'none'}); assert.deepEqual(errors,[]); fs.writeFileSync(`${dir}/result.json`,JSON.stringify({origin,delayed,checks,errors},null,2)); console.log(JSON.stringify({passed:checks.length,delayed,errors})); }catch(error){ await page?.screenshot({path:`${dir}/failure.png`}).catch(()=>{}); fs.writeFileSync(`${dir}/failure.json`,JSON.stringify({message:error.message,checks,errors},null,2)); throw error; }finally{await browser.close()}