f3a8a38acd
web-platform-ci / Standalone decision service (no cloud credentials) (push) Has been cancelled
web-platform-ci / TypeScript, lint, unit, build (push) Has been cancelled
web-platform-ci / Playwright E2E (push) Has been cancelled
lekiwi-compatibility / cpu-compatibility (push) Has been cancelled
web-platform-ci / Standalone decision service (no cloud credentials) (pull_request) Has been cancelled
web-platform-ci / TypeScript, lint, unit, build (pull_request) Has been cancelled
web-platform-ci / Playwright E2E (pull_request) Has been cancelled
lekiwi-compatibility / cpu-compatibility (pull_request) Has been cancelled
集成同源 BYOK 会话隔离、精简模型设置、官方订阅入口和 HTTPS 发布运维;保留本地训练/调参与控制能力。同步 npm 版本及 CHANGELOG,记录公网真实 API 验收仍待用户凭据。
77 lines
3.1 KiB
TypeScript
77 lines
3.1 KiB
TypeScript
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<void>((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);
|
|
});
|