Если ты хоть раз настраивал аудит доступности (a11y) в репозитории фронтенда, то наверняка начинал со стандартного набора: линтер eslint-plugin-jsx-a11y, плагин axe-core и отчет Lighthouse в CI/CD. Казалось бы, зеленый щит получен, релиз готов. Но стоит незрячему пользователю со скринридером или человеку, управляющему интерфейсом только с клавиатуры, открыть страницу — и начинаются проблемы.
Фокус проваливается в невидимые слои, закрытие модального окна выбрасывает курсор в начало документа, а вкладки переключаются табуляцией вместо стрелок. Почему автоматические инструменты пропускают такие детские болезни?
Давай разберем эту механику и посмотрим, как британская команда TetraLogical подошла к автоматизации более 95% требований стандарта WCAG 2.2 через связку BDD-сценариев и headless-браузера Playwright.
Слепая зона статических линтеров: почему 60% багов не ловятся в CI
Проблема традиционных сканеров DOM и линтеров кроется в их архитектурных ограничениях. Инструменты вроде axe-core или Pa11y анализируют статичный снимок разметки в определенный момент времени. Они отлично находят базовые дефекты:
- Отсутствие атрибута
altу тега<img>; - Недостаточный коэффициент контрастности текста относительно фона;
- Нарушение иерархии заголовков от
h1доh6; - Потерю текстовой метки у кнопки или поля ввода.
Однако такие статические ошибки составляют лишь треть от общего числа барьеров доступности. Остальные проблемы возникают исключительно в динамике — при интерактивном взаимодействии с интерфейсом.
Статический анализатор не может проверить, заперт ли фокус внутри открытого всплывающего окна (keyboard focus trap), пока окно не открыто кликом. Он не знает, куда вернется фокус после нажатия клавиши Escape — на кнопку открытия или в корень страницы. Он не проверит, объявляет ли скринридер динамическое обновление корзины через aria-live.
До недавнего времени единственным выходом оставалось медленное ручное тестирование с программами экранного доступа (NVDA, JAWS, VoiceOver). Но встроить живого тестировщика в каждый пуллреквест дизайн-системы нереально. Этот разрыв и закрывает открытая библиотека accessible-components-library от TetraLogical.
Архитектура библиотеки TetraLogical: пять направлений WCAG 2.2 и 8 базовых компонентов
Инженеры TetraLogical выделили 8 фундаментальных компонентов интерфейса, из которых строится любая дизайн-система:
- Accordion: раскрывающиеся панели с заголовками и ролями кнопок;
- Breadcrumbs: навигационная цепочка с семантикой
navиaria-current="page"; - Dialog / Modal: изолированные модальные окна с удержанием фокуса;
- Disclosure: одиночная кнопка скрытия и отображения блока;
- Pagination: доступная навигация по страницам списков;
- Tabs: переключение контента по официальному шаблону ARIA tabs;
- Text Input: формы со связыванием меток и сообщений об ошибках;
- Textarea: поля ввода с динамическим счетчиком символов.
Все критерии стандарта WCAG 2.2 декомпозированы по пяти прикладным направлениям:
- Adaptability: адаптивность при зуме текста до 200% без потери функционала и рефлоу без горизонтальной прокрутки при ширине 320px (WCAG 1.4.10).
- Keyboard: доступ ко всем элементам без мыши (WCAG 2.1.1), отсутствие ловушек фокуса (WCAG 2.1.2) и видимый контрастный индикатор фокуса (WCAG 2.4.7).
- Assistive Technology: строгая семантика ролей и состояний (
aria-expanded,aria-selected,aria-controls), уведомление об изменениях через живые регионы. - Colour and Contrast: минимальный контраст текста 4.5:1 для обычного и 3:1 для крупного кегля.
- Input Assistance: подсветка ошибок через
aria-invalid="true"и их связывание с подсказками черезaria-describedby.
От BDD-сценариев Gherkin к шагам Playwright: как устроен тест-раннер
Вместо написания разрозненных тестов разработчики TetraLogical оформили требования на языке Gherkin. Это позволяет дизайнерам, менеджерам доступности и инженерам работать в единой понятной терминологии.
Сценарии хранятся в .feature-файлах. Тег @automatic помечает проверки, которые полностью берет на себя тестовый раннер, а тег @manual отсекает критерии, требующие субъективной человеческой оценки.
Вот фрагмент спецификации для проверки изоляции фокуса модального окна из GitHub-репозитория TetraLogical:
@dialog @keyboard @2.1.2 @a @automatic
Feature: Dialog — Keyboard Trap and Focus Management
Background:
Given I load the sample "components/dialog/examples/modal.html"
Scenario: Tab key cycles focus within the active dialog
When I click the button "Open Dialog"
Then the element "#modal-dialog" should be visible
And the focus should be on the element "#dialog-first-input"
When I press "Tab" until reaching the last focusable element
And I press "Tab"
Then the focus should be on the element "#dialog-close-button"
@dialog @keyboard @escape @automatic
Scenario: Escape key dismisses the dialog and returns focus to trigger
When I click the button "Open Dialog"
And I press "Escape"
Then the element "#modal-dialog" should not be visible
And the focus should be on the element "#open-dialog-trigger"
Слой определений (step definitions) транслирует формулировки Gherkin напрямую в асинхронные команды Playwright API:
import { Given, When, Then } from '@cucumber/cucumber';
import { expect } from '@playwright/test';
import { AccessibilityWorld } from '../support/world';
Given('I load the sample {string}', async function (this: AccessibilityWorld, url: string) {
await this.page.goto(url);
await this.page.waitForLoadState('networkidle');
});
When('I press {string}', async function (this: AccessibilityWorld, key: string) {
await this.page.keyboard.press(key);
});
Then('the focus should be on the element {string}', async function (this: AccessibilityWorld, selector: string) {
const targetElement = this.page.locator(selector);
await expect(targetElement).toBeFocused();
});
Если ты уже изучал, как устроена библиотека компонентов TetraLogical, то знаешь, что браузер берет на себя часть базового управления фокусом. Но сценарии TetraLogical гарантируют, что при любой композиции кастомных кнопок и вложенных полей фокус ни при каких обстоятельствах не потеряется в дереве документа.
Внедрение в CI/CD конвейер: автоматический запуск тестов
Для интеграции тестового набора в проект потребуется среда Node.js и установка раннеров:
npm install --save-dev @playwright/test @cucumber/cucumber ts-node typescript
npx playwright install --with-deps chromium
В конфигурационном файле cucumber.js задается запуск только автоматических тестов:
module.exports = {
default: {
paths: ['features/**/*.feature'],
require: ['features/step_definitions/**/*.ts', 'features/support/**/*.ts'],
requireModule: ['ts-node/register'],
format: ['summary', 'html:reports/accessibility-report.html'],
tags: '@automatic and not @manual',
parallel: 2
}
};
Вспомогательный класс AccessibilityWorld создает браузерный контекст Playwright:
import { setWorldConstructor, World, IWorldOptions, Before, After } from '@cucumber/cucumber';
import { Browser, BrowserContext, Page, chromium } from 'playwright';
export class AccessibilityWorld extends World {
browser!: Browser;
context!: BrowserContext;
page!: Page;
constructor(options: IWorldOptions) {
super(options);
}
}
setWorldConstructor(AccessibilityWorld);
Before(async function (this: AccessibilityWorld) {
this.browser = await chromium.launch({ headless: true });
this.context = await this.browser.newContext({ viewport: { width: 1280, height: 720 } });
this.page = await this.context.newPage();
});
After(async function (this: AccessibilityWorld) {
await this.page.close();
await this.context.close();
await this.browser.close();
});
Запуск в пайплайне CI выполняется командой npx cucumber-js. Если разработчик забудет вернуть фокус после закрытия модалки или нарушит цикличность клавиши Tab, сборка в пуллреквесте моментально остановится с указанием нарушенного критерия WCAG. Это экономит десятки часов ручной проверки и защищает интерфейс от скрытых регрессий.
