1eb05132f1
build / setup (compute matrix) (pull_request) Has been cancelled
build / ${{ matrix.label }} (pull_request) Has been cancelled
build / macos-15-arm64-studio (pull_request) Has been cancelled
build / ubuntu-24.04-clang-18-studio (pull_request) Has been cancelled
build / ubuntu-24.04-gcc-14-studio (pull_request) Has been cancelled
build / windows-2025-ninja-studio (pull_request) Has been cancelled
build / ubuntu-24.04-clang-18-wasm (pull_request) Has been cancelled
build / ubuntu-24.04-clang-18-mjx (pull_request) Has been cancelled
lint / pre-commit (pull_request) Has been cancelled
115 lines
5.3 KiB
TypeScript
115 lines
5.3 KiB
TypeScript
import {expect, test} from '@playwright/test';
|
|
import {fileURLToPath} from 'node:url';
|
|
|
|
const fixture = (relative: string) => fileURLToPath(new URL(`../fixtures/${relative}`, import.meta.url));
|
|
|
|
const SIMPLE_MODEL = `
|
|
<mujoco model="e2e">
|
|
<worldbody>
|
|
<light pos="0 0 3"/>
|
|
<body name="box" pos="0 0 1">
|
|
<joint name="slide" type="slide" axis="1 0 0" range="-1 1"/>
|
|
<geom name="box_geom" type="box" size=".2 .2 .2" mass="1"/>
|
|
</body>
|
|
</worldbody>
|
|
<actuator><motor name="motor" joint="slide" ctrlrange="-2 2"/></actuator>
|
|
</mujoco>`;
|
|
|
|
const LARGE_MODEL = `
|
|
<mujoco model="soak">
|
|
<option timestep=".005"/>
|
|
<worldbody>
|
|
<replicate count="6" offset=".12 0 0">
|
|
<replicate count="6" offset="0 .12 0">
|
|
<replicate count="6" offset="0 0 .12">
|
|
<body pos="-.3 -.3 .5"><joint type="free"/><geom type="sphere" size=".03" mass=".01"/></body>
|
|
</replicate>
|
|
</replicate>
|
|
</replicate>
|
|
<geom type="plane" size="2 2 .1"/>
|
|
</worldbody>
|
|
</mujoco>`;
|
|
|
|
test('显示中文平台骨架并加载单文件模型', async ({page}) => {
|
|
page.on('console', (message) => console.log(`[browser:${message.type()}] ${message.text()}`));
|
|
page.on('pageerror', (error) => console.log(`[browser:error] ${error.message}`));
|
|
page.on('requestfailed', (request) => console.log(`[browser:requestfailed] ${request.url()} ${request.failure()?.errorText}`));
|
|
page.on('response', (response) => { if (response.status() >= 400 || response.url().endsWith('.wasm')) console.log(`[browser:response] ${response.status()} ${response.url()} ${response.headers()['content-type'] ?? ''}`); });
|
|
await page.goto('/');
|
|
await expect(page.getByRole('heading', {name: 'MuJoCo Web 仿真平台'})).toBeVisible();
|
|
await expect(page.getByRole('main').getByText('拖放模型工程到此处')).toBeVisible();
|
|
await expect(page.getByRole('img',{name:'XYZ 方向指示器'})).toBeVisible();
|
|
|
|
await page.locator('input[type="file"]').first().setInputFiles({
|
|
name: 'model.xml',
|
|
mimeType: 'text/xml',
|
|
buffer: Buffer.from(SIMPLE_MODEL),
|
|
});
|
|
|
|
await expect(page.getByText('WASM 已加载')).toBeVisible({timeout: 30_000});
|
|
await expect(page.getByText('motor')).toBeVisible();
|
|
await expect(page.getByText('slide')).toBeVisible();
|
|
});
|
|
|
|
test('加载包含 include、OBJ、STL 与 PNG 的工程', async ({page}) => {
|
|
await page.goto('/');
|
|
await page.locator('input[type="file"]').first().setInputFiles([
|
|
fixture('mjcf_include/model.xml'),
|
|
fixture('mjcf_include/world.xml'),
|
|
fixture('mjcf_include/triangle.obj'),
|
|
fixture('mjcf_include/triangle.stl'),
|
|
fixture('mjcf_include/checker.png'),
|
|
]);
|
|
await expect(page.getByText('WASM 已加载')).toBeVisible({timeout: 30_000});
|
|
await expect(page.getByText('5 个文件')).toBeVisible();
|
|
});
|
|
|
|
test('加载引用 OBJ 的 URDF 工程', async ({page}) => {
|
|
await page.goto('/');
|
|
await page.locator('input[type="file"]').first().setInputFiles([
|
|
fixture('urdf_mesh/robot.urdf'),
|
|
fixture('urdf_mesh/triangle.obj'),
|
|
]);
|
|
await expect(page.getByText('WASM 已加载')).toBeVisible({timeout: 30_000});
|
|
await expect(page.getByText('2 个文件')).toBeVisible();
|
|
await expect(page.getByLabel('URDF 处理方式')).toHaveValue('mjcf');
|
|
await expect(page.getByLabel('URDF 基座类型')).toHaveValue('floating');
|
|
await expect(page.getByText(/URDF 已转换为 MJCF(浮动基座),并整体平移/)).toBeVisible();
|
|
await expect(page.getByLabel('显示碰撞几何')).not.toBeChecked();
|
|
});
|
|
|
|
test('中等规模模型持续步进并可重复加载', async ({page}) => {
|
|
await page.goto('/');
|
|
const input = page.locator('input[type="file"]').first();
|
|
const modelFile = {name:'large.xml',mimeType:'text/xml',buffer:Buffer.from(LARGE_MODEL)};
|
|
await input.setInputFiles(modelFile);
|
|
await expect(page.getByText('WASM 已加载')).toBeVisible({timeout: 30_000});
|
|
await page.getByRole('button', {name:'▶ 播放'}).click();
|
|
await page.waitForTimeout(2_000);
|
|
await expect(page.locator('footer')).not.toContainText('时间 0.000 s');
|
|
|
|
// 播放过程中重置必须同时暂停底层会话,之后仍可正常播放和暂停。
|
|
await page.getByRole('button',{name:'重置'}).click();
|
|
await expect(page.getByRole('button',{name:'▶ 播放'})).toBeVisible();
|
|
await expect(page.locator('footer')).toContainText('时间 0.000 s');
|
|
await page.getByRole('button',{name:'▶ 播放'}).click();
|
|
await page.waitForTimeout(500);
|
|
await page.getByRole('button',{name:'⏸ 暂停'}).click();
|
|
await page.waitForTimeout(200);
|
|
const pausedTime=(await page.locator('footer').innerText()).match(/时间 ([\d.]+) s/)?.[1];
|
|
expect(Number(pausedTime)).toBeGreaterThan(0);
|
|
await page.waitForTimeout(500);
|
|
expect((await page.locator('footer').innerText()).match(/时间 ([\d.]+) s/)?.[1]).toBe(pausedTime);
|
|
|
|
await input.setInputFiles(modelFile);
|
|
await expect(page.getByText('WASM 已加载')).toBeVisible({timeout: 30_000});
|
|
await expect(page.getByRole('alert')).toHaveCount(0);
|
|
});
|
|
|
|
test('无效模型显示中文诊断且保留工程树', async ({page}) => {
|
|
await page.goto('/');
|
|
await page.locator('input[type="file"]').first().setInputFiles(fixture('invalid.xml'));
|
|
await expect(page.getByRole('alert')).toContainText('模型编译失败', {timeout: 30_000});
|
|
await expect(page.getByText('invalid.xml', {exact: false}).first()).toBeVisible();
|
|
});
|