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') const fixture=JSON.parse(fs.readFileSync(path.join(work,'fixture.json'))),checks=[],errors=[] const session=await login('root'),token=session.accessToken const check=(name,pass,detail='')=>{checks.push({name,pass,detail});console.log(pass?'PASS':'FAIL',name);if(!pass)process.exitCode=1} const base=`tran/v1/teaching/guides/${fixture.id}` const 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();page.on('pageerror',e=>errors.push(e.message)) 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())}) page.on('response',r=>{if(r.status()>=400)console.log('HTTP',r.status(),new URL(r.url()).pathname)}) const shot=async name=>page.screenshot({path:path.join(report,name+'.png'),fullPage:false}) try{ const assets=await call(`${base}/deliveries`,token);check('发布版本提供两份正式交付物',assets.data?.length===2) for(const format of (process.argv.includes('--browser-only')?[]:['OFD','OFFLINE_HTML'])){const r=await context.request.get(`http://127.0.0.1:6180/api/${base}/delivery?format=${format}`,{headers:{Authorization:`Bearer ${token}`},timeout:180000});check('统一文件服务交付物下载 '+format,r.ok());if(r.ok())fs.writeFileSync(path.join(report,format==='OFD'?'formal.ofd':'formal-offline.zip'),await r.body())} const before=(await call(`${base}/reader-state`,token)).data const command={...before,pageId:'page:1',pageIndex:1,pageCount:3,favorite:true,notes:[{id:'formal-note',pageId:'page:1',text:'正式版本笔记:核对狐狸模型与卡车资源,注意安全检查。'}]} const saved=await call(`${base}/reader-state`,token,command,'PUT');check('阅读位置、收藏、笔记真实写入',saved.status===200&&saved.data.favorite&&saved.data.notes.length===1) const stale=await call(`${base}/reader-state`,token,command,'PUT');check('旧乐观锁版本被拒绝',stale.status===409) const release=await call(`${base}/reader-state`,token,{...command,versionId:'1'},'PUT');check('旧发布版本不能覆盖当前阅读记录',release.status===409) const stats=await call(`${base}/reader-statistics`,token);check('收藏和笔记进入真实统计',stats.data?.favorites>=1&&stats.data?.notes>=1) await page.goto('http://127.0.0.1:6180/teaching/guides') await page.getByPlaceholder('搜索指导书名称或编码').fill('正式闭环-狐狸与卡车指导书-20260910') await page.getByPlaceholder('搜索指导书名称或编码').press('Enter') await expect(page.locator('.guide-card-v2')).toHaveCount(1) await page.locator('.guide-card-v2 button').first().click() await expect(page.locator('.legacy-published-reader .ofd-app')).toBeVisible({timeout:60000}) await expect(page.getByRole('button',{name:'取消收藏',exact:true})).toBeVisible() await page.getByRole('button',{name:/笔记 1/}).click() await expect(page.locator('.reader-note')).toContainText('正式版本笔记') await shot('05-reader-notes') await page.getByPlaceholder('记录当前页的阅读笔记').fill('浏览器实测新增笔记:页码和内容由正式服务保存。') const noteResponse=page.waitForResponse(r=>r.url().endsWith('/reader-state')&&r.request().method()==='PUT') await page.getByRole('button',{name:'添加笔记',exact:true}).click() check('浏览器笔记保存请求成功',(await noteResponse).ok()) await page.locator('.el-drawer__close-btn:visible').click() await page.locator('.legacy-published-reader [data-action="preview-next"]').click() await page.waitForTimeout(1300);await shot('06-reader-layout') const persisted=await call(`${base}/reader-state`,token);check('浏览器翻页位置已同步',persisted.data?.pageIndex===2) await page.locator('.legacy-published-reader [data-action="preview-tab"][data-tab="interactive"]').click() await page.waitForTimeout(3500);await shot('07-reader-interactive') await page.locator('.legacy-published-reader canvas').first().scrollIntoViewIfNeeded() await page.waitForTimeout(1200);await shot('09-reader-model') check('教学动态阅读显示三维画布',await page.locator('.legacy-published-reader canvas').count()>0) await page.goto('http://127.0.0.1:6180/content/ofd/api') await page.getByRole('button',{name:'执行调用',exact:true}).click() await expect(page.locator('.guide-http-page pre')).toContainText('records') await shot('08-http-integration') check('对外接口面板返回真实工程数据',await page.locator('.guide-http-page pre').textContent().then(t=>t.includes('GUIDE-'))) }catch(e){check('浏览器闭环流程',false,e.message);await shot('failure-review')} finally{check('浏览器无脚本异常',errors.length===0,errors.join('\n'));fs.writeFileSync(path.join(report,'review.json'),JSON.stringify({checks,errors},null,2));await browser.close()}