Files
Mujoco_WASM/web_platform/e2e/ui-domain-panels.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

192 lines
8.8 KiB
TypeScript

import { expect, test, type Page } from '@playwright/test';
const MODEL = `<mujoco model="domain-panels"><worldbody><geom type="plane" size="3 3 .1"/><body name="box" pos="0 0 1"><joint name="slide" type="slide" axis="1 0 0" range="-1 1"/><geom type="box" size=".2 .2 .2" mass="1"/></body></worldbody><actuator><motor name="motor" joint="slide" ctrlrange="-2 2"/></actuator></mujoco>`;
async function load(page: Page) {
await page.goto('/');
await page
.locator('#mujoco-project-files')
.setInputFiles({ name: 'domain.xml', mimeType: 'text/xml', buffer: Buffer.from(MODEL) });
await expect(page.getByRole('button', { name: '▶ 播放' })).toBeEnabled();
}
async function left(page: Page) {
const button = page.getByRole('button', { name: '显示工程面板', exact: true });
if (await button.isVisible()) await button.click();
}
async function right(page: Page) {
const button = page.getByRole('button', { name: '显示右侧面板', exact: true });
if (await button.isVisible()) await button.click();
}
async function screenshot(page: Page, name: string) {
expect(await page.evaluate(() => document.documentElement.scrollWidth <= innerWidth)).toBe(true);
await page.screenshot({
path: test.info().outputPath(name + '.png'),
mask: [page.getByLabel('视口状态')],
});
}
for (const theme of ['dark', 'light']) {
for (const [width, height] of [
[1920, 1080],
[1440, 900],
[1366, 768],
[1024, 768],
[768, 800],
]) {
test(`领域检查器与录制 ${theme} ${width}`, async ({ page }) => {
const errors: string[] = [];
page.on('pageerror', (error) => errors.push(error.message));
await page.setViewportSize({ width, height });
await page.addInitScript(
(value) => localStorage.setItem('mujoco-platform-theme', value),
theme,
);
await load(page);
await left(page);
await page
.getByRole('navigation', { name: '模型结构树' })
.getByRole('treeitem', { name: /box/ })
.first()
.click();
await expect(page.getByText(/^Body #/)).toBeVisible();
await expect(page.getByRole('button', { name: '标识与层级' })).toHaveAttribute(
'aria-expanded',
'false',
);
await screenshot(page, 'body');
await page
.getByRole('tabpanel', { name: '检查器' })
.getByRole('button', { name: /slide/ })
.click();
await expect(page.getByRole('slider', { name: 'slide' })).toBeVisible();
await page.getByRole('slider', { name: 'slide' }).focus();
await page.keyboard.press('ArrowRight');
await expect(page.getByRole('slider', { name: 'slide' })).not.toHaveValue('0');
await page.getByText('增益与输出限幅', { exact: true }).click();
await expect(page.getByText(/参数立即作用于模型/)).toBeVisible();
await screenshot(page, 'joint-actuator');
await page.getByRole('tab', { name: '数据录制' }).click();
await page.getByRole('button', { name: '开始记录' }).click();
await expect(page.getByText('记录中', { exact: true })).toBeVisible();
await expect(page.getByLabel('采样频率', { exact: true })).toBeDisabled();
await page.getByRole('button', { name: '▶ 播放' }).click();
await page.waitForTimeout(150);
await page.getByRole('button', { name: '⏸ 暂停' }).click();
await page.getByRole('button', { name: '停止记录' }).click();
await page.getByText('实时运动状态', { exact: true }).click();
await expect(page.getByRole('button', { name: '导出 CSV' })).toBeEnabled();
await screenshot(page, 'recording');
await left(page);
await page.getByLabel('地图资产库').getByRole('button', { name: '添加基础方盒' }).click();
await page.getByRole('tab', { name: '检查器' }).click();
await expect(page.getByLabel('对象参数sizeX', { exact: true })).toBeVisible();
await expect(page.getByText('表面材质', { exact: true }).locator('..')).not.toHaveAttribute(
'open',
);
await page.getByLabel('对象参数sizeX', { exact: true }).fill('1.5');
await page.getByLabel('对象参数sizeX', { exact: true }).press('Enter');
await expect(page.getByLabel('地图草稿状态')).toContainText('未保存');
await screenshot(page, 'map-draft');
if (width === 1440) {
await page
.getByLabel('对象参数sizeX', { exact: true })
.locator('..')
.getByRole('button')
.focus();
await expect(page.getByRole('tooltip')).toContainText('Shift 精调');
const tip = await page.getByRole('tooltip').boundingBox();
expect(tip!.x + tip!.width).toBeLessThanOrEqual(width);
await screenshot(page, 'map-keyboard-tooltip');
await page.keyboard.press('Escape');
await expect(page.getByRole('tooltip')).toHaveCount(0);
}
expect(errors).toEqual([]);
});
}
test(`训练折叠详情与跨视图状态 ${theme}`, async ({ page }) => {
await page.setViewportSize({ width: 1440, height: 900 });
await page.addInitScript(
(value) => localStorage.setItem('mujoco-platform-theme', value),
theme,
);
let starts = 0;
let offline = true;
const job = {
id: 'a'.repeat(32),
taskId: 'Unitree-Go2-Flat',
state: 'running',
progress: 0.4,
iteration: 4,
maxIterations: 10,
message: '模拟训练中,未启动真实作业',
artifactReady: false,
logs: ['Learning iteration 4 / 10', 'Mean value loss: 0.9'],
};
await page.route('http://127.0.0.1:8765/**', async (route) => {
const request = route.request();
if (offline) return route.fulfill({ status: 503, json: { error: '模拟服务断连' } });
if (request.url().endsWith('/health'))
return route.fulfill({
json: {
ready: true,
trainerRoot: '/mock/trainer',
tasks: ['Unitree-Go2-Flat'],
taskMetadata: [
{
id: 'Unitree-Go2-Flat',
name: 'Go2 平地',
terrainPresets: ['plane'],
terrainParameters: { size: { min: 4, max: 30, default: 8 } },
sensorParameters: {},
browserCompatible: true,
},
],
},
});
if (request.url().endsWith('/presets')) return route.fulfill({ json: { presets: [] } });
if (request.method() === 'POST') starts++;
return route.fulfill({ json: job });
});
await load(page);
await right(page);
await page.getByRole('tab', { name: '控制台' }).click();
const tools = page.getByRole('tabpanel', { name: '控制台' });
const group = tools.getByRole('button', { name: /强化学习任务/ });
await group.click();
await page.getByLabel('训练服务访问令牌').fill('mock-token');
await tools.getByRole('button', { name: '连接', exact: true }).click();
await expect(tools.getByRole('alert')).toContainText('模拟服务断连');
await screenshot(page, 'training-disconnected');
offline = false;
await tools.getByRole('button', { name: '连接', exact: true }).click();
await page.getByLabel('训练地形', { exact: true }).selectOption('plane');
await tools.getByRole('button', { name: '地形详细参数' }).click();
await page.getByLabel('地图尺寸 m', { exact: true }).fill('100');
await tools.getByRole('button', { name: '地形详细参数' }).click();
await tools.getByRole('button', { name: '发起本地训练' }).click();
await expect(tools.getByRole('alert')).toContainText('超出允许范围');
await expect(tools.getByRole('alert')).toBeInViewport();
await expect(page.getByLabel('地图尺寸 m', { exact: true })).toBeVisible();
expect(starts).toBe(0);
await screenshot(page, 'training-validation');
await page.getByLabel('地图尺寸 m', { exact: true }).fill('12');
await page.getByLabel('运行名称', { exact: true }).fill('preserved');
await group.click();
await page.getByRole('tab', { name: '检查器' }).click();
await page.getByRole('tab', { name: '数据录制' }).click();
await page.getByRole('tab', { name: '控制台' }).click();
await group.click();
await expect(page.getByLabel('运行名称', { exact: true })).toHaveValue('preserved');
await expect(page.getByLabel('训练服务访问令牌')).toHaveValue('mock-token');
await tools.getByRole('button', { name: '发起本地训练' }).click();
await expect(tools.getByRole('button', { name: '停止训练' })).toBeVisible();
await screenshot(page, 'training-running');
await group.click();
await expect(group).toContainText('训练中');
await page.getByRole('tab', { name: '检查器' }).click();
await page.getByRole('tab', { name: '控制台' }).click();
await expect(group).toContainText('训练中');
expect(starts).toBe(1);
});
}