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 report='reports/virtual-html-workspace-20260912',session=await login('root'),checks=[],errors=[]; const reuse=process.argv.includes('--reuse')?JSON.parse(fs.readFileSync(report+'/media-checks.json','utf8')).checks:[]; const api=async(endpoint,body)=>{const r=await call(endpoint,session.accessToken,body);assert.equal(r.status,200,r.message);return r.data;}; const browser=await chromium.launch({channel:'msedge',headless:true,args:['--enable-unsafe-swiftshader']}); const context=await browser.newContext({viewport:{width:1440,height:900}}); await context.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:'military'}));},session); const page=await context.newPage();page.on('pageerror',e=>errors.push(e.message)); try { for(const id of [161,411]) { const source=await api('tran/v1/content/projects/'+id),base=id===411?await api('tran/v1/content/projects/419'):source,content=base.currentVersion.content,suffix=Date.now().toString(36); if(id===411)content.steps[0].audio=source.currentVersion.content.steps[0].audio; content.trainingMode='VIRTUAL';content.enabledChannels=['VIRTUAL']; content.channelProfiles={VIRTUAL:{...(content.channelProfiles?.PHYSICAL||content.modeConfig),type:'VIRTUAL'}}; content.modeConfig=content.channelProfiles.VIRTUAL; let p=source.project; const previous=reuse[id===161?0:1]?.assignmentId; if(id!==161&&!previous) { const created=await api('tran/v1/content/projects',{type:'TRAINING',name:`HTML媒体验收-${id}-${suffix}`,categoryCode:'MAINTENANCE',trainingMode:'VIRTUAL',content,dependencies:base.currentVersion.dependencies,assets:source.currentVersion.assets.filter(a=>a.code!=='TRAINING_COVER')}); await api(`tran/v1/content/projects/${created.project.id}/publish`,{version:created.project.version}); p=(await api(`tran/v1/content/projects/${created.project.id}`)).project; } const task=previous?await api('tran/v1/teaching/assignments/'+previous):await api('tran/v1/teaching/assignments',{code:'HTML-MEDIA-'+suffix,name:p.name,channel:'VIRTUAL',assignmentKind:'TRAINING',executionMode:'PRACTICE',audienceType:'COMMON',collaborationMode:'INDIVIDUAL',digitalHumanAllowed:true,contentProjectId:p.id,contentVersionId:p.publishedVersionId,members:[]}); await page.goto(`http://127.0.0.1:6180/teaching/virtual-training/preview/${task.id}`); await page.locator('.training-shell').waitFor({timeout:120000}); await expect.poll(async()=>Boolean(await page.evaluate(()=>globalThis.__trainingSceneDiagnostics?.()?.publishedContentMounted)),{timeout:90000}).toBe(true); await page.waitForTimeout(3000); if(id===161) { const d=await page.evaluate(()=>globalThis.__trainingSceneDiagnostics()); const clips=d.server.animations.flatMap(a=>a.native); assert.equal(d.server.animations.length,5,'All five published scene objects, including native lights and equipment, must load'); assert.ok(clips.some(c=>c.name==='Run'),'Fox GLB and its native clips must load'); await expect(page.locator('[data-role="model-commandbar"]')).toBeVisible(); await page.locator('.training-server-preparation summary').click(); const overflow=await page.evaluate(()=>document.documentElement.scrollWidth>innerWidth); assert.equal(overflow,false,'Small screen must not have page horizontal overflow'); await page.locator('.training-server-preparation summary').click(); await expect(page.locator('[data-role="teacher-image"]')).toBeVisible(); checks.push({name:'狐狸历史固定场景与 GLB 原生动画资源加载;1440×900 布局',pass:true,assignmentId:task.id,clips}); } else { const play=page.locator('[data-action="teacher-speak"]'); await play.click(); const video=page.locator('video.training-server-video');await expect(video).toBeVisible(); await expect.poll(()=>video.evaluate(v=>v.currentTime),{timeout:30000}).toBeGreaterThan(0); const media=await video.evaluate(v=>({duration:v.duration,readyState:v.readyState,authorizedBlob:v.currentSrc.startsWith('blob:')})); assert.equal(media.authorizedBlob,true); checks.push({name:'数字人服务成片通过任务授权资源播放',pass:true,assignmentId:task.id,media}); } await page.screenshot({path:`${report}/${id===161?'06-fox-native':'07-digital-human-video'}.png`,fullPage:true}); console.log('PASS',checks.at(-1).name); } assert.equal(errors.length,0,errors.join('\n')); }catch(error){checks.push({name:'媒体与小屏验证',pass:false,error:error.message});console.error(error);process.exitCode=1;await page.screenshot({path:report+'/media-failure.png',fullPage:true});} finally{fs.writeFileSync(report+'/media-checks.json',JSON.stringify({checks,errors},null,2));await browser.close();}