Тестирование клавиатурной навигации
Updated Jul 2026
Почему тестирование клавиатуры важно
Если ваш процесс оформления заказа нельзя пройти только с клавиатуры, вы исключаете пользователей, которые не могут пользоваться мышью — людей с моторными нарушениями, опытных пользователей, предпочитающих клавиатуру, и всех с временными травмами. Клавиатурная доступность также является юридическим требованием в рамках WCAG 2.1.1 (Клавиатура) и 2.4.7 (Видимый фокус).
Тестирование полного потока через клавиатуру
// tests/accessibility/keyboard-navigation.spec.ts
import { test, expect } from '@playwright/test';
test('entire checkout flow is completable via keyboard only', async ({ page }) => {
await page.goto('/cart');
// Tab to "Proceed to Checkout" button
let attempts = 0;
while (attempts < 20) {
await page.keyboard.press('Tab');
const focusedText = await page.evaluate(() =>
document.activeElement?.textContent?.trim()
);
if (focusedText?.includes('Proceed to Checkout')) break;
attempts++;
}
await page.keyboard.press('Enter');
// Verify we reached the checkout page
await expect(page).toHaveURL(/\/checkout/);
// Fill shipping form via keyboard
await page.keyboard.press('Tab'); // Focus first field
await page.keyboard.type('John Doe');
await page.keyboard.press('Tab');
await page.keyboard.type('123 Main St');
await page.keyboard.press('Tab');
await page.keyboard.type('Springfield');
await page.keyboard.press('Tab');
// Select state from dropdown via keyboard
await page.keyboard.press('Space'); // Open dropdown
await page.keyboard.type('Il'); // Type to search
await page.keyboard.press('Enter'); // Select Illinois
await page.keyboard.press('Tab');
await page.keyboard.type('62701'); // ZIP code
// Tab to submit and press Enter
attempts = 0;
while (attempts < 10) {
await page.keyboard.press('Tab');
const focusedRole = await page.evaluate(() =>
document.activeElement?.getAttribute('type')
);
if (focusedRole === 'submit') break;
attempts++;
}
await page.keyboard.press('Enter');
// Verify order confirmation
await expect(page.locator('[data-testid="order-confirmation"]')).toBeVisible();
});
Тестирование ловушки фокуса
Модальные диалоги должны захватывать фокус — Tab должен циклически перемещаться по элементам внутри модального окна, не выходя на страницу за ним. Это предотвращает случайное взаимодействие пользователей с контентом, который они не могут видеть.
test('focus trap works in modal dialogs', async ({ page }) => {
await page.goto('/');
// Open a modal
await page.click('[data-testid="open-modal"]');
await expect(page.locator('[role="dialog"]')).toBeVisible();
// Collect all focusable elements in the modal
const modalFocusable = await page.evaluate(() => {
const modal = document.querySelector('[role="dialog"]');
if (!modal) return [];
const focusable = modal.querySelectorAll(
'a[href], button, input, select, textarea, [tabindex]:not([tabindex="-1"])'
);
return Array.from(focusable).map(el => ({
tag: el.tagName,
text: el.textContent?.trim().substring(0, 20),
}));
});
expect(modalFocusable.length).toBeGreaterThan(0);
// Tab through all elements -- focus should cycle within the modal
for (let i = 0; i < modalFocusable.length + 2; i++) {
await page.keyboard.press('Tab');
const isInModal = await page.evaluate(() => {
const modal = document.querySelector('[role="dialog"]');
return modal?.contains(document.activeElement);
});
expect(isInModal).toBe(true);
}
// Escape should close the modal
await page.keyboard.press('Escape');
await expect(page.locator('[role="dialog"]')).not.toBeVisible();
// Focus should return to the trigger element
const focusedAfterClose = await page.evaluate(() =>
document.activeElement?.getAttribute('data-testid')
);
expect(focusedAfterClose).toBe('open-modal');
});
Проверка порядка Tab
test('tab order follows logical visual flow', async ({ page }) => {
await page.goto('/checkout');
const tabOrder: string[] = [];
// Press Tab through the page and record the order
for (let i = 0; i < 30; i++) {
await page.keyboard.press('Tab');
const label = await page.evaluate(() => {
const el = document.activeElement;
return el?.getAttribute('aria-label')
|| el?.getAttribute('name')
|| el?.textContent?.trim().substring(0, 30)
|| el?.tagName
|| 'unknown';
});
tabOrder.push(label);
}
// Verify the expected tab order for checkout
const expectedOrder = [
'Full Name',
'Email',
'Address Line 1',
'Address Line 2',
'City',
'State',
'ZIP Code',
'Continue to Payment',
];
// Each expected element should appear in the tab order in sequence
let lastIndex = -1;
for (const expected of expectedOrder) {
const index = tabOrder.findIndex(
(label, i) => i > lastIndex && label.includes(expected)
);
expect(index).toBeGreaterThan(lastIndex);
lastIndex = index;
}
});
Skip-ссылки
Skip-ссылки позволяют пользователям клавиатуры пропустить повторяющуюся навигацию и перейти непосредственно к основному контенту:
test('skip link is available and functional', async ({ page }) => {
await page.goto('/');
// Press Tab once -- skip link should be the first focusable element
await page.keyboard.press('Tab');
const skipLink = page.locator(':focus');
const text = await skipLink.textContent();
expect(text?.toLowerCase()).toContain('skip to');
// Skip link should be visible when focused
await expect(skipLink).toBeVisible();
// Activate the skip link
await page.keyboard.press('Enter');
// Focus should move to the main content area
const focusedId = await page.evaluate(() => document.activeElement?.id);
expect(focusedId).toBe('main-content');
});
Паттерны клавиатурного взаимодействия
Различные типы компонентов имеют ожидаемые клавиатурные поведения, определённые WAI-ARIA:
| Компонент | Tab | Enter/Space | Стрелки | Escape |
|---|---|---|---|---|
| Кнопка | Фокус | Активация | Н/Д | Н/Д |
| Ссылка | Фокус | Переход | Н/Д | Н/Д |
| Чекбокс | Фокус | Переключение | Н/Д | Н/Д |
| Группа радио | Фокус группы | Выбор | Перемещение выбора | Н/Д |
| Выпадающий список | Фокус | Открыть | Навигация по вариантам | Закрыть |
| Панель вкладок | Фокус активной вкладки | Н/Д | Переключение вкладок | Н/Д |
| Модальный диалог | Фокус первого элемента | Н/Д | Н/Д | Закрыть модальное окно |
| Меню | Фокус первого пункта | Активация | Навигация по пунктам | Закрыть меню |
| Аккордеон | Фокус заголовка | Переключение секции | Н/Д | Н/Д |
test('dropdown keyboard interactions follow WAI-ARIA patterns', async ({ page }) => {
await page.goto('/form');
// Tab to the dropdown
const dropdown = page.locator('[role="listbox"]');
await dropdown.focus();
// Space opens the dropdown
await page.keyboard.press('Space');
await expect(page.locator('[role="option"]').first()).toBeVisible();
// Arrow down navigates options
await page.keyboard.press('ArrowDown');
const focused = await page.evaluate(() =>
document.activeElement?.getAttribute('aria-selected')
);
expect(focused).toBe('true');
// Enter selects the option
await page.keyboard.press('Enter');
await expect(page.locator('[role="option"]').first()).not.toBeVisible();
// Escape closes without selecting
await page.keyboard.press('Space'); // Reopen
await page.keyboard.press('Escape');
await expect(page.locator('[role="option"]').first()).not.toBeVisible();
});
Тестирование клавиатурной навигации выявляет реальные проблемы пользователей. Если ваши наиболее критичные потоки нельзя пройти с клавиатуры, вы и исключаете пользователей, и приглашаете судебные иски. Тестируйте каждый критичный пользовательский сценарий с взаимодействием только через клавиатуру.