import { test, expect } from '@playwright/test'; import { resolve } from 'node:path'; import { writeFile } from 'node:fs/promises'; test('完整 CAD 主视口:60 秒渲染/物理性能记录(不承诺固定 FPS)', async ({ page }, info) => { const errors: string[] = []; page.on('pageerror', (error) => errors.push(error.message)); await page.goto('/'); const started = performance.now(); await page .getByLabel('打开文件', { exact: true }) .setInputFiles(resolve('build/lekiwi/lekiwi-v1.zip')); const dialog = page.getByRole('dialog', { name: '配置 URDF 仿真组件' }); await expect(dialog).toBeVisible({ timeout: 30000 }); await dialog.getByLabel('URDF 机器人 profile').selectOption('lekiwi-v1'); await dialog.getByRole('button', { name: '转换并加载' }).click(); await expect(page.getByRole('button', { name: '▶ 播放' })).toBeEnabled({ timeout: 60000 }); const loadMs = performance.now() - started; await page.getByRole('button', { name: '▶ 播放' }).click(); const result = await page.evaluate(async () => { const path = '/src/stores/useAppStore.ts'; const { useAppStore } = (await import( /* @vite-ignore */ path )) as typeof import('../src/stores/useAppStore'); const canvas = document.querySelector('main canvas') as HTMLCanvasElement; const gl = canvas.getContext('webgl2')!, ext = gl.getExtension('WEBGL_debug_renderer_info'); const graphics = { size: [canvas.width, canvas.height], renderer: ext ? String(gl.getParameter(ext.UNMASKED_RENDERER_WEBGL)) : 'unknown', }; const start = performance.now(), frames: number[] = [], steps: number[] = []; const longTasks: number[] = []; const observer = new PerformanceObserver((list) => { longTasks.push(...list.getEntries().map((e) => e.duration)); }); observer.observe({ type: 'longtask' }); let previous = start; await new Promise((resolve) => { const tick = (now: number) => { frames.push(now - previous); previous = now; steps.push(useAppStore.getState().stepMs); if (now - start >= 60000) resolve(); else requestAnimationFrame(tick); }; requestAnimationFrame(tick); }); observer.disconnect(); const p95 = (values: number[]) => [...values].sort((a, b) => a - b)[Math.floor(values.length * 0.95)] ?? 0; const state = useAppStore.getState(); return { graphics, frames: frames.length, fps: (1000 * frames.length) / (previous - start), frameP95Ms: p95(frames), stepP95Ms: p95(steps), longTasks, heap: state.snapshot?.wasmHeapBytes, simTime: state.snapshot?.time, diagnostic: state.diagnostic?.summary, }; }); await writeFile( info.outputPath('full-cad-performance.json'), JSON.stringify({ loadMs, ...result }, null, 2), ); console.log('full CAD performance', { loadMs, ...result }); await page.screenshot({ path: info.outputPath('full-cad.png') }); expect(errors).toEqual([]); expect(result.diagnostic).toBeUndefined(); expect(result.simTime).toBeGreaterThan(0); });