Files
chenlin cb3fb47561
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
feat(web-platform): release V0.9.4 前端设计优化
2026-09-09 13:15:46 +08:00

115 lines
5.1 KiB
TypeScript

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();
});
}