import { expect, test } from '@playwright/test' async function login(page: import('@playwright/test').Page) { await page.goto('http://127.0.0.1:5173/login') await page.getByLabel(/ایمیل|Email/).fill('designer@microlearn.test') await page.getByLabel(/رمز عبور|Password/).fill('password') await page.getByRole('button', { name: /ورود|Sign in/ }).click() await expect(page).toHaveURL(/\/app\/dashboard$/) } test('command palette traps focus and restores its trigger', async ({ page }) => { await login(page) const trigger = page.getByRole('button', { name: 'جست‌وجو و فرمان سریع' }) await trigger.focus(); await trigger.click() const dialog = page.getByRole('dialog', { name: 'پالت فرمان' }) const input = dialog.getByPlaceholder('جست‌وجوی صفحه یا فرمان…') await expect(input).toBeFocused() await page.keyboard.press('Shift+Tab') await expect(dialog.getByRole('button').last()).toBeFocused() await page.keyboard.press('Tab') await expect(input).toBeFocused() await page.keyboard.press('Escape') await expect(dialog).toHaveCount(0) await expect(trigger).toBeFocused() }) test('library tabs support arrows, Home, and End', async ({ page }) => { await login(page) await page.goto('http://127.0.0.1:5173/app/library') const tabs = page.getByRole('tablist', { name: 'نوع فایل' }) const all = tabs.getByRole('tab', { name: 'همه' }) await all.focus(); await all.press('ArrowRight') await expect(tabs.getByRole('tab', { name: 'تصاویر' })).toBeFocused() await expect(tabs.getByRole('tab', { name: 'تصاویر' })).toHaveAttribute('aria-selected', 'true') await page.keyboard.press('End') await expect(tabs.getByRole('tab', { name: 'اسناد' })).toBeFocused() await page.keyboard.press('Home') await expect(all).toBeFocused() }) test('semantic colors meet contrast targets and reduced motion is honored', async ({ page }) => { await page.emulateMedia({ reducedMotion: 'reduce' }) await page.goto('http://127.0.0.1:5173/login') for (const theme of ['light', 'dark'] as const) { await page.evaluate(value => window.localStorage.setItem('microlearn.theme', value), theme) await page.reload() await expect(page.locator('html')).toHaveAttribute('data-theme', theme) const result = await page.evaluate(() => { const style = getComputedStyle(document.documentElement) const rgb = (value: string) => { const hex = value.trim().replace('#', '') return [0, 2, 4].map(offset => Number.parseInt(hex.slice(offset, offset + 2), 16)) } const luminance = (value: string) => { const channels = rgb(value).map(channel => channel / 255).map(channel => channel <= .04045 ? channel / 12.92 : ((channel + .055) / 1.055) ** 2.4) return .2126 * channels[0] + .7152 * channels[1] + .0722 * channels[2] } const contrast = (foreground: string, background: string) => { const [lighter, darker] = [luminance(foreground), luminance(background)].sort((a, b) => b - a) return (lighter + .05) / (darker + .05) } const token = (name: string) => style.getPropertyValue(name) const probe = document.createElement('button') probe.className = 'button' document.body.append(probe) const transition = getComputedStyle(probe).transitionDuration.split(',').map(value => value.trim().endsWith('ms') ? Number.parseFloat(value) / 1000 : Number.parseFloat(value)) probe.remove() return { text: contrast(token('--text'), token('--surface')), muted: contrast(token('--text-muted'), token('--surface')), focus: contrast(token('--focus'), token('--canvas')), maxTransitionSeconds: Math.max(...transition), } }) expect(result.text, `${theme} primary text contrast`).toBeGreaterThanOrEqual(4.5) expect(result.muted, `${theme} secondary text contrast`).toBeGreaterThanOrEqual(4.5) expect(result.focus, `${theme} focus indicator contrast`).toBeGreaterThanOrEqual(3) expect(result.maxTransitionSeconds, `${theme} reduced motion`).toBeLessThanOrEqual(.001) } })