import fs from 'node:fs'; import assert from 'node:assert/strict'; import {chromium, expect} from '@playwright/test'; import {login} from './role-permissions-live.mjs'; import videoManifest from '../../unreal_tran_web/src/features/virtual-training-scripts/narration-video-manifest.js'; const dir='reports/bridge-introduction-layout-20260920'; fs.mkdirSync(dir,{recursive:true}); const signed=await login('root'), checks=[], errors=[], writes=[]; const browser=await chromium.launch({channel:'msedge',headless:true,args:['--enable-unsafe-swiftshader']}); const page=await browser.newPage({viewport:{width:1920,height:1080}}); 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'})); },signed); 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.introLayoutState=()=>activeTraining?{step:activeTraining.session.stepIndex,completed:[...activeTraining.session.completed]}:null;'}); }); 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 state=()=>page.evaluate(()=>globalThis.introLayoutState?.()); const overlay=page.locator('.training-introduction'); try { for(const [id,pages,steps] of [[86,6,5],[87,3,7],[88,1,11],[89,1,16]]) { await page.goto(`http://127.0.0.1:6180/teaching/virtual-training/preview/${id}`); await expect(overlay).toBeVisible({timeout:120000}); await expect(page.locator('[data-intro-page]')).toHaveCount(pages); await expect(page.locator('[data-step-index]')).toHaveCount(steps); await expect(page.locator('[data-role="step-count"]')).toHaveText(`1 / ${pages+steps}`); await expect.poll(()=>overlay.locator('img').evaluateAll(images=>images.every(i=>i.complete&&i.naturalWidth>0)),{timeout:30000}).toBe(true); const original=await state(), writeCount=writes.length; await page.screenshot({path:`${dir}/${id}-desktop.png`}); for(const viewport of [{width:1366,height:768},{width:1280,height:720}]) { await page.setViewportSize(viewport); const layout=await page.evaluate(()=>{ const scroller=document.querySelector('.training-sequence-scroll'); const root=document.querySelector('[data-role="steps"]'); const content=document.querySelector('.training-introduction-content'); const image=document.querySelector('.introduction-visual'),text=document.querySelector('.introduction-reading'); const before=scroller.querySelector('[data-intro-page]').getBoundingClientRect().top; scroller.scrollTop=scroller.scrollHeight; return {scrollable:scroller.scrollHeight>scroller.clientHeight, introMoves:scroller.querySelector('[data-intro-page]').getBoundingClientRect().topcontent.clientWidth, imageTextOverlap:image&&text?image.getBoundingClientRect().right>text.getBoundingClientRect().left:false}; }); assert.ok(layout.scrollable);assert.ok(layout.introMoves); assert.equal(layout.stepsOverflow,'visible');assert.equal(layout.horizontalOverflow,false);assert.equal(layout.imageTextOverlap,false); await expect(page.locator('[data-step-index]').last()).toBeInViewport(); await expect(overlay.locator('[data-intro-command="skip"]')).toBeInViewport(); await expect(overlay.locator('[data-intro-command="next"]')).toBeInViewport(); await page.locator('.training-sequence-scroll').evaluate(el=>el.scrollTop=0); await page.screenshot({path:`${dir}/${id}-${viewport.width}.png`}); checks.push({id,viewport,...layout}); } for(let i=1;i1)await page.screenshot({path:`${dir}/${id}-lecture.png`}); await overlay.locator('[data-intro-command="next"]').click(); await expect(overlay).toBeHidden(); assert.deepEqual(await state(),original); await expect(page.locator('[data-step-index="0"]')).toBeInViewport(); await page.locator('[data-intro-page="0"]').click(); await expect(overlay).toBeVisible(); await overlay.locator('[data-intro-command="skip"]').click(); await expect(overlay).toBeHidden();assert.deepEqual(await state(),original); assert.equal(writes.length,writeCount); await page.setViewportSize({width:1920,height:1080}); console.log('PASS task',id); } // Browser-only response fixture validates optional media without changing any saved task. 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:{enabled:true,title:'纯文字背景',body:'没有背景图时,正文保持居中阅读。\n讲解切换不推进操作步骤。'},lectures:[{id:'video-check',title:'视频讲解',format:'video',videoUrl:videoManifest.scripts[0].steps[0].url}]}; await route.fulfill({response,json:body}); }); await page.goto('http://127.0.0.1:6180/teaching/virtual-training/preview/86'); await expect(overlay).toBeVisible({timeout:120000}); await expect(overlay.locator('.is-text-only')).toBeVisible(); await expect(overlay.locator('.introduction-reading p')).toHaveCount(2); await page.screenshot({path:`${dir}/text-only.png`}); await overlay.locator('[data-intro-command="next"]').click(); const video=overlay.locator('video');await expect(video).toBeVisible(); await video.evaluate(el=>el.play()); await expect.poll(()=>video.evaluate(el=>el.currentTime),{timeout:30000}).toBeGreaterThan(0); await page.screenshot({path:`${dir}/video.png`}); await page.evaluate(()=>globalThis.previousIntroVideo=document.querySelector('.training-introduction video')); await overlay.locator('[data-intro-command="skip"]').click(); assert.equal(await page.evaluate(()=>globalThis.previousIntroVideo.paused&&!globalThis.previousIntroVideo.getAttribute('src')),true); checks.push({optionalTextAndVideo:true,videoStopsOnSkip:true}); await page.unroute(/\/api\/tran\/v1\/teaching\/assignments\/86$/); await page.goto('http://127.0.0.1:6180/teaching/virtual-training/preview/86'); await expect(overlay).toBeVisible({timeout:120000}); await expect.poll(()=>overlay.locator('img').evaluateAll(images=>images.every(i=>i.complete&&i.naturalWidth>0))).toBe(true); for(const mode of ['dark','military']) { await page.evaluate(mode=>{document.documentElement.classList.toggle('dark',mode==='dark');document.documentElement.dataset.colorTheme=mode;document.documentElement.dataset.theme=mode==='dark'?'dark':'light';},mode); await page.screenshot({path:`${dir}/theme-${mode}.png`}); } const lan=await page.request.get('http://192.168.31.168:6180/api/auth/v1/system-config/public'); assert.equal(lan.status(),200);checks.push({lanGatewayProxy:true}); assert.deepEqual(errors,[]); fs.writeFileSync(`${dir}/result.json`,JSON.stringify({pass:true,checks,errors,writes,accountType:'internal maintenance preview',testDataCreated:false},null,2)); fs.writeFileSync(`${dir}/index.html`,`背景讲解布局验收

背景讲解布局验收

四个任务通过:统一滚动、图文分栏、小屏布局、翻页与跳过;操作进度保持不变。未写入任务数据。

${[86,87,88,89].map((id,i)=>`

TASK-VIRTUAL-00${i+5}

桌面截图 · 1280 × 720
`).join('')}`); } catch(error) { await page.screenshot({path:`${dir}/failure.png`}); fs.writeFileSync(`${dir}/failure.json`,JSON.stringify({error:error.stack,errors},null,2)); throw error; } finally {await browser.close();}