Дайджесты новостей
Инженерная сцена аудита доступности интерфейса с клавиатурной навигацией модального окна и тестовыми отчетами WCAG 2.2 на экране ноутбука.

Автоматизация проверок доступности интерфейсов по сценариям WCAG 2.2 и Playwright

Если ты хоть раз настраивал аудит доступности (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 декомпозированы по пяти прикладным направлениям:

  1. Adaptability: адаптивность при зуме текста до 200% без потери функционала и рефлоу без горизонтальной прокрутки при ширине 320px (WCAG 1.4.10).
  2. Keyboard: доступ ко всем элементам без мыши (WCAG 2.1.1), отсутствие ловушек фокуса (WCAG 2.1.2) и видимый контрастный индикатор фокуса (WCAG 2.4.7).
  3. Assistive Technology: строгая семантика ролей и состояний (aria-expanded, aria-selected, aria-controls), уведомление об изменениях через живые регионы.
  4. Colour and Contrast: минимальный контраст текста 4.5:1 для обычного и 3:1 для крупного кегля.
  5. 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. Это экономит десятки часов ручной проверки и защищает интерфейс от скрытых регрессий.