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/animation-capabilities-20260912'),fixturePath=path.join(report,'fixture.json'); const fixture=JSON.parse(fs.readFileSync(fixturePath,'utf8')),session=await login('root'); const save=()=>fs.writeFileSync(fixturePath,JSON.stringify(fixture,null,2)); 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(),checks=[],errors=[]; page.on('pageerror',e=>errors.push(e.message)); const shot=async name=>page.screenshot({path:path.join(report,name+'.png')}); try { await page.goto('http://127.0.0.1:6180/content/ofd');await expect(page.locator('.legacy-guide-page')).toBeVisible({timeout:60000}); if(!fixture.guideId){ fixture.guide=await page.evaluate(async({modelId,versionId,candidateId})=>{ const {FormalGuideStore}=await import('/src/features/guide-legacy/formalStore.ts'); const {blankGuideState}=await import('/src/features/guide-legacy/formalDocument.ts'); const {createMediaBlock}=await import('/src/features/guide-legacy/demo-src/ofd-media.js'); const server=new FormalGuideStore(),state=blankGuideState(); state.project.name='动画能力验收指导书-20260912';state.project.owner='功能验收';state.project.equipment='牛奶卡车动画测试'; const record=candidateId?await server.load(candidateId):await server.createOFDProject({title:state.project.name,state}); await server.loadCatalog(); const key=`${modelId}@${versionId}`; const step=record.state.chapters[0].steps[0];step.title='旋出、清洁和流向演示';step.summary='选择动作片段,观察车轮组合运动、污渍到清洁状态变化以及回路开通后的流向。'; step.modelRef={assetId:key,modelId:key,nodeName:'Wheels'}; step.animationRef={clipName:'旋出与清洁演示',start:0,end:4,loop:false};step.animation='旋出与清洁演示'; step.mediaBlocks=[createMediaBlock({kind:'model',assetId:key,assetName:'组合动作演示',clipName:'旋出与清洁演示',start:0,end:4})]; const saved=await server.saveOFDProjectState(record.id,record.state); const resource=saved.state.resources.find(r=>r.id===key); server.dispose();return {id:record.id,resource:{id:resource.id,tracks:resource.timeline?.tracks.length,clips:resource.timeline?.clips.length,overrides:Object.keys(resource.modelOverrides||{}).length}}; },{modelId:fixture.modelId,versionId:fixture.modelVersionId,candidateId:fixture.guideCandidateId}); fixture.guideId=fixture.guide.id;save(); } assert.equal(fixture.guide.resource.tracks,4);assert.equal(fixture.guide.resource.clips,2); checks.push({name:'指导书通过正式接口固定引用模型版本并持久化动画元数据',pass:true,resource:fixture.guide.resource}); await page.goto(`http://127.0.0.1:6180/content/ofd/${fixture.guideId}/studio?view=editor`); await expect(page.locator('.ofd-view-editor')).toBeVisible({timeout:60000}); await page.locator('[data-action="select-step"]').first().click(); await page.locator('[data-action="canvas-block"][data-block-id*="media:"]').first().click(); await page.locator('[data-action="media-open"]').first().click(); await expect(page.locator('.ofd-media-panel canvas')).toBeVisible({timeout:60000}); await shot('05-guide-model-animation'); console.log('MEDIA_BUTTONS',await page.locator('.ofd-media-panel button').allTextContents()); const outcome=await page.evaluate(async({guideId,modelId,versionId})=>{ const {FormalGuideStore}=await import('/src/features/guide-legacy/formalStore.ts'); const {createGuideViewport}=await import('/src/features/guide-legacy/demo-src/ofd-viewport.js'); const server=new FormalGuideStore();await server.load(guideId); const asset=server.record.state.resources.find(r=>r.id===`${modelId}@${versionId}`); const viewport=createGuideViewport(); const blob=(await server.assetStore.get(asset.assetKey)).blob; await viewport.load(asset.id,blob,asset); const clips=viewport.clips.map(c=>c.name),wheel=viewport.findModelNode('Wheels'); viewport.playClip('旋出与清洁演示');viewport.pause(); const initial=wheel.position.toArray(); // Seek through the same mixer transport used by reader controls. const {createAnimationRuntimeTools}=await import('/src/features/guide-legacy/demo-src/animation-runtime.js'); const THREE=await import('/node_modules/.vite/deps/three.js'); const tools=createAnimationRuntimeTools(THREE);tools.apply(viewport.editorTimeline,4); const final=wheel.position.toArray(),state=viewport.editorTimeline.materials.values().next().value.clones[0].roughness; viewport.dispose();server.dispose();return {clips,initial,final,state}; },{guideId:fixture.guideId,modelId:fixture.modelId,versionId:fixture.modelVersionId}); assert.ok(outcome.clips.includes('旋出与清洁演示'));assert.ok(outcome.clips.includes('阀门复位演示'));assert.notDeepEqual(outcome.initial,outcome.final);assert.equal(outcome.state,.2); checks.push({name:'正式指导书视口加载命名动作并还原组合运动与材质状态',pass:true,detail:outcome}); // Publish using the page's normal delivery pipeline. await page.keyboard.press('Escape'); const close=page.locator('[data-action="media-close"]');if(await close.count())await close.first().click(); await page.locator('[data-action="save"]').click();await page.waitForTimeout(700); await page.locator('[data-action="open-publish"]').click(); const wait=page.waitForResponse(r=>r.url().endsWith('/publish')&&r.request().method()==='POST',{timeout:180000}); await page.locator('[data-action="lifecycle-action"][data-lifecycle-action="publish"]').click(); const published=await wait;assert.ok(published.ok(),(await published.text()).slice(0,300)); await shot('06-guide-published'); const response=await context.request.get(`http://127.0.0.1:6180/api/tran/v1/teaching/guides/${fixture.guideId}/delivery?format=OFFLINE_HTML`,{headers:{Authorization:`Bearer ${session.accessToken}`},timeout:120000}); assert.ok(response.ok());fs.writeFileSync(path.join(report,'animation-guide.zip'),await response.body()); checks.push({name:'正式发布并下载带动画的离线指导书',pass:true}); }catch(error){checks.push({name:'指导书动画端到端',pass:false,error:error.message});process.exitCode=1;console.error(error.message);await shot('guide-failure').catch(()=>{});} finally{fs.writeFileSync(path.join(report,'guide-checks.json'),JSON.stringify({checks,errors},null,2));await browser.close();}