import fs from 'node:fs'; import path from 'node:path'; import assert from 'node:assert/strict'; import {chromium,expect} from '@playwright/test'; import {login,call} from './role-permissions-live.mjs'; const dir='reports/bridge-content-authoring-20260924'; const fixture=JSON.parse(fs.readFileSync(dir+'/fixture.json','utf8')); const signed=await login('root'),checks=[],errors=[]; const api=async ep=>{const r=await call('tran/v1/'+ep,signed.accessToken);assert.equal(r.status,200,ep);return r.data;}; const browser=await chromium.launch({channel:'msedge',headless:true,args:['--enable-unsafe-swiftshader']}); const page=await browser.newPage({viewport:{width:Number(process.env.VIEW_WIDTH)||1440,height:Number(process.env.VIEW_HEIGHT)||1000}}); page.on('pageerror',e=>errors.push(e.message)); await page.addInitScript(({s,theme})=>{sessionStorage.setItem('unreal-tran:web:access-token:v1',s.accessToken);sessionStorage.setItem('unreal-tran:web:refresh-token:v1',s.refreshToken);if(theme)localStorage.setItem('unreal-tran:web:preferences:v1',JSON.stringify({themeMode:theme}));},{s:signed,theme:process.env.THEME}); const base=process.env.BASE_URL||'http://127.0.0.1:6180'; const screenshot=name=>page.screenshot({path:`${dir}/${process.env.IMAGE_PREFIX||''}${name}.png`}); const ready=async()=>{await expect(page.getByRole('button',{name:'播放动作',exact:true})).toBeEnabled({timeout:120000});}; try{ for(const item of process.env.FLOW_ONLY==='1'?[]:fixture.items){ await page.goto(base+'/content/models/'+item.modelProjectId+'/production');await ready(); await expect(page.locator('.production-action-item')).toHaveCount(item.actions); const dimensions=await page.locator('.production-preview').evaluate(host=>({width:host.clientWidth,canvas:host.querySelector('canvas').clientWidth,height:host.clientHeight})); assert.ok(Math.abs(dimensions.width-dimensions.canvas)<5,JSON.stringify(dimensions)); await screenshot(`${item.taskCode}-model`); const candidates=item.taskCode.endsWith('005')?[1,2,4,5]:item.taskCode.endsWith('006')?[2,4,8]:item.taskCode.endsWith('007')?[3,7,11]:[2,5,35]; for(const index of candidates){ await page.locator('.production-action-item').nth(index).click(); await page.getByRole('button',{name:'播放动作',exact:true}).click();await ready(); await screenshot(`${item.taskCode}-action-${index+1}`); } checks.push({task:item.taskCode,stage:'model',actions:item.actions,played:candidates.map(i=>i+1),pass:true}); await page.locator('.production-chain button').nth(1).click();await ready(); await expect(page.locator('.production-object')).toHaveCount(item.taskCode.endsWith('005')?2:item.taskCode.endsWith('008')?3:4); await screenshot(`${item.taskCode}-scene`); checks.push({task:item.taskCode,stage:'scene',pass:true}); await page.locator('.production-chain button').nth(2).click();await ready(); await page.getByRole('tab',{name:'背景与讲解'}).click(); await screenshot(`${item.taskCode}-training`); checks.push({task:item.taskCode,stage:'training',pass:true}); if(process.env.CAPTURE_ONLY==='1')break; } if(process.env.CAPTURE_ONLY!=='1'){ const item=fixture.items[0]; await page.goto(base+'/content/models/'+item.modelProjectId+'/production');await ready(); await page.locator('.production-action-item').nth(2).click(); const angle=page.locator('.el-form-item').filter({hasText:'旋转角度(°)'}).getByRole('spinbutton'); await angle.fill('75');await angle.press('Tab'); await page.getByRole('button',{name:'播放动作',exact:true}).click();await ready(); await page.getByRole('button',{name:'保存',exact:true}).click(); await expect(page.getByText('已保存,可刷新继续编辑')).toBeVisible(); await page.reload();await ready();await page.locator('.production-action-item').nth(2).click();await expect(angle).toHaveValue('75'); const changed=await api('content/projects/'+item.modelProjectId);assert.equal(changed.currentVersion.content.productionActions[2].motion.angle,75); await angle.fill('90');await angle.press('Tab'); await page.getByRole('button',{name:'发布并进入场景'}).click();await expect(page).toHaveURL(new RegExp('/scenes/'+item.sceneProjectId+'/production'));await ready(); await page.getByRole('button',{name:'发布并进入编排'}).click();await expect(page).toHaveURL(new RegExp('/training-projects/'+item.trainingProjectId+'/production'));await ready(); await page.getByRole('button',{name:'发布训练内容'}).click();await expect(page.getByText('训练内容已发布;现有仿真仍保持固定版本')).toBeVisible(); checks.push({name:'005 90→75 保存刷新→90 发布模型→场景→编排',pass:true}); const model=await api('content/projects/'+item.modelProjectId),scene=await api('content/projects/'+item.sceneProjectId),training=await api('content/projects/'+item.trainingProjectId); assert.equal(scene.currentVersion.dependencies[0].targetVersionId,model.project.publishedVersionId); assert.equal(training.currentVersion.dependencies[0].targetVersionId,scene.project.publishedVersionId); assert.equal(training.currentVersion.content.steps[2].modelActions[0].motion.angle,90); assert.equal(training.currentVersion.content.steps[2].modelActions[0].modelVersionId,model.project.publishedVersionId); await page.locator('.production-chain button').nth(3).click(); await expect(page).toHaveURL(new RegExp('/virtual-training/preview/'+item.taskId)); await expect(page.getByRole('button',{name:'跳过讲解',exact:true})).toBeVisible({timeout:120000}); await screenshot('existing-005-preview'); checks.push({name:'制作链进入原005背景与讲解',pass:true}); for(const list of ['models','scenes','training-projects']){ await page.goto(base+'/content/'+list);await expect(page.getByText(/重型支援桥液压泵应急切换/).first()).toBeVisible({timeout:30000}); await screenshot(`list-${list}`); const row=page.locator(list==='training-projects'?'.training-project-card':'.asset-row-list article').filter({hasText:'重型支援桥液压泵应急切换'}).first(); await row.getByRole('button',{name:list==='training-projects'?'编辑':'进入编辑',exact:true}).click();await ready(); await expect(page).toHaveURL(/\/production$/); } for(const id of [86,87,88,89,116]){const before=JSON.parse(fs.readFileSync(`${dir}/before-task-${id}.json`,'utf8')),after=await api('teaching/assignments/'+id);delete before.serverNow;delete after.serverNow;assert.deepEqual(after,before);} checks.push({name:'原 005~008 及 005 独立入口固定定义完全未变',pass:true}); } assert.deepEqual(errors,[]); fs.writeFileSync(dir+'/'+(process.env.RESULT_FILE||'ui-result.json'),JSON.stringify({pass:true,base,checks,errors},null,2)); console.log(JSON.stringify({pass:true,checks:checks.length})); }catch(e){await page.screenshot({path:dir+'/failure.png'}).catch(()=>{});fs.writeFileSync(dir+'/failure.json',JSON.stringify({pass:false,message:e.message,checks,errors},null,2));throw e;}finally{await browser.close();}