Modern QA2026Тестирование клавиатурной навигации
Join

Course10 Visual & Accessibility Testing

Cutting-edge · Chapter 10

Тестирование клавиатурной навигации

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

Тестирование клавиатурной навигации выявляет реальные проблемы пользователей. Если ваши наиболее критичные потоки нельзя пройти с клавиатуры, вы и исключаете пользователей, и приглашаете судебные иски. Тестируйте каждый критичный пользовательский сценарий с взаимодействием только через клавиатуру.