Доступность интерфейса легко ломается незаметно: правка убирает подпись у поля, кнопка-иконка теряет доступное имя, контраст падает ниже нормы после смены палитры. Функциональный тест этого не поймает - он кликает по элементам, которые сам же нашёл, и не задаёт вопрос, доберётся ли до них человек со скринридером. E2E-набор, который уже водит браузер по реальным страницам, - естественное место, чтобы ловить регрессии доступности рано и автоматически.
Основной инструмент - axe, движок проверки доступности, интегрированный с Playwright пакетом @axe-core/playwright. В тесте создают AxeBuilder с текущей страницей и вызывают analyze(); результат содержит список нарушений - violations. Проверка сводится к утверждению, что нарушений нет: expect(results.violations).toEqual([]). Axe прогоняет страницу по набору правил и находит типовые проблемы - отсутствие подписей, недостаточный контраст, неверные роли.
Проверку можно сузить и настроить под контекст. include и exclude ограничивают анализ конкретной областью страницы - полезно, когда сторонний виджет нельзя починить и его исключают из проверки осознанно. withTags выбирает набор правил - например, уровни WCAG 2.0 A и AA, - чтобы проверять именно тот стандарт, который вы обещаете. Так axe становится не шумным сканером всего подряд, а точной проверкой заявленного уровня доступности.
Полезно один раз увидеть базовую проверку целиком, чтобы встроить её в набор. Ниже - тест, который открывает страницу, прогоняет axe с выбранными тегами WCAG и утверждает отсутствие нарушений. К этой форме возвращаются, добавляя проверку доступности к ключевым страницам - входу, оформлению заказа, формам, - там, где недоступный интерфейс напрямую отсекает часть пользователей.
Рядом стоит другой механизм - снимок доступного дерева через toMatchAriaSnapshot. В отличие от скриншота, он фиксирует не пиксели, а структуру ролей и доступных имён: заголовок, список, кнопки с их именами. Это устойчивый контракт того, как страницу видят скринридер и клавиатура, - он не ломается от смены цвета или отступа, но краснеет, если пропала роль или доступное имя. Семантический аналог визуального теста.
У aria-снимка своя ниша. Визуальный тест ловит смещение и цвет, но молчит про семантику; aria-снимок ловит семантику, но молчит про пиксели. Для доступности важен именно второй: пользователю скринридера всё равно, съехала ли кнопка на пару пикселей, но критично, есть ли у неё имя и роль. Поэтому aria-снимок точнее выражает контракт доступности, чем картинка.
Важная оговорка - автопроверка не заменяет ручную. Axe находит машинно-обнаружимые нарушения, но не судит о смысле: понятна ли подпись, логичен ли порядок фокуса, осмысленно ли звучит страница вслух. Полноценная доступность требует проверки реальным скринридером и клавиатурой. Ценность автоматики в другом: она ловит регрессии рано и дёшево, не давая уже достигнутому уровню молча деградировать между релизами.
Типичные провалы доступности в E2E предсказуемы. Полное отсутствие таких проверок - и регрессии всплывают у реальных пользователей, а не в CI. Axe без include/exclude на странице с неисправимым сторонним виджетом - вечно красный тест, который перестают читать. И вера, что зелёный axe означает доступность, - тогда как машина ловит лишь часть. Проверяйте ключевые страницы axe с нужными тегами, фиксируйте семантику aria-снимком и дополняйте это ручной проверкой.
npm i -D @axe-core/playwright
import AxeBuilder from '@axe-core/playwright'
test('страница входа доступна', async ({ page }) => {
await page.goto('/login')
const results = await new AxeBuilder({ page })
.withTags(['wcag2a', 'wcag2aa'])
.analyze()
expect(results.violations).toEqual([])
})
// Семантический контракт: роли и доступные имена, а не пиксели
await expect(page.getByRole('navigation')).toMatchAriaSnapshot(`
- navigation:
- link "Главная"
- link "Заказы"
`)