192 lines
8.8 KiB
TypeScript
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);
|
|
});
|
|
}
|