Files
Mujoco_WASM/web_platform/e2e/multiRing.spec.ts
T
chenlin 13e35be98b
web-platform-ci / TypeScript, lint, unit, build (push) Has been cancelled
web-platform-ci / Playwright E2E (push) Has been cancelled
feat(web-platform): release V0.9.3 全模块界面重构
2026-09-08 17:57:57 +08:00

159 lines
7.8 KiB
TypeScript

import { expect, test } from '@playwright/test';
import { readFileSync } from 'node:fs';
import { resolve } from 'node:path';
const fixture = JSON.parse(
readFileSync(resolve('web_platform/src/rl/fixtures/multiRingDeployment.json'), 'utf8'),
);
// Original Go2 collision/inertia model, visual meshes omitted to keep the smoke fixture lightweight.
const go2 = readFileSync(
resolve('training_server/rl/src/assets/robots/unitree_go2/xmls/go2.xml'),
'utf8',
)
.replace(/<mesh\b[^>]*\/>/g, '')
.replace(/<geom\b[^>]*\bmesh="[^"]*"[^>]*\/>/g, '');
let model = readFileSync(
process.env.GO2_MULTI_SMOKE_POLICY ??
resolve('web_platform/fixtures/obstacle/multi-zero-action.onnx'),
);
test('训练作业一键导入:真实WASM地图+97维ORT+PiP+射线开关', async ({ page }) => {
await page.addInitScript(() => {
localStorage.setItem('mujoco-local-training-job-id', 'a'.repeat(32));
sessionStorage.setItem('mujoco-local-training-token', 'test');
});
const job = {
id: 'a'.repeat(32),
taskId: 'Unitree-Go2-ObstacleAvoidance',
state: 'succeeded',
artifactReady: true,
progress: 1,
iteration: 1,
maxIterations: 1,
logs: [
'Learning iteration 0 / 1',
'Mean value loss: 0.9',
'Mean surrogate loss: -0.1',
'Mean entropy loss: -1',
'Mean reward: 2',
'Mean episode length: 20',
'Learning iteration 1 / 1',
'Mean value loss: 0.5',
'Mean surrogate loss: -0.2',
'Mean entropy loss: -0.8',
'Mean reward: 3',
'Mean episode length: 30',
],
message: '测试专用零动作策略',
deployment: fixture,
};
await page.route('http://127.0.0.1:8765/**', (route) => {
const url = route.request().url();
if (url.endsWith('/policy.onnx'))
return route.fulfill({ contentType: 'application/octet-stream', body: model });
if (url.endsWith('/health'))
return route.fulfill({
json: { ready: true, trainerRoot: '/test', tasks: [job.taskId], activeJobId: job.id },
});
if (url.includes('/presets')) return route.fulfill({ json: { presets: [] } });
return route.fulfill({ json: job });
});
await page.goto('/');
await page
.locator('input[type="file"]')
.first()
.setInputFiles({ name: 'go2.xml', mimeType: 'text/xml', buffer: Buffer.from(go2) });
await expect(page.getByRole('button', { name: '▶ 播放' })).toBeEnabled({ timeout: 30_000 });
await page.getByRole('tab', { name: '控制台' }).click();
const tools = page.getByRole('tabpanel', { name: '控制台' });
await tools.getByRole('button', { name: /强化学习任务/ }).click();
await tools.getByRole('button', { name: '连接', exact: true }).click();
await expect(tools.getByRole('button', { name: '导入策略' })).toBeEnabled();
await tools.getByRole('button', { name: '导入策略' }).click();
await expect(page.getByText('训练配套物理地图', { exact: false })).toBeVisible({
timeout: 30_000,
});
await expect(page.getByLabel('摄像头画面', { exact: true })).toBeVisible();
const rays = page.getByRole('checkbox', { name: '显示避障射线' });
await expect(rays).toBeChecked();
await rays.uncheck();
await rays.check();
const section = tools.getByRole('button', { name: /ONNX 策略运行/ });
if ((await section.getAttribute('aria-expanded')) === 'false') await section.click();
await expect(tools.getByText('97 / 12')).toBeVisible();
await expect(tools.getByText('Go2 前视射线避障导航', { exact: true })).toBeVisible();
await tools.getByText('推理详情', { exact: true }).click();
const count = tools.getByText('推理次数', { exact: true }).locator('..');
await expect
.poll(async () => Number((await count.textContent())?.replace(/\D/g, '')))
.toBeGreaterThan(0);
const pause = page.getByRole('button', { name: '⏸ 暂停' });
if (await pause.isVisible()) await pause.click();
const stop = tools.getByRole('button', { name: '停止', exact: true });
if (await stop.isVisible()) await stop.click();
await section.evaluate((element) => element.scrollIntoView({ block: 'start' }));
await page.screenshot({ path: test.info().outputPath('onnx-policy.png') });
const targetRow = tools.getByText('当前目标 (X, Y) m', { exact: true }).locator('..');
const initialTarget = await targetRow.textContent();
const selection = () => page.locator('[aria-selected="true"]').allTextContents();
const beforeSelection = await selection();
await tools.getByRole('button', { name: '设定目标', exact: true }).click();
await page.keyboard.press('Escape');
await expect(tools.getByRole('button', { name: '设定目标', exact: true })).toHaveAttribute(
'aria-pressed',
'false',
);
await expect(targetRow).toHaveText(initialTarget!);
await tools.getByRole('button', { name: '设定目标', exact: true }).click();
const canvas = page.locator('.viewport-shell canvas').first();
const bounds = (await canvas.boundingBox())!;
await canvas.click({ position: { x: bounds.width * 0.5, y: bounds.height * 0.55 } });
await expect(targetRow).not.toHaveText(initialTarget!);
await expect(tools.getByRole('button', { name: '设定目标', exact: true })).toHaveAttribute(
'aria-pressed',
'false',
);
expect(await selection()).toEqual(beforeSelection);
await expect(tools.getByRole('button', { name: '启用', exact: true })).toBeVisible();
await tools.getByRole('button', { name: '复位目标点' }).click();
await expect(targetRow).toHaveText(initialTarget!);
await tools.getByRole('button', { name: '导入策略' }).click();
await expect(tools.getByRole('alert')).toHaveCount(0);
// Switch real valid graph/metadata pairs in both directions, without relaxing transactions.
const multiModel = model;
job.deployment = JSON.parse(
readFileSync(resolve('web_platform/src/rl/fixtures/obstacleDeployment.json'), 'utf8'),
);
model = readFileSync(resolve('web_platform/fixtures/obstacle/zero-action.onnx'));
await tools.getByRole('button', { name: '连接', exact: true }).click();
await tools.getByRole('button', { name: '导入策略' }).click();
await expect(tools.getByText('81 / 12')).toBeVisible();
job.deployment = fixture;
model = multiModel;
await tools.getByRole('button', { name: '连接', exact: true }).click();
await tools.getByRole('button', { name: '导入策略' }).click();
await expect(tools.getByText('97 / 12')).toBeVisible();
const pauseAgain = page.getByRole('button', { name: '⏸ 暂停' });
if (await pauseAgain.isVisible()) await pauseAgain.click();
const stopAgain = tools.getByRole('button', { name: '停止', exact: true });
if (await stopAgain.isVisible()) await stopAgain.click();
// A real 81-feature ONNX graph with valid 97 metadata must fail transactionally.
model = readFileSync(resolve('web_platform/fixtures/obstacle/multi-wrong-graph.onnx'));
await tools.getByRole('button', { name: '导入策略' }).click();
await expect(tools.getByRole('alert')).toContainText('维度');
await expect(tools.getByText('97 / 12')).toBeVisible();
await page.screenshot({ path: test.info().outputPath('onnx-contract-error.png') });
await expect(targetRow).toHaveText(initialTarget!);
await tools.getByRole('button', { name: '设定目标', exact: true }).click();
await tools.getByRole('button', { name: '卸载', exact: true }).click();
await expect(targetRow).toHaveCount(0);
await expect(tools.locator('.uplot')).toHaveCount(0);
await tools.getByRole('button', { name: /训练指标趋势/ }).click();
await expect(tools.locator('.uplot canvas')).toHaveCount(1);
await tools.getByRole('tab', { name: '综合', exact: true }).click();
await expect(tools.locator('.uplot canvas')).toHaveCount(5);
await page.screenshot({ path: test.info().outputPath('training-metrics.png') });
await tools.getByRole('button', { name: /训练指标趋势/ }).click();
await expect(tools.locator('.uplot')).toHaveCount(0);
});