feat(web-platform): release V0.9.4 前端设计优化
web-platform-ci / TypeScript, lint, unit, build (push) Has been cancelled
web-platform-ci / Playwright E2E (push) Has been cancelled
web-platform-ci / TypeScript, lint, unit, build (pull_request) Has been cancelled
web-platform-ci / Playwright E2E (pull_request) Has been cancelled

This commit is contained in:
2026-09-09 13:15:46 +08:00
parent 13e35be98b
commit cb3fb47561
31 changed files with 563 additions and 182 deletions
-4
View File
@@ -264,7 +264,6 @@ test('转换后的 MJCF 可编辑并重新载入', async ({ page }) => {
.first()
.setInputFiles({ name: 'model.xml', mimeType: 'text/xml', buffer: Buffer.from(SIMPLE_MODEL) });
await expect(page.getByRole('button', { name: '▶ 播放' })).toBeEnabled({ timeout: 30_000 });
await page.getByRole('button', { name: '工程', exact: true }).click();
await page.getByRole('button', { name: '源代码' }).click();
const dialog = page.getByRole('dialog', { name: '转换后的 MJCF 编辑器' });
await expect(dialog).toBeVisible();
@@ -291,7 +290,6 @@ test('关闭已修改的 MJCF 前要求确认', async ({ page }) => {
.first()
.setInputFiles({ name: 'model.xml', mimeType: 'text/xml', buffer: Buffer.from(SIMPLE_MODEL) });
await expect(page.getByRole('button', { name: '▶ 播放' })).toBeEnabled({ timeout: 30_000 });
await page.getByRole('button', { name: '工程', exact: true }).click();
await page.getByRole('button', { name: '源代码' }).click();
const editorDialog = page.getByRole('dialog', { name: '转换后的 MJCF 编辑器' });
await editorDialog.locator('.monaco-editor').click({ position: { x: 240, y: 120 } });
@@ -354,7 +352,6 @@ test('加载引用 OBJ 的 URDF 工程', async ({ page }) => {
await expect(notifications).toContainText(/URDF 已转换为 MJCF(浮动基座),并整体平移/);
await page.keyboard.press('Escape');
await expect(page.getByLabel('显示碰撞几何')).not.toBeChecked();
await page.getByRole('button', { name: '工程', exact: true }).click();
await page.getByRole('button', { name: '源代码' }).click();
const sourceDialog = page.getByRole('dialog', { name: '转换后的 MJCF 编辑器' });
await expect(sourceDialog.getByText('缓存文件 · 可编辑')).toBeVisible();
@@ -434,7 +431,6 @@ test('转换后的 MJCF 保存时保留 DAE 转换缓存资源', async ({ page }
.getByRole('button', { name: '转换并加载' })
.click();
await expect(page.getByRole('button', { name: '▶ 播放' })).toBeEnabled({ timeout: 30_000 });
await page.getByRole('button', { name: '工程', exact: true }).click();
await page.getByRole('button', { name: '源代码' }).click();
const dialog = page.getByRole('dialog', { name: '转换后的 MJCF 编辑器' });
await dialog.locator('.monaco-editor').click({ position: { x: 240, y: 120 } });
+114
View File
@@ -0,0 +1,114 @@
import { expect, test } from '@playwright/test';
const MODEL = `<mujoco model="hud"><worldbody><light pos="0 0 3"/>
<geom type="plane" size="3 3 .1"/><body pos="0 0 1"><freejoint/>
<geom type="box" size=".2 .2 .2" mass="1"/></body></worldbody></mujoco>`;
for (const theme of ['dark', 'light']) {
test(`Cyber HUD 材质与可读性 ${theme}`, async ({ page }, info) => {
await page.setViewportSize({ width: 1440, height: 900 });
await page.addInitScript((value) => {
if (!localStorage.getItem('mujoco-platform-theme')) {
localStorage.setItem('mujoco-platform-theme', value);
}
}, theme);
await page.goto('/');
const welcome = page.getByRole('region', { name: '导入模型工程' });
await expect(welcome).toBeVisible();
await expect(page.locator('.cyber-workspace')).toHaveClass(new RegExp(`theme-${theme}`));
await expect(welcome).toHaveCSS('backdrop-filter', 'blur(12px) saturate(1.2)');
const primary = page.getByRole('button', { name: '选择文件', exact: true });
await primary.focus();
await expect(primary).toBeFocused();
await expect(primary).toHaveCSS('outline-style', 'solid');
// 在真实浏览器中验证主按钮与正文的计算色对比度,而非仅比较 token。
const contrast = await primary.evaluate((button) => {
function luminance(color: string) {
const rgb = color
.match(/[\d.]+/g)!
.slice(0, 3)
.map(Number);
const linear = rgb.map((value) => {
const v = value / 255;
return v <= 0.04045 ? v / 12.92 : ((v + 0.055) / 1.055) ** 2.4;
});
return linear[0] * 0.2126 + linear[1] * 0.7152 + linear[2] * 0.0722;
}
const style = getComputedStyle(button);
const a = luminance(style.color),
b = luminance(style.backgroundColor);
return (Math.max(a, b) + 0.05) / (Math.min(a, b) + 0.05);
});
expect(contrast).toBeGreaterThanOrEqual(4.5);
const disabled = page.getByRole('button', { name: '源代码', exact: true });
await expect(disabled).toBeDisabled();
await expect(disabled).toHaveCSS('box-shadow', 'none');
await page.screenshot({
path: info.outputPath(`workbench-${theme}.png`),
animations: 'disabled',
});
await page.goto('/tuning.html');
await expect(page.locator('.cyber-workspace')).toHaveClass(new RegExp(`theme-${theme}`));
await page
.getByRole('button', { name: `切换到${theme === 'dark' ? '白天' : '黑夜'}主题` })
.click();
await page.reload();
await expect(page.locator('.cyber-workspace')).toHaveClass(
new RegExp(`theme-${theme === 'dark' ? 'light' : 'dark'}`),
);
});
}
test('运行状态呼吸遵循减少动态效果,装饰不拦截交互', async ({ page }) => {
await page.goto('/');
await page.locator('#mujoco-project-files').setInputFiles({
name: 'hud.xml',
mimeType: 'text/xml',
buffer: Buffer.from(MODEL),
});
const play = page.getByRole('button', { name: '▶ 播放' });
await expect(play).toBeEnabled({ timeout: 30_000 });
await play.click();
const signal = page.locator('.hud-signal');
await expect(signal).toHaveAttribute('data-running', 'true');
const animation = () => signal.evaluate((el) => getComputedStyle(el, '::after').animationName);
expect(await animation()).toBe('hud-breathe');
expect(await signal.evaluate((el) => getComputedStyle(el, '::after').pointerEvents)).toBe('none');
await page.emulateMedia({ reducedMotion: 'reduce' });
await expect.poll(animation).toBe('none');
await page.getByRole('button', { name: '⏸ 暂停' }).click();
await expect(signal).toHaveAttribute('data-running', 'false');
await page.emulateMedia({ forcedColors: 'active' });
await expect(page.locator('.engineering-glass').first()).toHaveCSS('backdrop-filter', 'none');
});
for (const theme of ['dark', 'light']) {
test(`无模糊能力时使用实色回退 ${theme}`, async ({ page }) => {
// Chromium 支持模糊:使构建 CSS 的能力分支为 false,验证回退规则本身。
let replaced = false;
await page.route('**/*.css', async (route) => {
const response = await route.fetch();
const css = await response.text();
const fallback = css.replace(/backdrop-filter:\s*blur\(1px\)/g, 'cyber-unsupported: none');
replaced ||= css !== fallback;
await route.fulfill({ response, body: fallback });
});
await page.addInitScript(
(value) => localStorage.setItem('mujoco-platform-theme', value),
theme,
);
await page.goto('/');
const welcome = page.getByRole('region', { name: '导入模型工程' });
await expect(welcome).toBeVisible();
expect(replaced).toBe(true);
await expect(welcome).toHaveCSS('backdrop-filter', 'none');
await expect(welcome).toHaveCSS(
'background-color',
theme === 'dark' ? 'rgb(11, 18, 36)' : 'rgb(251, 252, 254)',
);
await page.getByRole('button', { name: '打开命令面板' }).click();
await expect(page.getByRole('dialog', { name: '命令面板' })).toBeVisible();
await page.keyboard.press('Escape');
await expect(page.getByRole('dialog', { name: '命令面板' })).toBeHidden();
});
}
+19 -4
View File
@@ -72,7 +72,24 @@ for (const theme of ['dark', 'light'] as const)
await page.getByRole('button', { name: '连接/刷新' }).click();
await expect(page.getByRole('img', { name: /收敛曲线/ })).toBeVisible();
await expect(page.getByRole('button', { name: '停止 Session' })).toBeVisible();
await expect(page.getByRole('button', { name: /决策与审批 · 待审批 1/ })).toBeVisible();
const sessions = page.getByRole('region', { name: '会话与 Trial' });
const metrics = page.getByRole('main', { name: '指标与排行' });
const decisions = page.getByRole('complementary', { name: '决策与审批' });
await expect(sessions).toBeVisible();
await expect(metrics).toBeVisible();
await expect(decisions).toBeVisible();
const left = (await sessions.boundingBox())!;
const center = (await metrics.boundingBox())!;
const right = (await decisions.boundingBox())!;
if (width >= 1280) {
expect(Math.abs(left.y - center.y)).toBeLessThan(2);
expect(Math.abs(center.y - right.y)).toBeLessThan(2);
expect(left.x + left.width).toBeLessThanOrEqual(center.x + 1);
expect(center.x + center.width).toBeLessThanOrEqual(right.x + 1);
} else {
expect(left.y + left.height).toBeLessThanOrEqual(center.y + 1);
expect(center.y + center.height).toBeLessThanOrEqual(right.y + 1);
}
await page.getByRole('button', { name: '收敛曲线放大', exact: true }).click();
await page
.getByRole('button', { name: `切换到${theme === 'dark' ? '白天' : '黑夜'}主题` })
@@ -88,12 +105,11 @@ for (const theme of ['dark', 'light'] as const)
await page.getByRole('heading', { name: 'Trial Leaderboard' }).scrollIntoViewIfNeeded();
await noOverflow(page);
await page.screenshot({ path: info.outputPath('tuning-leaderboard.png') });
await page.getByRole('button', { name: '会话与 Trial' }).click();
await page.getByRole('button', { name: '导入', exact: true }).scrollIntoViewIfNeeded();
await expect(page.getByRole('button', { name: '导入', exact: true })).toBeVisible();
await expect(page.getByText('导入会替换主工作台策略;无主窗口时下载文件。')).toBeVisible();
await noOverflow(page);
await page.screenshot({ path: info.outputPath('tuning-sessions.png') });
await page.getByRole('button', { name: /决策与审批/ }).click();
await page.getByRole('button', { name: 'Monaco Diff 审查' }).scrollIntoViewIfNeeded();
await noOverflow(page);
await page.screenshot({ path: info.outputPath('tuning-decisions.png') });
@@ -204,7 +220,6 @@ for (const theme of ['dark', 'light'] as const)
await expect(page.getByRole('dialog', { name: '诊断与事件日志' })).toBeVisible();
await page.screenshot({ path: info.outputPath('diagnostics.png') });
await page.keyboard.press('Escape');
await page.getByRole('button', { name: '工程', exact: true }).click();
await page.getByRole('button', { name: '源代码', exact: true }).click();
const source = page.getByRole('dialog', { name: '转换后的 MJCF 编辑器' });
await expect(source.locator('.monaco-editor')).toBeVisible();