import { expect, test, type Page } from '@playwright/test'; const MODEL = ``; 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); }); }