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
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:
@@ -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 } });
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
}
|
||||
@@ -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();
|
||||
|
||||
Reference in New Issue
Block a user