import fs from 'node:fs' import path from 'node:path' import { chromium, expect } from '@playwright/test' import { login, call } from './role-permissions-live.mjs' const report=path.resolve('reports/guide-formal-20260910'),work=path.resolve('../../.codex-tmp/guide-formal') fs.mkdirSync(report,{recursive:true});fs.mkdirSync(work,{recursive:true}) const phase=process.argv[2]||'create', fixturePath=path.join(work,'fixture.json') const fixture=fs.existsSync(fixturePath)?JSON.parse(fs.readFileSync(fixturePath)):{} const session=await login('root'),browser=await chromium.launch({channel:'msedge',headless:true,args:['--enable-unsafe-swiftshader']}) const context=await browser.newContext({viewport:{width:1920,height:1080}}) await context.addInitScript(s=>{sessionStorage.setItem('unreal-tran:web:access-token:v1',s.accessToken);sessionStorage.setItem('unreal-tran:web:refresh-token:v1',s.refreshToken)},session) const page=await context.newPage(),checks=[],errors=[] page.on('pageerror',e=>errors.push(e.message)) page.on('response',r=>{if(r.status()>=400) console.log('HTTP',r.status(),new URL(r.url()).pathname)}) await page.addInitScript(()=>{new MutationObserver(()=>{for(const el of document.querySelectorAll('.ofd-toast,.el-message'))if(!el.dataset.reported){el.dataset.reported='true';console.log('NOTICE',el.textContent)}}).observe(document,{childList:true,subtree:true})}) page.on('console',m=>{if(m.text().startsWith('NOTICE'))console.log(m.text())}) const shot=async name=>page.screenshot({path:path.join(report,name+'.png')}) const check=(name,pass,detail='')=>{checks.push({name,pass,detail});console.log(pass?'PASS':'FAIL',name);if(!pass)process.exitCode=1} try{ if(phase==='create'){ await page.goto('http://127.0.0.1:6180/content/ofd') await page.getByRole('button',{name:'新建指导书',exact:true}).click() await page.locator('[data-wizard="name"]').fill('正式闭环-狐狸与卡车指导书-20260910') await page.locator('[data-wizard="equipment"]').fill('狐狸与牛奶卡车功能测试素材') await page.locator('[data-wizard="owner"]').fill('功能闭环测试') await page.locator('[data-action="wizard-next"]').click() await page.locator('[data-action="wizard-template"][data-template="__blank__"]').click() await page.locator('[data-action="wizard-next"]').click() const response=page.waitForResponse(r=>r.url().endsWith('/content/projects')&&r.request().method()==='POST') await page.locator('[data-action="wizard-next"]').click() const created=await response;check('新建请求实际进入内容服务',created.status()===200,await created.text()) await page.waitForURL(/\/ofd\/\d+\/studio/) fixture.id=page.url().match(/\/ofd\/(\d+)\/studio/)[1];fs.writeFileSync(fixturePath,JSON.stringify(fixture)) const server=await call(`tran/v1/content/projects/${fixture.id}`,session.accessToken) check('正式后台可查询新工程',server.status===200) await shot('01-formal-created') } if(phase==='publish'){ await page.goto(`http://127.0.0.1:6180/content/ofd/${fixture.id}/studio?view=editor`) await expect(page.locator('.ofd-view-editor')).toBeVisible({timeout:60000}) await page.locator('[data-action="save"]').click();await page.waitForTimeout(700) await page.locator('[data-action="open-publish"]').click() const response=page.waitForResponse(r=>r.url().endsWith('/publish')&&r.request().method()==='POST',{timeout:600000}) await page.locator('[data-action="lifecycle-action"][data-lifecycle-action="publish"]').click() const result=await response;check('正式发布事务完成',result.ok(),(await result.text()).slice(0,400)) await page.waitForTimeout(700);await shot('04-formal-published') const guides=await call('tran/v1/teaching/guides?keyword='+encodeURIComponent('正式闭环-狐狸与卡车指导书-20260910'),session.accessToken) check('教学指导书列表出现正式发布工程',guides.data?.records?.some(r=>r.projectId===fixture.id)) const assets=await call(`tran/v1/teaching/guides/${fixture.id}/deliveries`,session.accessToken) check('发布版本提供 OFD 与离线 ZIP',assets.data?.length===2) for(const format of ['OFD','OFFLINE_HTML']){ const result=await context.request.get(`http://127.0.0.1:6180/api/tran/v1/teaching/guides/${fixture.id}/delivery?format=${format}`,{headers:{Authorization:`Bearer ${session.accessToken}`},timeout:180000}) check('下载正式交付物 '+format,result.ok());if(result.ok())fs.writeFileSync(path.join(report,format==='OFD'?'formal.ofd':'formal-offline.zip'),await result.body()) } } if(phase==='upload'){ await page.goto(`http://127.0.0.1:6180/content/ofd/${fixture.id}/studio?view=editor`) await expect(page.locator('.ofd-view-editor')).toBeVisible({timeout:60000}) for(const name of ['09-Fox.glb','08-CesiumMilkTruck.glb']){ const wait=page.waitForResponse(r=>r.url().includes('/assets/upload')&&r.request().method()==='POST') await page.locator('#ofd-resource-file').setInputFiles(path.resolve('../../文档/素材',name)) const response=await wait;check('上传 '+name,response.ok(),(await response.text()).slice(0,300)) await page.waitForTimeout(1800) } await page.locator('[data-action="save"]').click();await page.waitForTimeout(1200) await shot('02-formal-assets') await page.reload();await expect(page.locator('.ofd-view-editor')).toBeVisible({timeout:60000}) await page.locator('[data-action="select-step"]').first().click() await page.locator('[data-action="right-dock-tab"]').filter({hasText:'资源'}).click() await page.locator('[data-action="library-insert"][data-kind="model"]').filter({hasText:'Fox'}).first().click() await page.locator('[data-action="media-open"]').first().click() await expect(page.locator('.ofd-media-panel canvas')).toBeVisible({timeout:30000}) await page.waitForTimeout(1200);await shot('03-formal-fox') check('刷新后受控模型资源恢复显示',true) } }catch(e){check(phase,false,e.message);console.log(await page.locator('.legacy-guide-page').evaluate(el=>el.__vueParentComponent?.setupState?.mountError).catch(()=>''));await shot(phase+'-failure').catch(()=>{})} finally{fs.writeFileSync(path.join(report,phase+'.json'),JSON.stringify({checks,errors},null,2));await browser.close()}