import fs from 'node:fs/promises'; import path from 'node:path'; import crypto from 'node:crypto'; import assert from 'node:assert/strict'; import { chromium, expect } from '@playwright/test'; import { login } from './role-permissions-live.mjs'; const dir=path.resolve('reports/bridge-task-covers-20260915'); const files={ 'TASK-VIRTUAL-005':'bridge-pump-switch-v2.jpg', 'TASK-VIRTUAL-006':'bridge-pump-substitute-v3.jpg', 'TASK-VIRTUAL-007':'bridge-pump-replace-v3-overview.jpg', 'TASK-VIRTUAL-008':'bridge-balance-valve-swap-v3.jpg', }; const signed=await login('root'),checks=[],errors=[],writes=[]; const browser=await chromium.launch({channel:'msedge',headless:true}); 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:'military'}));},signed); 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(r.url());}); try{ for(const origin of ['http://127.0.0.1:6180',process.env.LAN_URL].filter(Boolean)){ const label=origin.includes('127.0.0.1')?'local':'lan'; await page.goto(origin+'/teaching/virtual-training'); await expect(page.locator('.ttc-assignment-card')).toHaveCount(4,{timeout:30000}); for(const [code,file] of Object.entries(files)){ const card=page.getByTestId('teaching-assignment-'+code),image=card.locator('figure img'); await expect(image).toHaveAttribute('src','/models/bridge-training/'+file); await image.scrollIntoViewIfNeeded(); await expect.poll(()=>image.evaluate(i=>i.complete && i.naturalWidth===1200 && i.naturalHeight===1000)).toBe(true); await expect(card.locator('.ttc-capability-tags')).toHaveCount(0); await expect(card.locator('.ttc-card-body > p')).toHaveCount(0); await expect(card.locator('.ttc-card-body > header small')).toHaveCount(0); const response=await page.request.get(origin+'/models/bridge-training/'+file); assert.equal(response.status(),200); const bytes=await response.body(),source=await fs.readFile(path.resolve('../unreal_tran_web/public/models/bridge-training',file)); assert.ok(bytes.equals(source)); checks.push({origin,code,file,bytes:bytes.length,sha256:crypto.createHash('sha256').update(bytes).digest('hex')}); } for(const width of [1920,1440]){ await page.setViewportSize({width,height:1080}); await page.locator('.ttc-assignment-card').first().scrollIntoViewIfNeeded(); await page.screenshot({path:path.join(dir,`${label}-list-${width}.png`),fullPage:true}); assert.ok(await page.evaluate(()=>document.documentElement.scrollWidth<=innerWidth)); } } assert.deepEqual(errors,[]);assert.deepEqual(writes,[]); await fs.writeFile(path.join(dir,'verification.json'),JSON.stringify({pass:true,checks,errors,businessWrites:writes},null,2)); const frames=JSON.parse(await fs.readFile(path.join(dir,'frames.json'),'utf8')); await fs.writeFile(path.join(dir,'index.html'),`四套桥梁虚拟仿真场景封面

四套桥梁虚拟仿真 · 场景封面

原列表四条任务没有配置封面,统一回退至 vehicle-closeup-thumb.png。现从正式发布的三维预览取景,沿用对应脚本动作,保存为独立静态图片;按脚本标识和版本匹配,任务自行配置的封面仍优先。拍摄时隐藏界面浮层和场景文字牌,没有修改任务定义、训练记录或成绩。

006 的软管工位和 008 的平衡阀工位属于当前训练的操作示意;007 泵体与四颗安装螺栓来自整车模型。封面反映项目现有模型能力。本机/局域网加载及零业务写入核验 · 原场景边界、相机与取景步骤

${frames.map(f=>`
${f.name}
${f.code} · 内容版本 ${f.contentVersionId} · ${f.composition.step < 0 ? '开场整车视角' : `第 ${f.composition.step+1} 步动作后`}

${f.name}

${f.composition.subject}

`).join('')}

任务列表实际效果

1920宽度任务列表
`); console.log(`PASS: ${checks.length} cover loads, four distinct published scripts, desktop layouts, no business writes`); }finally{await browser.close();}