import { expect, test } from '@playwright/test' /** * 双列菜单:第一列选中项样式回归 —— 针对 ERP 应用。 * * 需求:layout-mixed-rail__item.is-active 的背景要比原来"深一点"、文字白色。 * * 对照 vben(menu-ui/src/components/menu.vue): * @mixin menu-item-active (375-379): * color: var(--menu-item-active-color); * background: var(--menu-item-active-background-color); * 深色档赋值 (470-471): * --menu-item-active-color: hsl(var(--accent-foreground)) ← 高对比文字 * --menu-item-active-background-color: hsl(var(--accent)) ← 强调底色 * * 判定:选中项背景应比所在列背景明显更深,且文字为亮色(对比度足够)。 */ const ERP_URL = process.env.ERP_URL ?? 'http://localhost:5090' async function useMixedNavLayout(page: import('@playwright/test').Page) { await page.goto(`${ERP_URL}/`) await page.evaluate(() => { const KEY = 'g3erp:preferences' const raw = localStorage.getItem(KEY) const pref: Record> = raw ? JSON.parse(raw) : {} const app = pref.app || {} app.layout = 'sidebar-mixed-nav' pref.app = app const sidebar = pref.sidebar || {} sidebar.enable = true sidebar.extraCollapse = false pref.sidebar = sidebar // 浅色模式,确保测的是浅色档 const theme = pref.theme || {} theme.mode = 'light' pref.theme = theme localStorage.setItem(KEY, JSON.stringify(pref)) for (const key of Object.keys(localStorage)) { if (key.indexOf('g3erp:app') === 0) localStorage.removeItem(key) } }) await page.reload() await page.waitForTimeout(400) } /** 解析 rgb(a) 字符串 → [r,g,b] */ function parseRgb(value: string): [number, number, number] { const m = value.match(/rgba?\(([^)]+)\)/) if (!m) throw new Error(`无法解析颜色: ${value}`) const parts = m[1].split(',').map((s) => Number(s.trim())) return [parts[0], parts[1], parts[2]] } /** 相对亮度(WCAG) */ function luminance([r, g, b]: [number, number, number]): number { const f = (c: number) => { const s = c / 255 return s <= 0.03928 ? s / 12.92 : ((s + 0.055) / 1.055) ** 2.4 } return 0.2126 * f(r) + 0.7152 * f(g) + 0.0722 * f(b) } /** 对比度 */ function contrast(a: [number, number, number], b: [number, number, number]): number { const la = luminance(a) const lb = luminance(b) const [hi, lo] = la > lb ? [la, lb] : [lb, la] return (hi + 0.05) / (lo + 0.05) } test.describe('ERP 双列菜单 - 图标栏选中态', () => { test('选中项背景比列背景更深,且文字是亮色(对比度足够)', async ({ page }) => { await useMixedNavLayout(page) await page.goto(`${ERP_URL}/ocean/booking`) await page.waitForTimeout(400) const rail = page.locator('.layout-mixed-rail') const active = page.locator('.layout-mixed-rail__item.is-active') await expect(active, '应有一个选中项').toHaveCount(1) const styles = await page.evaluate(() => { const railEl = document.querySelector('.layout-mixed-rail') as HTMLElement const itemEl = document.querySelector('.layout-mixed-rail__item.is-active') as HTMLElement const cs = getComputedStyle(itemEl) return { railBg: getComputedStyle(railEl).backgroundColor, itemBg: cs.backgroundColor, itemColor: cs.color, fontWeight: cs.fontWeight, } }) // eslint-disable-next-line no-console console.log('[active-style]', JSON.stringify(styles)) const railBg = parseRgb(styles.railBg) const itemBg = parseRgb(styles.itemBg) const itemColor = parseRgb(styles.itemColor) // 1) 选中背景确实比列背景更深(亮度更低) expect( luminance(itemBg), `选中背景应比列背景更深:item=${styles.itemBg} rail=${styles.railBg}`, ).toBeLessThan(luminance(railBg)) // 2) 背景不是透明(否则测的是列背景) expect(styles.itemBg, '选中背景不应是透明').not.toMatch(/rgba\(0,\s*0,\s*0,\s*0\)/) // 3) 文字与选中背景的对比度达标(可读) const ratio = contrast(itemColor, itemBg) expect(ratio, `文字/背景对比度应 >= 4.5,实际 ${ratio.toFixed(2)}`).toBeGreaterThanOrEqual(4.5) // 4) 文字比背景亮("白色/亮色文字") expect( luminance(itemColor), `文字应比选中背景更亮:text=${styles.itemColor} bg=${styles.itemBg}`, ).toBeGreaterThan(luminance(itemBg)) }) test('未选中项没有深色底(对照)', async ({ page }) => { await useMixedNavLayout(page) await page.goto(`${ERP_URL}/ocean/booking`) await page.waitForTimeout(400) const inactive = page.locator('.layout-mixed-rail__item:not(.is-active)').first() const bg = await inactive.evaluate((el) => getComputedStyle(el).backgroundColor) // 未选中项是透明(继承列的底色) expect(bg, `未选中项不应有背景填充,实际 ${bg}`).toMatch(/rgba\(0,\s*0,\s*0,\s*0\)/) }) })