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