import path from 'path' import { mkdtempSync, rmSync, existsSync } from 'fs' import { join } from 'path' import { tmpdir } from 'os' import { test, expect, _electron as electron, type Page } from '@playwright/test' const ROOT = path.join(import.meta.dirname, '..') const LIGHT_THEMES = ['ricepaper', 'mist', 'teagarden'] as const async function launchApp(userDataDir: string) { return electron.launch({ args: [ROOT], env: { ...process.env, BILIN_E2E: '1', BILIN_E2E_USER_DATA: userDataDir } }) } async function skipOnboarding(page: Page): Promise { await page.waitForLoadState('domcontentloaded') await expect(page.locator('#app')).toBeVisible({ timeout: 20_000 }) if (await page.getByText('欢迎使用笔临').isVisible()) { await page.getByRole('button', { name: '跳过' }).click() await expect(page.getByText('欢迎使用笔临')).toBeHidden({ timeout: 10_000 }) } } async function openSettings(page: Page): Promise { await page.getByRole('button', { name: /系统设置/ }).click() await expect(page.locator('#settings-page')).toBeVisible() } function parseRgb(color: string): [number, number, number] | null { const m = color.match(/rgba?\((\d+),\s*(\d+),\s*(\d+)/) if (!m) return null return [Number(m[1]), Number(m[2]), Number(m[3])] } function relativeLuminance(r: number, g: number, b: number): number { const [rs, gs, bs] = [r, g, b].map((c) => { const s = c / 255 return s <= 0.03928 ? s / 12.92 : Math.pow((s + 0.055) / 1.055, 2.4) }) return 0.2126 * rs + 0.7152 * gs + 0.0722 * bs } function contrastRatio(fg: string, bg: string): number { const fgRgb = parseRgb(fg) const bgRgb = parseRgb(bg) if (!fgRgb || !bgRgb) return 0 const l1 = relativeLuminance(...fgRgb) const l2 = relativeLuminance(...bgRgb) const lighter = Math.max(l1, l2) const darker = Math.min(l1, l2) return (lighter + 0.05) / (darker + 0.05) } test.describe('Theme contrast', () => { let userDataDir: string test.beforeEach(() => { userDataDir = mkdtempSync(join(tmpdir(), 'bilin-e2e-contrast-')) }) test.afterEach(() => { if (userDataDir && existsSync(userDataDir)) { try { rmSync(userDataDir, { recursive: true, force: true, maxRetries: 3, retryDelay: 200 }) } catch { /* ignore */ } } }) for (const theme of LIGHT_THEMES) { test(`E2E-CONTRAST: ${theme} theme has readable form controls`, async () => { const app = await launchApp(userDataDir) const page = await app.firstWindow() await skipOnboarding(page) await openSettings(page) await page.getByTestId('settings-theme').selectOption(theme) await expect(page.locator('html')).toHaveAttribute('data-theme', theme) await expect(page.getByRole('heading', { name: '系统设置' })).toBeVisible() await expect(page.locator('.setting-item').first()).toBeVisible() const penNameOk = await page.evaluate(() => { const el = document.querySelector('[data-testid="settings-pen-name"]') as HTMLElement | null if (!el) return false const cs = getComputedStyle(el) const fg = cs.color const bg = cs.backgroundColor if (fg === 'rgba(0, 0, 0, 0)' || bg === 'rgba(0, 0, 0, 0)') return false if (fg === bg) return false return true }) expect(penNameOk).toBe(true) const ratio = await page.evaluate(() => { const el = document.querySelector('[data-testid="settings-pen-name"]') as HTMLElement const cs = getComputedStyle(el) return { fg: cs.color, bg: cs.backgroundColor } }) expect(contrastRatio(ratio.fg, ratio.bg)).toBeGreaterThanOrEqual(4.5) await app.close() }) } test('E2E-CONTRAST: light themes show visible headings on main page', async () => { const app = await launchApp(userDataDir) const page = await app.firstWindow() await skipOnboarding(page) await openSettings(page) for (const theme of LIGHT_THEMES) { await page.getByTestId('settings-theme').selectOption(theme) await page.locator('.logo-area').click() await expect(page.locator('#settings-page')).toBeHidden() const headingVisible = await page.evaluate(() => { const selectors = ['h1', 'h2', 'button', '.setting-item'] for (const sel of selectors) { const el = document.querySelector(`#app ${sel}`) as HTMLElement | null if (!el) continue const cs = getComputedStyle(el) if (cs.visibility === 'hidden' || cs.display === 'none') continue const fg = cs.color if (fg === 'rgba(0, 0, 0, 0)') continue return true } return false }) expect(headingVisible).toBe(true) await page.getByRole('button', { name: '⚙ 系统设置' }).click() await expect(page.locator('#settings-page')).toBeVisible() } await app.close() }) })