import { expect, test } from '@playwright/test'; import { pagePath } from './helpers'; test.describe('keyboard and terminal dock', () => { 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); const background = await firstChild.evaluate((element) => { const submenu = element.closest('.submenu'); return submenu ? getComputedStyle(submenu).backgroundColor : ''; }); expect(background, 'submenu background must be fully opaque').toMatch( /^rgb\(\d+,\s*\d+,\s*\d+\)$/, ); }); test('terminal dock opens with Control+K without locking the page and restores on Escape', async ({ page, }) => { await page.goto('/'); const trigger = page.locator('.terminal-dock-trigger'); await trigger.focus(); await page.keyboard.press('Control+k'); const panel = page.locator('.terminal-dock-panel'); await expect(panel).toBeVisible(); await expect(page.locator('.site')).not.toHaveAttribute('inert'); expect(await page.evaluate(() => document.body.style.overflow)).not.toBe('hidden'); await expect(page.locator('[role="dialog"]')).toHaveCount(0); await page.keyboard.press('Escape'); await expect(panel).toHaveCount(0); await expect(trigger).toBeFocused(); }); });