124 lines
4.6 KiB
TypeScript
124 lines
4.6 KiB
TypeScript
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(/<div[^>]*class="site"/);
|
|
expect(html).not.toMatch(/<div[^>]*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();
|
|
});
|
|
});
|