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();
});
});