|
- 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 report = path.resolve('reports/guide-closure-20260910')
- const work = path.resolve('../../.codex-tmp/guide-closure-20260910')
- fs.mkdirSync(report, { recursive:true }); fs.mkdirSync(work, { recursive:true })
- const phase = process.argv[2] || 'create'
- const fixtureFile = path.join(work, 'fixture.json')
- const fixture = fs.existsSync(fixtureFile) ? JSON.parse(fs.readFileSync(fixtureFile, 'utf8')) : {}
- const checks = [], errors = [], requests = []
- const browser = await chromium.launch({ channel:'msedge', headless:true, args:['--enable-unsafe-swiftshader'] })
- const session = await login(phase === 'reader' ? 'admin' : 'root')
- const stateFile = path.join(work, 'browser.json')
- const context = await browser.newContext({ viewport:{ width:1920, height:1080 }, ...(fs.existsSync(stateFile) ? { storageState:stateFile } : {}) })
- await context.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' }))
- }, session)
- const page = await context.newPage()
- page.on('pageerror', e => errors.push(e.message))
- page.on('request', r => { if (r.url().includes('/api/tran/')) requests.push({ method:r.method(), path:new URL(r.url()).pathname }) })
- const check = (name, pass, detail='') => { checks.push({ name, status:pass ? 'PASS':'FAIL', detail }); console.log(`${pass ? 'PASS':'FAIL'} ${name}`) }
- const shot = async name => { await page.screenshot({ path:path.join(report, `${name}.png`), animations:'disabled' }) }
- const open = async url => { await page.goto(`http://127.0.0.1:6180${url}`); await expect(page.locator('.legacy-guide-page')).toBeVisible({ timeout:60000 }) }
- const state = async () => page.evaluate(async id => (await import('/src/features/guide-legacy/runtime.ts')).legacyGuideContentStore.getOFDProject(id)?.state, fixture.id)
- try {
- if (phase === 'create') {
- await open('/content/ofd')
- await page.getByRole('button', { name:'新建指导书', exact:true }).click()
- await page.locator('[data-wizard="name"]').fill('闭环实测-狐狸巡检作业指导-20260910')
- await page.locator('[data-wizard="code"]').fill('REVIEW-GUIDE-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()
- await shot('01-create-wizard')
- await page.locator('[data-action="wizard-next"]').click()
- await page.waitForURL(/\/content\/ofd\/[^/]+\/studio\?view=analysis/)
- fixture.id = page.url().match(/\/ofd\/([^/]+)\/studio/)[1]
- fs.writeFileSync(fixtureFile, JSON.stringify(fixture))
- await expect(page.locator('.ofd-view-analysis')).toBeVisible()
- const saved = await state()
- check('通过界面从空白模板创建指导书', saved?.project?.name.includes('闭环实测') && saved.chapters.length === 1)
- check('创建调用正式内容接口', requests.some(r=>r.method==='POST' && r.path.endsWith('/content/projects')), '当前页面使用浏览器本地工程库')
- const server = await call(`tran/v1/content/projects?type=GUIDE&keyword=${encodeURIComponent('闭环实测-狐狸巡检作业指导-20260910')}`, session.accessToken)
- check('新建工程出现在正式后端列表', server.status===200 && server.data.records.some(r=>r.name.includes('闭环实测-狐狸')))
- await shot('02-planning')
- await page.locator('[data-action="goto"][data-view="editor"]').first().click()
- await expect(page.locator('.ofd-view-editor')).toBeVisible()
- await shot('03-editor-baseline')
- }
- if (phase === 'inspect') {
- await open(`/content/ofd/${fixture.id}/studio?view=editor`)
- await expect(page.locator('.ofd-view-editor')).toBeVisible()
- const dom = await page.locator('.ofd-app').evaluate(el=>({
- actions:[...el.querySelectorAll('[data-action]')].map(x=>({action:x.dataset.action,text:(x.innerText||x.getAttribute('title')||'').slice(0,60),view:x.dataset.view,field:x.dataset.field})),
- fields:[...el.querySelectorAll('input,textarea,select')].map(x=>({field:x.dataset.field,stepField:x.dataset.stepField,projectField:x.dataset.projectField,placeholder:x.getAttribute('placeholder'),type:x.type,value:x.type==='password'?'':x.value.slice(0,80)})),
- }))
- fs.writeFileSync(path.join(work,'dom.json'),JSON.stringify(dom,null,2))
- console.log(JSON.stringify(dom,null,2))
- }
- if (phase === 'edit') {
- await open(`/content/ofd/${fixture.id}/studio?view=editor`)
- const first = (await state()).chapters[0].steps[0].id
- fixture.firstStep = first
- for (const [field,value] of Object.entries({title:'狐狸巡检目标识别',summary:'定位狐狸模型,观察目标外观,并逐项记录本次巡检结果。',tools:'测试模型、观察记录表',safety:'仅使用测试模型,不作为真实装备维修规程。',acceptance:'模型显示完整,能够识别并记录目标。'})) {
- await page.locator(`[data-step-field="${field}"]`).fill(value)
- await page.locator(`[data-step-field="${field}"]`).blur()
- }
- await page.locator('[data-action="save"]').click()
- await expect.poll(async()=> (await state()).chapters[0].steps[0].title).toBe('狐狸巡检目标识别')
- check('步骤标题、说明、工具、安全、验收编辑保存', (await state()).chapters[0].steps[0].summary.includes('逐项记录'))
- await page.reload(); await expect(page.locator('[data-step-field="title"]')).toHaveValue('狐狸巡检目标识别')
- check('刷新后恢复已保存的本地内容', true)
- const before = (await state()).chapters.flatMap(c=>c.steps).length
- await page.locator('[data-action="add-step"]').click()
- await page.locator('[data-step-field="title"]').fill('牛奶卡车状态检查')
- await page.locator('[data-step-field="title"]').blur()
- await page.locator('[data-action="save"]').click()
- check('新增步骤后保存', (await state()).chapters.flatMap(c=>c.steps).length===before+1)
- await page.locator('[data-action="page-add-for-step"]').last().click()
- await page.locator('[data-action="save"]').click()
- const pages=(await state()).documentPages.length
- await page.locator('[data-action="page-duplicate"]').click()
- await page.locator('[data-action="save"]').click()
- check('正文续页和复制页面保存', (await state()).documentPages.length===pages+1)
- await page.locator('[data-action="undo"]').click(); await page.locator('[data-action="save"]').click()
- check('撤销复制恢复页数', (await state()).documentPages.length===pages)
- await page.locator('[data-action="redo"]').click(); await page.locator('[data-action="save"]').click()
- check('重做复制恢复页数', (await state()).documentPages.length===pages+1)
- await shot('04-edit-pages')
- for (const tab of ['three','motion','annotation','formula','layout']) {
- const button=page.locator(`[data-action="property-tab"][data-property-tab="${tab}"]`)
- if (await button.count()) { await button.click(); check(`属性面板 ${tab} 打开`,true) }
- }
- await page.locator('#ofd-resource-file').setInputFiles(path.resolve('../../文档/素材/09-Fox.glb'))
- await expect.poll(async()=> (await state()).resources.some(r=>r.name.includes('Fox')), {timeout:30000}).toBe(true)
- check('导入真实 Fox.glb 文件并登记本地资源',true)
- await page.locator('[data-action="right-dock-tab"]').filter({hasText:'资源'}).click()
- await shot('05-import-fox')
- const actions=await page.locator('.ofd-app').evaluate(el=>[...el.querySelectorAll('[data-action]')].map(x=>({action:x.dataset.action,text:(x.innerText||x.title||'').slice(0,50),data:{...x.dataset}})))
- fs.writeFileSync(path.join(work,'resource-actions.json'),JSON.stringify(actions,null,2))
- fs.writeFileSync(fixtureFile,JSON.stringify(fixture))
- }
- if (phase === 'reader') {
- await open(`/content/ofd/${fixture.id}/studio?view=editor`)
- await expect(page.locator('.ofd-view-editor')).toBeVisible()
- check('查看角色的正文编辑控件禁用',await page.locator('[data-step-field="title"]').isDisabled())
- check('查看角色可以使用导出菜单',await page.locator('[data-field="export-target"]').isEnabled())
- check('查看角色可以使用页码和缩放',await page.locator('[data-field="canvas-page"]').isEnabled() && await page.locator('[data-field="canvas-zoom-choice"]').isEnabled())
- const before=JSON.stringify(await state())
- const download=page.waitForEvent('download')
- await page.locator('[data-field="export-target"]').selectOption('ofd')
- await (await download).saveAs(path.join(report,'reader-export.ofd'))
- await page.waitForTimeout(700)
- check('只读用户实际导出 OFD 且不修改工程',before===JSON.stringify(await state()))
- await shot('reader-before')
- }
- if (phase === 'missing') {
- const unknown='REVIEW-NOT-EXIST-20260910'
- await open(`/content/ofd/${unknown}/studio?view=editor`)
- await page.waitForTimeout(1200)
- const exists=await page.evaluate(async id=>Boolean((await import('/src/features/guide-legacy/runtime.ts')).legacyGuideContentStore.getOFDProject(id)),unknown)
- check('访问不存在的指导书不会自动新建',!exists)
- await shot('missing-before')
- if(exists) await page.evaluate(async id=>(await import('/src/features/guide-legacy/runtime.ts')).legacyGuideContentStore.deleteOFDProject(id),unknown)
- }
- if (phase === 'media') {
- await open(`/content/ofd/${fixture.id}/studio?view=editor`)
- // Playwright storageState does not preserve Blob prototypes across contexts.
- // Re-import real bytes, then test reload inside this context below.
- await page.locator('#ofd-resource-file').setInputFiles(path.resolve('../../文档/素材/09-Fox.glb'))
- await page.waitForTimeout(1000)
- await page.reload()
- await expect(page.locator('.ofd-view-editor')).toBeVisible()
- 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:'09-Fox.glb'}).first().click()
- await page.locator('[data-action="save"]').click()
- check('模型插入步骤并保存关联', (await state()).chapters[0].steps[0].mediaBlocks?.some(b=>b.assetId))
- await page.locator('[data-action="right-dock-tab"]').filter({hasText:'资源'}).click()
- await page.locator('[data-action="library-insert"][data-kind="animation"]').filter({hasText:'Run'}).first().click()
- await page.locator('[data-action="save"]').click()
- check('关联导入模型的真实动画', JSON.stringify((await state()).chapters[0].steps[0]).includes('Run'))
- await page.locator('[data-action="media-open"]').first().click()
- await expect(page.locator('.ofd-media-panel canvas').first()).toBeVisible({timeout:30000})
- await expect(page.locator('.ofd-media-panel')).not.toContainText('模型暂不可用', {timeout:30000})
- await page.waitForTimeout(2500)
- await shot('06-fox-model-panel')
- await page.locator('[data-action="media-view"][data-view="all"]').click()
- await page.waitForTimeout(1500)
- await shot('06b-fox-framed')
- const controls=await page.locator('.ofd-media-panel').evaluate(el=>[...el.querySelectorAll('[data-action]')].map(x=>({action:x.dataset.action,text:(x.innerText||x.title||'').slice(0,60),data:{...x.dataset}})))
- fs.writeFileSync(path.join(work,'media-controls.json'),JSON.stringify(controls,null,2))
- check('三维模型放大面板渲染画布',true)
- await page.locator('[data-action="media-close"]').click()
- await page.locator('[data-action="right-dock-tab"]').filter({hasText:'属性'}).click()
- for (const [text, tab] of Object.entries({'三维':'model','动画':'animation','标注':'annotation','公式':'formula','版式':'layout','内容':'content'})) {
- await page.locator(`[data-action="property-tab"][data-tab="${tab}"]`).click()
- check(`步骤属性「${text}」可打开`,true)
- await shot(`property-${text}`)
- }
- await page.locator('#ofd-resource-file').setInputFiles(path.resolve('../../文档/素材/08-CesiumMilkTruck.glb'))
- await expect.poll(async()=> (await state()).resources.some(r=>r.name.includes('MilkTruck')), {timeout:30000}).toBe(true)
- await page.locator('[data-action="select-step"]').last().click()
- await page.locator('[data-action="right-dock-tab"]').filter({hasText:'资源'}).click()
- await page.locator('[data-action="library-insert"][data-kind="model"]').filter({hasText:'MilkTruck'}).first().click()
- await page.locator('[data-action="save"]').click()
- check('第二步骤绑定牛奶卡车模型',JSON.stringify((await state()).chapters.flatMap(c=>c.steps).at(-1)).includes('MilkTruck'))
- await shot('07-milk-truck-step')
- }
- if (phase === 'export') {
- await open(`/content/ofd/${fixture.id}/studio?view=editor`)
- for (const file of ['09-Fox.glb','08-CesiumMilkTruck.glb']) {
- await page.locator('#ofd-resource-file').setInputFiles(path.resolve('../../文档/素材',file))
- await page.waitForTimeout(900)
- }
- for (const target of ['project','requirement','plan','pdf','ofd','offline']) {
- try {
- const waiting=page.waitForEvent('download',{timeout:90000})
- await page.locator('[data-field="export-target"]').selectOption(target)
- const download=await waiting
- const file=path.join(report, `export-${target}${path.extname(download.suggestedFilename())}`)
- await download.saveAs(file)
- check(`导出 ${target} 下载文件`,fs.statSync(file).size>0)
- if(target==='project') fixture.exportFile=file
- } catch(error) { check(`导出 ${target}`,false,error.message); await shot(`export-${target}-failure`) }
- }
- fs.writeFileSync(fixtureFile,JSON.stringify(fixture))
- await page.locator('[data-action="open-health"]').click()
- await shot('08-preflight')
- const details=await page.locator('.ofd-app').innerText()
- fs.writeFileSync(path.join(work,'preflight.txt'),details)
- await page.locator('[data-action="close-workspace-dialog"].ofd-icon-btn').click()
- await page.locator('[data-action="open-publish"]').click()
- await shot('09-publish-panel')
- const actions=await page.locator('.ofd-app').evaluate(el=>[...el.querySelectorAll('[data-action]')].map(x=>({action:x.dataset.action,text:(x.innerText||x.title||'').slice(0,60),data:{...x.dataset}})))
- fs.writeFileSync(path.join(work,'publish-actions.json'),JSON.stringify(actions,null,2))
- }
- if (phase === 'content') {
- await open(`/content/ofd/${fixture.id}/studio?view=editor`)
- 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="property-tab"][data-tab="formula"]').click()
- await page.locator('[data-action="formula-add"]').click()
- await page.locator('[data-formula-field="source"]').last().fill('F = p \\times A')
- await page.locator('[data-formula-field="source"]').last().blur()
- await page.locator('[data-formula-field="caption"]').last().fill('闭环验证公式,仅用于排版测试')
- await page.locator('[data-formula-field="caption"]').last().blur()
- await page.locator('[data-action="save"]').click()
- check('公式与说明保存', (await state()).chapters[0].steps[0].formulas.some(f=>f.caption.includes('闭环验证')))
- await shot('10-formula')
- await page.locator('[data-action="ribbon-tab"]').filter({hasText:'插入'}).click()
- fs.writeFileSync(path.join(work,'insert-dom.json'), JSON.stringify(await page.locator('.ofd-app').evaluate(el=>({actions:[...el.querySelectorAll('[data-action]')].map(x=>({...x.dataset,text:(x.innerText||x.title||'').slice(0,50)})),inputs:[...el.querySelectorAll('input,select,textarea')].map(x=>({id:x.id,...x.dataset,type:x.type,value:x.value.slice(0,40)}))})),null,2))
- await shot('11-insert-tools')
- await page.locator('[data-action="open-publish"]').click()
- await shot('09-publish-panel')
- await page.locator('[data-action="lifecycle-action"][data-lifecycle-action="publish"]').click()
- await expect.poll(async()=>(await state()).lifecycle.status).toBe('published')
- check('发布保存本地正式版本',true)
- fixture.publishedTitle=(await state()).chapters[0].steps[0].title
- await shot('12-published')
- fs.writeFileSync(path.join(work,'published-state.json'),JSON.stringify(await state()))
- fs.writeFileSync(fixtureFile,JSON.stringify(fixture))
- }
- if (phase === 'revision') {
- await open(`/content/ofd/${fixture.id}/studio?view=publish`)
- await page.locator('[data-action="lifecycle-action"][data-lifecycle-action="edit"]').click()
- await expect.poll(async()=>(await state()).lifecycle.status).toBe('draft')
- await page.locator('[data-action="goto"][data-view="editor"]').click()
- 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="property-tab"][data-tab="content"]').click()
- await page.locator('[data-step-field="title"]').fill('狐狸巡检目标识别(修订)')
- await page.locator('[data-step-field="title"]').blur()
- await page.locator('[data-action="save"]').click()
- const edited=await state()
- check('修订不改变旧的正式版本快照',edited.lifecycle.publishedSnapshot.chapters[0].steps[0].title===fixture.publishedTitle && edited.chapters[0].steps[0].title!==fixture.publishedTitle)
- // 清理由重复运行测试产生的重叠媒体块,通过用户可用的 JSON 导入入口回灌。
- const bundle={schema:'',state:edited}
- Object.assign(bundle,JSON.parse(fs.readFileSync(path.join(report,'export-project.json'),'utf8')),{state:edited})
- for (const c of edited.chapters) for (const s of c.steps) {
- s.mediaBlocks=(s.mediaBlocks||[]).filter(b=>b.kind==='model').slice(-1)
- for (const b of s.mediaBlocks) { b.poster='';b.posterAssetSignature='';b.camera=null }
- }
- const cleanFile=path.join(work,'roundtrip.json');fs.writeFileSync(cleanFile,JSON.stringify(bundle))
- await page.locator('#ofd-project-file').setInputFiles(cleanFile)
- await expect.poll(async()=>(await state()).chapters[0].steps[0].mediaBlocks.length).toBe(1)
- check('工程 JSON 导入恢复步骤与资源关联',true)
- for (const file of ['09-Fox.glb','08-CesiumMilkTruck.glb']) {
- await page.locator('#ofd-resource-file').setInputFiles(path.resolve('../../文档/素材',file));await page.waitForTimeout(1200)
- }
- await page.reload();await expect(page.locator('.ofd-view-editor')).toBeVisible()
- for (let i=0;i<2;i++) {
- await page.locator('[data-action="select-step"]').nth(i).click()
- const b=(await state()).chapters[0].steps[i].mediaBlocks[0]
- await page.locator(`[data-action="canvas-block"][data-block-id="step:${(await state()).chapters[0].steps[i].id}:media:${b.id}"]`).click()
- await page.locator('[data-action="media-open"]').first().click()
- await expect(page.locator('.ofd-media-panel canvas')).toBeVisible()
- await page.waitForTimeout(1300)
- await shot(i?'14-truck-panel':'13-fox-panel')
- const before=await page.locator('.ofd-media-panel canvas').screenshot()
- await page.locator('[data-action="media-play"]').click()
- await page.waitForTimeout(380)
- const after=await page.locator('.ofd-media-panel canvas').screenshot()
- check(`${i?'卡车':'狐狸'}动画真实改变渲染帧`,!before.equals(after))
- await page.locator('[data-action="media-apply"]').click()
- await page.locator('[data-action="save"]').click()
- check(`${i?'卡车':'狐狸'}截取当前角度保存纸面图片`,(await state()).chapters[0].steps[i].mediaBlocks[0].poster.startsWith('data:image/jpeg'))
- }
- await shot('15-clean-editor')
- await page.locator('[data-action="open-publish"]').click()
- await page.locator('[data-publish="note"]').fill('狐狸与牛奶卡车模型闭环验证')
- await page.locator('[data-publish="note"]').blur()
- await page.locator('[data-action="lifecycle-action"][data-lifecycle-action="publish"]').click()
- await expect.poll(async()=>(await state()).lifecycle.status).toBe('published')
- check('新修订再次发布并留存历史版本',(await state()).versions.filter(v=>v.immutable).length>=2)
- await page.locator('[data-action="lifecycle-action"][data-lifecycle-action="withdraw"]').click()
- await expect.poll(async()=>(await state()).lifecycle.status).toBe('withdrawn')
- check('撤回本地正式版本',true)
- await shot('16-withdrawn')
- }
- if (phase === 'templates') {
- await open('/content/ofd/templates')
- const ids=await page.locator('[data-guide-tpl-action="select"]').evaluateAll(els=>els.map(e=>e.dataset.guideTplId))
- check('四类模板列表',ids.length===4)
- for (let i=0;i<ids.length;i++) {
- const id=ids[i]
- await page.locator(`[data-guide-tpl-action="preview"][data-guide-tpl-id="${id}"]`).click()
- await expect(page.locator('.guide-tpl-ofd-pages')).toBeVisible()
- check(`模板 ${i+1} 静态版式预览`,true)
- await page.locator('[data-guide-tpl-mode="interactive"]').click()
- await page.locator('[data-guide-tpl-action="preview-step"]').last().click()
- check(`模板 ${i+1} 动态目录切换`,await page.locator('[data-guide-tpl-action="preview-step"]').last().evaluate(e=>e.classList.contains('active')))
- if (i===0) {
- await shot('17-template-preview')
- for (const format of ['pdf','ofd']) {
- const wait=page.waitForEvent('download');await page.locator(`[data-guide-tpl-action="download"][data-guide-tpl-format="${format}"]`).click()
- await (await wait).saveAs(path.join(report,`template.${format}`));check(`模板下载 ${format}`,true)
- }
- }
- await page.locator('[data-guide-tpl-action="close-preview"][aria-label="关闭"]').click()
- }
- await page.locator('[data-guide-tpl-action="create"]').click()
- await page.locator('[data-wizard="name"]').fill('闭环实测-牛奶卡车指导书-20260910')
- await page.locator('[data-wizard="code"]').fill('REVIEW-GUIDE-TRUCK-20260910')
- await page.locator('[data-wizard="equipment"]').fill('牛奶卡车测试模型')
- await page.locator('[data-action="wizard-next"]').click()
- await page.locator('[data-action="wizard-next"]').click()
- await page.waitForURL(/view=analysis/)
- fixture.templateId=page.url().match(/\/ofd\/([^/]+)\/studio/)[1]
- const created=await page.evaluate(async id=>(await import('/src/features/guide-legacy/runtime.ts')).legacyGuideContentStore.getOFDProject(id).state,fixture.templateId)
- check('选用模板真正生成章节步骤',created.project.templateId===ids.at(-1) && created.chapters.length===4 && created.chapters.flatMap(c=>c.steps).length===9)
- check('新文档不混入演示版本和反馈',!created.versions.length && !created.feedback.length && !created.lifecycle.audit.length)
- check('新文档不混入其他装备参数与来源',!created.project.sourcePackage && !created.partsCatalog.length && !created.technicalParameters.length)
- await shot('18-new-template-guide')
- fs.writeFileSync(fixtureFile,JSON.stringify(fixture))
- await open('/content/ofd')
- await expect(page.locator('.el-table__body-wrapper tbody tr').first()).toContainText('牛奶卡车指导书')
- check('列表默认按创建时间倒序',true)
- await shot('19-list-created-order')
- }
- if (phase === 'api') {
- await open('/content/ofd/api')
- await page.locator('[data-field="api-project"]').selectOption(fixture.id)
- const ids=await page.locator('[data-action="api-endpoint"]').evaluateAll(es=>es.map(e=>e.dataset.id))
- check('对外接口展示九个操作',ids.length===9)
- // 写操作只操作隔离浏览器中的本轮测试文档,生命周期已单独实测。
- for (const id of ids) {
- await page.locator(`[data-action="api-endpoint"][data-id="${id}"]`).click()
- if (/lifecycle|workflow|step-update|resource-register/.test(id)) continue
- await page.locator('[data-action="api-run"]').click()
- await page.waitForTimeout(300)
- const response=await page.locator('.ofd-api-result').innerText()
- fs.writeFileSync(path.join(work,`api-${id}.txt`),response)
- check(`本地接口调度 ${id} 返回成功`,/^20\d/.test(response),response.slice(0,80))
- }
- check('接口面板未发送真实 HTTP 业务请求',requests.length===0,'这里通过本地函数调度,需要接入正式 API 才可外部调用')
- await shot('20-api-local')
- }
- if (phase === 'offline') {
- const external=[]
- page.on('console',msg=>{if(msg.type()==='error') console.log('BROWSER',msg.text())})
- page.on('requestfailed',r=>console.log('NETWORK',r.url(),r.failure()))
- await context.route('**/*',r=>{
- if (new URL(r.request().url()).host==='127.0.0.1:18766') return r.continue()
- external.push(r.request().url());return r.abort()
- })
- await page.goto('http://127.0.0.1:18766')
- await page.locator('[data-action="start"]').click()
- await page.locator('[data-action="next"]').click()
- check('离线包章节步骤可以阅读与切换',await page.locator('.reader-panel:visible').count()>0)
- await shot('21-offline-reading')
- await page.locator('[data-action="open-model-library"]').click()
- console.log('OFFLINE DOM',await page.evaluate(()=>({scripts:[...document.scripts].map(s=>({type:s.type,src:s.src,size:s.textContent.length})),status:document.querySelector('[data-model-status]')?.textContent})))
- await expect(page.locator('[data-model-stage] canvas')).toBeVisible({timeout:30000})
- await expect(page.locator('[data-model-status]')).toContainText(/个网格/, {timeout:30000})
- for (let i=0;i<2;i++) {
- await page.locator('[data-model-select]').selectOption(String(i))
- await page.waitForTimeout(1400)
- await shot(`22-offline-model-${i}`)
- const before=await page.locator('[data-model-stage] canvas').screenshot()
- await page.locator('[data-model-action="play-animation"]').click();await page.waitForTimeout(500)
- const after=await page.locator('[data-model-stage] canvas').screenshot()
- check(`离线模型 ${i+1} 动画可播放`,!before.equals(after))
- await page.locator('[data-model-action="pause-animation"]').click()
- for (const action of ['zoom-in','zoom-out','rotate','toggle-transparent','highlight','explode','reset']) await page.locator(`[data-model-action="${action}"]`).click()
- check(`离线模型 ${i+1} 观察操作可执行`,true)
- }
- check('断开公网和业务服务后没有外部依赖请求',external.length===0,external.join('\n'))
- check('离线个人批注与收藏入口',await page.getByText(/收藏|添加笔记|保存笔记/).count()>0,'当前离线阅读器未提供原始需求中的个人笔记、收藏和学习进度回传')
- await page.setViewportSize({width:390,height:844});await shot('23-offline-mobile')
- }
- if (phase === 'layout') {
- await open(`/content/ofd/${fixture.templateId}/studio?view=editor`)
- await page.waitForTimeout(700)
- console.log('LOAD',await page.locator('.el-message').allTextContents(),await page.locator('.legacy-guide-page').innerText().then(t=>t.slice(0,100)))
- const get=async()=>page.evaluate(async id=>(await import('/src/features/guide-legacy/runtime.ts')).legacyGuideContentStore.getOFDProject(id).state,fixture.templateId)
- await page.locator('[data-action="select-step"]:visible').first().click()
- await page.locator('[data-action="ribbon-tab"][data-tab="insert"]').click()
- await page.locator('[data-field="canvas-insert"]').selectOption('table')
- await page.locator('[data-action="table-cell"]').first().dblclick()
- await page.locator('[data-inline-field="cell"]').fill('检测项目')
- await page.locator('[data-inline-field="cell"]').blur()
- await page.locator('[data-action="ribbon-tab"][data-tab="home"]').click()
- await page.locator('[data-action="table-op"][data-op="merge-selection"]').click()
- await page.locator('[data-action="table-op"][data-op="split-cell"]').click()
- await page.locator('[data-field="table-border-style"]').selectOption('dashed')
- await page.locator('[data-action="table-op"][data-op="border-apply"]').click()
- await page.locator('[data-action="save"]').click()
- check('插入表格、填写单元格、合并拆分与边框保存',JSON.stringify(await get()).includes('检测项目'))
- await shot('24-table-edit')
- await page.locator('[data-action="ribbon-tab"][data-tab="insert"]').click()
- await page.locator('[data-field="canvas-insert"]').selectOption('text')
- await page.locator('[data-action="save"]').click()
- const stored=JSON.stringify(await get())
- check('插入正文自由块后保存',stored.includes('extraBlocks'))
- await page.locator('[data-action="ribbon-tab"][data-tab="review"]').click()
- await page.locator('[data-action="canvas-tool"][data-tool="rect"]').click()
- const canvas=await page.locator('[data-action="annotate-canvas"]').boundingBox()
- await page.mouse.move(canvas.x+170,canvas.y+150)
- await page.mouse.down();await page.mouse.move(canvas.x+280,canvas.y+220,{steps:8});await page.mouse.up()
- await page.locator('[data-action="save"]').click()
- check('二维框标注保存',JSON.stringify(await get()).includes('markups'))
- await shot('25-markup')
- await page.locator('[data-action="canvas-tab"][data-tab="interactive"]').click()
- await page.locator('[data-field="interactive-search"]').fill('牛奶卡车')
- await expect(page.locator('.ofd-ia-search-results')).toBeVisible()
- check('动态阅读检索步骤',true)
- await page.locator('[data-action="interactive-search-clear"]').click()
- await page.locator('[data-action="interactive-next"]').click()
- await shot('26-interactive-search')
- await page.reload();await expect(page.locator('.ofd-view-editor')).toBeVisible()
- check('表格、自由块、二维标注重开后保留',JSON.stringify(await get()).includes('检测项目') && JSON.stringify(await get()).includes('markups'))
- await open('/content/ofd')
- await page.getByPlaceholder('搜索指导书标题、编号或作者').fill('REVIEW-GUIDE-TRUCK-20260910')
- await page.getByRole('button',{name:'搜索',exact:true}).click()
- check('列表按编号检索',await page.locator('.el-table__body-wrapper tbody tr').count()>=1)
- await page.getByRole('button',{name:'重置',exact:true}).click()
- check('重置恢复创建时间排序',await page.getByRole('combobox',{name:'指导书排序'}).getAttribute('aria-label')==='指导书排序')
- }
- if (phase === 'final-document') {
- await open(`/content/ofd/${fixture.id}/studio?view=editor`)
- const current=await state()
- const bundle=JSON.parse(fs.readFileSync(path.join(report,'export-project.json'),'utf8'));bundle.state=current
- current.documentPages=current.documentPages.filter(p=>p.contentMode!=='continuation')
- for (const c of current.chapters) for (const [i,s] of c.steps.entries()) {
- s.image='';s.modelRef.camera=null
- s.summary=i?'确认牛奶卡车测试模型完整显示,播放车轮动画并记录测试结果。':s.summary
- s.safety='测试素材仅用于功能验证。';s.acceptance='模型与动画可查看,记录保存后能够重新打开。'
- for (const b of s.mediaBlocks) { b.camera=null;b.poster='';b.posterAssetSignature='' }
- }
- const file=path.join(work,'final-document.json');fs.writeFileSync(file,JSON.stringify(bundle))
- await page.locator('#ofd-project-file').setInputFiles(file)
- await page.waitForTimeout(800)
- for (const file of ['09-Fox.glb','08-CesiumMilkTruck.glb']) {
- await page.locator('#ofd-resource-file').setInputFiles(path.resolve('../../文档/素材',file));await page.waitForTimeout(1000)
- }
- await page.reload();await expect(page.locator('.ofd-view-editor')).toBeVisible()
- for (let i=0;i<2;i++) {
- await page.locator('[data-action="select-step"]').nth(i).click()
- const s=(await state()).chapters[0].steps[i],b=s.mediaBlocks[0]
- await page.locator(`[data-action="canvas-block"][data-block-id="step:${s.id}:media:${b.id}"]`).click()
- await page.locator('[data-action="media-open"]').first().click();await page.waitForTimeout(1300)
- await page.locator('[data-action="media-view"][data-view="all"]').click();await page.waitForTimeout(250)
- await shot(i?'28-final-truck':'27-final-fox')
- await page.locator('[data-action="media-apply"]').click();await page.locator('[data-action="save"]').click()
- check(`${i?'卡车':'狐狸'}按实际尺寸取景后保存`,(await state()).chapters[0].steps[i].mediaBlocks[0].poster.startsWith('data:image/jpeg'))
- }
- await page.reload();await expect(page.locator('.ofd-view-editor')).toBeVisible()
- check('两个模型的取景图片重开后保留',(await state()).chapters[0].steps.every(s=>s.mediaBlocks[0].poster.startsWith('data:image/jpeg')))
- }
- if (phase === 'resources') {
- await open(`/content/ofd/${fixture.templateId}/studio?view=editor`)
- await page.locator('[data-action="select-step"]:visible').first().click()
- await page.locator('#ofd-resource-file').setInputFiles(path.resolve('../../.codex-tmp/guide-test-video.mp4'))
- await page.waitForTimeout(700)
- await page.locator('[data-action="right-dock-tab"]').filter({hasText:'资源'}).click()
- await page.locator('[data-action="library-insert"][data-kind="resource"]').filter({hasText:'guide-test-video'}).first().click()
- await page.locator('[data-action="media-open"]').first().click()
- const video=page.locator('[data-media-video]')
- await expect.poll(()=>video.evaluate(v=>v.readyState)).toBeGreaterThanOrEqual(2)
- const start=await video.evaluate(v=>{v.play();return v.currentTime})
- await page.waitForTimeout(450)
- check('导入本地视频并在原生播放器实际播放',await video.evaluate(v=>v.currentTime)>start)
- await page.locator('[data-action="video-capture-poster"]').click()
- await shot('29-video-poster')
- await page.locator('[data-action="media-close"]').click()
- await page.locator('[data-action="save"]').click()
- const doc=await page.evaluate(async id=>(await import('/src/features/guide-legacy/runtime.ts')).legacyGuideContentStore.getOFDProject(id).state,fixture.templateId)
- check('视频当前帧海报保存到步骤',doc.chapters.flatMap(c=>c.steps).some(s=>s.mediaBlocks.some(b=>b.kind==='video' && b.poster.startsWith('data:image/jpeg'))))
- const original=JSON.stringify(doc)
- await page.locator('#ofd-project-file').setInputFiles({name:'broken.json',mimeType:'application/json',buffer:Buffer.from('{broken')})
- await page.waitForTimeout(500)
- const after=await page.evaluate(async id=>(await import('/src/features/guide-legacy/runtime.ts')).legacyGuideContentStore.getOFDProject(id).state,fixture.templateId)
- check('非法工程 JSON 被拒绝且原工程保留',JSON.stringify(after)===original)
- await shot('30-invalid-import')
- const content=await call('tran/v1/content/projects?type=GUIDE&keyword=REVIEW-GUIDE-20260910',session.accessToken)
- const teaching=await call('tran/v1/teaching/guides?keyword=REVIEW-GUIDE-20260910',session.accessToken)
- check('编制工程同步到正式内容库',content.status===200 && (content.data?.records||[]).some(r=>r.code==='REVIEW-GUIDE-20260910'),'实测仅写入浏览器本地库;正式内容库查询 HTTP '+content.status)
- check('编制发布结果进入教学指导书列表',teaching.status===200 && (teaching.data?.records||[]).some(r=>r.code==='REVIEW-GUIDE-20260910'),'教学查询 HTTP '+teaching.status)
- }
- if (phase === 'viewport-probe') {
- await open(`/content/ofd/${fixture.id}/studio?view=editor`)
- const bytes=[...fs.readFileSync(path.resolve('../../文档/素材/09-Fox.glb'))]
- console.log(await page.evaluate(async bytes=>{
- const {createGuideViewport}=await import('/src/features/guide-legacy/demo-src/ofd-viewport.js')
- const host=document.createElement('div'); host.style.cssText='position:fixed;inset:100px;background:white;z-index:99999';document.body.append(host)
- const v=createGuideViewport(); window.reviewViewport=v;v.attach(host);await v.load('review-fox',{bytes:new Uint8Array(bytes)})
- return {scene:v.describeScene(),fog:v.scene.fog,camera:v.camera.position.toArray(),target:v.controls.target.toArray(),stats:v.loadStats}
- },bytes))
- await page.waitForTimeout(1000); await shot('viewport-probe')
- await page.evaluate(()=>{window.reviewViewport.scene.fog=null;window.reviewViewport.invalidate()})
- await page.waitForTimeout(500);await shot('viewport-without-fog')
- console.log(await page.evaluate(()=>{const v=window.reviewViewport;return {stats:v.renderer.info.render,disposed:v.disposed,needsRender:v.needsRender,camera:v.camera.position.toArray(),target:v.controls.target.toArray(),bounds:v.modelBox().getSize(new v.camera.position.constructor()).toArray()}}))
- }
- } catch(error) {
- await shot(`${phase}-failure`).catch(()=>{})
- check(`${phase} 脚本执行`, false, error.message)
- process.exitCode=1
- } finally {
- if (!['offline','viewport-probe','reader'].includes(phase)) await context.storageState({ path:stateFile, indexedDB:true })
- fs.writeFileSync(path.join(report, `${phase}.json`), JSON.stringify({ checks, errors, requests }, null, 2))
- await browser.close()
- }
|