import { expect, test } from '@playwright/test'; import { pagePath } from './helpers'; test.describe('keyboard and palette', () => { test('skip link is first and moves focus to main', async ({ page }) => { await page.goto('/'); await page.keyboard.press('Tab'); const skip = page.locator('.skip-link'); await expect(skip).toBeFocused(); await page.keyboard.press('Enter'); await expect(page.locator('#main-content')).toBeFocused(); }); test('mobile nav toggle keeps aria-expanded in sync', async ({ page, request }) => { const ssr = await request.get('/'); expect(ssr.ok()).toBe(true); const html = await ssr.text(); expect(html).toContain('aria-expanded="true"'); expect(html).toContain('id="primary-nav"'); expect(html).toContain('class="site-nav"'); expect(html).toMatch(/]*class="site"/); expect(html).not.toMatch(/]*class="[^"]*\bsite\b[^"]*\bnav-collapsed\b/); await page.setViewportSize({ width: 390, height: 844 }); await page.goto('/'); const toggle = page.locator('.nav-toggle'); await expect(toggle).toBeVisible(); await expect(toggle).toHaveAttribute('aria-expanded', 'false'); await toggle.click(); await expect(toggle).toHaveAttribute('aria-expanded', 'true'); await toggle.click(); await expect(toggle).toHaveAttribute('aria-expanded', 'false'); }); test('collapsed header stays under 25% of the viewport and is not sticky below md', async ({ page, }) => { const viewport = { width: 390, height: 844 }; await page.setViewportSize(viewport); await page.goto('/'); const header = page.locator('.site-header'); const box = await header.boundingBox(); expect(box).toBeTruthy(); expect(box!.height).toBeLessThan(viewport.height * 0.25); await expect .poll(async () => header.evaluate((element) => getComputedStyle(element).position)) .not.toBe('sticky'); await page.locator('.nav-toggle').click(); await expect(page.locator('.nav-toggle')).toHaveAttribute('aria-expanded', 'true'); expect(await header.evaluate((element) => getComputedStyle(element).position)).not.toBe( 'sticky', ); for (const width of [768, 820, 1024, 1280, 1440]) { await page.setViewportSize({ width, height: 900 }); await page.goto('/'); expect( await header.evaluate((element) => getComputedStyle(element).position), `header must not be sticky at ${width}px`, ).not.toBe('sticky'); } }); test('compact services submenu stays keyboard accessible at 1024px', async ({ page, }, testInfo) => { test.skip(testInfo.project.name === 'mobile', 'compact submenu applies from md up'); await page.setViewportSize({ width: 1024, height: 900 }); await page.goto('/'); const servicesParent = page.locator( `.primary-nav > li > a[href="${pagePath('services', 'de')}"]`, ); const firstChild = page.locator( `.primary-nav > li > .submenu a[href="${pagePath('servicesSoftware', 'de')}"]`, ); await servicesParent.focus(); await expect(firstChild).toBeVisible(); await page.keyboard.press('Tab'); await expect(firstChild).toBeFocused(); const headerBox = await page.locator('.site-header').boundingBox(); expect(headerBox, 'header while submenu is open').toBeTruthy(); expect(headerBox!.height, 'header height while submenu is open').toBeLessThanOrEqual(200); }); test('palette opens with Control+K, traps focus, locks scroll and restores on Escape', async ({ page, }) => { await page.goto('/'); const trigger = page.locator('.command-palette-trigger'); await trigger.focus(); await page.keyboard.press('Control+k'); const dialog = page.locator('[role="dialog"]'); await expect(dialog).toBeVisible(); await expect(page.locator('.site')).toHaveAttribute('inert', ''); expect(await page.evaluate(() => document.body.style.overflow)).toBe('hidden'); const focusable = dialog.locator( 'a[href], button:not([disabled]), input:not([disabled]), textarea:not([disabled]), select:not([disabled])', ); const count = await focusable.count(); const first = focusable.first(); const last = focusable.nth(count - 1); await last.focus(); await page.keyboard.press('Tab'); await expect(first).toBeFocused(); await page.keyboard.press('Shift+Tab'); await expect(last).toBeFocused(); await page.keyboard.press('Escape'); await expect(dialog).toHaveCount(0); await expect(page.locator('.site')).not.toHaveAttribute('inert'); expect(await page.evaluate(() => document.body.style.overflow)).toBe(''); await expect(trigger).toBeFocused(); }); });