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
115 lines
5.1 KiB
TypeScript
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();
|
|
});
|
|
}
|