open atlas
↑ К треку
React с нуля до senior RCT · 10 · 04

Тестирование доступности: слои, ловящие то, что не видит axe

getByRole делает каждый тест дымовым a11y-тестом; axe ловит около трети проблем WCAG — порядок фокуса, качество имён, тайминг объявлений требуют ручного прохода скринридером. CI: jsx-a11y, ролевые запросы, axe на ключевых страницах; каждый a11y-баг сначала получает падающий тест.

RCT Senior ◷ 17 min
Уровень
ОсновыJuniorMiddleSenior

Слайд квартального ревью гласил: «Доступность: 100% компонентов покрыто автоматическими проверками, ноль нарушений». Шесть недель спустя аудит соответствия у государственного клиента — тот, который закупка требует до подписания, — провалил продукт на первой же сессии, и сделка съехала на квартал. Каждая находка прошла зелёный пайплайн насквозь. Закрытие диалога экспорта роняло фокус на футер страницы: axe прошёл, потому что axe инспектирует отрендеренное дерево, а не путешествие фокуса. Двенадцать ссылок портфолио назывались одинаково — «Подробнее»: axe прошёл, потому что имя у каждой было — правило проверяет наличие, а не смысл. Тост об успехе появлялся и исчезал без live region, так что пользователи скринридеров не слышали подтверждение перевода: axe прошёл, в статическом дереве не было ничего невалидного. А кастомный date picker запирал клавиатурный фокус, как мотель для тараканов: axe прошёл, потому что axe не нажимает клавиш. Ничто из этого не значит, что инструменты плохи; это значит, что команда тихо подменила вопрос «может ли пользователь скринридера завершить поток?» другим, автоматизируемым вопросом — и отчиталась вторым ответом как первым. Deque, авторы axe-core, маркетируют его как ловящий около 57% проблем «по объёму»; независимые оценки — например, аудит инструментов британского правительства — ставят автоматическое обнаружение ближе к трети нарушений WCAG (Web Content Accessibility Guidelines, международный стандарт доступности веб-контента). Планируйте по нижнему числу. Команда автоматизировала треть и объявила целое.

getByRole — самый дешёвый гейт, который у вас уже есть

Прежде чем добавлять инструменты, заметьте тот, что уже в сборке: приоритет запросов React Testing Library (RTL). getByRole("button", { name: "Save" }) — не просто селектор, а утверждение, что дерево доступности содержит узел с ролью кнопки и доступным именем «Save». Конвертируйте кликабельный div из первого урока — и тест не сможет его найти: нет роли, нет совпадения, красный тест. Сорвите подпись с кнопки-иконки — и опция name перестаёт совпадать. Оберните список опций в div, ломающий семантику списка, — и getAllByRole("listitem") вернётся короче. Каждый тест компонента, написанный так, — дымовой тест доступности, который не стоит ничего дополнительно и бежит на каждом PR; именно поэтому документация RTL ранжирует запросы по тому, насколько они отражают восприятие AT: сначала роль, затем подпись, затем текст, и data-testid как явная последняя инстанция. Антипаттерн — тянуться к testid по умолчанию: он пришпиливает тесты к невидимым атрибутам, снимая давление, из-за которого семантические регрессии падают громко. Полезное командное правило: testid требует комментария, объясняющего, почему ни один ролевой запрос не подошёл.

Тот же приоритет лепит API компонентов. Тест, которому нужен getByRole("dialog", { name: "Подтвердите удаление" }), заставляет компонент диалога принять и провести доступный заголовок; тест с toHaveAccessibleName() на кнопке-иконке протаскивает проп aria-label насквозь. Тесты, написанные в словаре дерева доступности, толкают компоненты к тому, чтобы это дерево экспонировать, — дешёвый добродетельный цикл.

Викторина

Коллега рефакторит примитив Button с нативной кнопки на стилизованный div по дизайнерским соображениям. Какой существующий слой тестов упадёт первым, если команда запрашивает по ролям?

Что axe автоматизирует — честно

jest-axe подключает axe-core к тестам компонентов: отрендерить, выполнить axe(container), утвердить отсутствие нарушений. Что axe-core проверяет на самом деле — статически вычислимые факты об отрендеренном дереве: изображения без alt, поля без подписей, ARIA-атрибуты, невалидные для роли, отсутствующая структура ориентиров, дубли id, ломающие aria-labelledby, цветовой контраст. На этой территории он превосходен — быстрый, детерминированный, с низким уровнем ложных срабатываний как целью дизайна. Граница — всё, что требует суждения или взаимодействия: имеет ли порядок фокуса смысл; хорошее ли имя «Подробнее» для двенадцатой одинаковой ссылки (наличие имени проверяемо, качество — нет); даёт ли тайминг тоста скринридеру дочитать фразу; закрывает ли Escape пикер; описывает ли alt на самом деле график. Поставьте числа на границу и держите их консервативными: собственное заявление Deque «по объёму» — около 57%; аудит автоматических инструментов британского правительства и большинство независимых исследований приземляются возле трети обнаруживаемых нарушений WCAG. В любом случае зелёный axe ограничивает меньше половины проблемы — считайте его полом и никогда сертификатом.

Одно jsdom-специфичное честное замечание про jest-axe: проверке контраста нужны настоящие layout и отрисовка, которых jsdom не делает, — правило там ненадёжно и обычно отключено. Серьёзное развёртывание — axe в настоящем браузере, @axe-core/playwright против работающего приложения, где полный набор правил применяется к настоящим страницам с настоящим CSS. Покомпонентный jest-axe всё равно отрабатывает свой хлеб как быстрая регрессионная сеть для валидности ARIA и подписей, но засчитывается именно браузерный прогон.

Викторина

У вашего компонента диалога зелёный jest-axe тест. Пользователь сообщает: после закрытия диалога фокус оказывается на футере, и NVDA читает строку копирайта. Почему тест это не поймал и какой слой должен был?

Ручной проход, CI-гейты и регрессионное правило

Слой, до которого автоматизация не дотягивается, закрывается ритуалом, а не героизмом: скриптованный проход скринридером по критическим потокам — checkout, регистрация, форма движения денег — перед каждым релизом. Скриптованный значит письменный чек-лист на поток: достижим ли каждый контрол через Tab и стрелки; объявляет ли каждый осмысленные роль, имя и состояние; объявляется ли смена роута; восстанавливает ли диалог фокус; читается ли сообщение об ошибке. Гоняйте его на VoiceOver плюс Safari на macOS и NVDA плюс Chrome или Firefox на Windows — связках, на которых сидят настоящие пользователи, — и проход занимает двадцать-тридцать минут на поток, когда скрипт уже существует. Регулярность бьёт охват: три критических потока каждый релиз превосходят героический аудит всего приложения раз в год, потому что регрессии кучкуются там, где код шевелится.

В CI слои сортируются по латентности и охвату. eslint-plugin-jsx-a11y работает в редакторе и в lint-CI: статический анализ JSX — интерактивные элементы без клавиатурных обработчиков, отсутствующие alt, ARIA-атрибуты, невалидные для роли. Он быстрый и мелкий; видит только литеральный JSX, не вычисленные значения и не runtime-композицию. Ролевые запросы RTL бегут с юнит-тестами на каждом PR. Браузерный axe идёт по ключевым страницам на PR или ночью, гейтуя по принципу ноль новых нарушений — храповик, а не мандат на уборку: существующий долг инвентаризован, новый блокируется. Ручной проход гейтует сам релиз.

Регрессионное правило связывает систему: баг доступности сначала получает падающий тест, ровно как любой другой дефект. Баг восстановления фокуса — interaction-тест, утверждающий document.activeElement после закрытия. Лгущий aria-expanded — RTL-утверждение атрибута против состояния открытости. Необъявленный тост — тест, утверждающий смену текста live region. Считать a11y-находки нетестируемым фольклором — способ отгрузить один баг трижды; считать их дефектами с репродукциями — способ удешевлять ручной проход каждый квартал, потому что всё, что он когда-либо ловил, теперь пришпилено машиной.

Почему это работает

Почему автоматическое покрытие упирается в треть, а не растёт к полному по мере взросления инструментов? Потому что потолок семантический, а не технический. Большая доля критериев WCAG — суждения: alt должен быть эквивалентен, подписи должны описывать, порядок фокуса должен сохранять смысл, — а суждение требует знания намерения, которого DOM не несёт. Другая доля — свойства взаимодействия, размазанные во времени: ловушки, восстановление, тайминг объявлений. Инструменты продолжают улучшаться внутри вычислимой территории (математика контраста, валидность ARIA, наличие имён), а LLM-помощники подбираются к качеству имён, но вопрос соответствия — может ли человек с AT завершить задачу — это утверждение о юзабилити. Честная позиция: автоматизируйте пол безжалостно, а человеческую верификацию потолка закладывайте в бюджет как регулярную статью — как дежурства.

Вспомните перед уходом
  1. 01
    Назовите, что axe-core может и не может проверить, с честными числами покрытия и jsdom-оговоркой для jest-axe.
  2. 02
    Разложите четырёхслойную стратегию тестирования с латентностью и слепым пятном каждого слоя, плюс регрессионное правило.
Итог

Тестирование доступности проваливается, когда автоматизируемый вопрос тихо подменяет настоящий. Настоящий вопрос — может ли человек с assistive technology завершить поток — раскладывается на слои, каждый из которых ловит то, что предыдущий структурно не может. Ролевые запросы React Testing Library — бесплатный первый слой: getByRole утверждает, что дерево доступности экспонирует нужные роль и имя, так что кнопка-обёрнутая-в-div или иконка без подписи роняет обычные тесты компонентов на каждом PR, а testid понижен до оправдываемых исключений. axe-core автоматизирует второй слой — статически вычислимые факты: отсутствующие подписи, невалидная ARIA, битые ссылки id, контраст — и его честное покрытие около трети нарушений WCAG (собственное заявление вендора по объёму достигает 57%); jest-axe даёт быстрое подмножество на компонентах, но jsdom не умеет контраст, поэтому решающий скан бежит в настоящем браузере через @axe-core/playwright с гейтом «ноль новых нарушений» на PR. Непокрываемые две трети — путешествия фокуса, тайминг объявлений, осмысленность имён и alt — принадлежат скриптованному ритуалу скринридера по критическим потокам перед каждым релизом, на VoiceOver и NVDA в связках, которыми пользуются настоящие люди, по двадцать-тридцать минут на поток. Регрессионное правило замыкает петлю: каждый баг доступности становится падающим автоматическим тестом до фикса — утверждения activeElement для фокуса, атрибутов для лгущего состояния, содержимого live region для молчащих объявлений, — и каждая ручная находка навсегда сжимает ручную поверхность. Автоматизируйте пол; закладывайте людей на потолок; никогда не отчитывайтесь полом как потолком. Теперь, когда увидишь зелёный axe в CI, ты знаешь ровно, что он гарантирует — и что две трети проблемы он оставил скриптованному проходу скринридером и регрессионным тестам, которые пишутся после каждой ручной находки.

Практика

Начни сверху. Задачи идут от простого к сложному: вспомнить факт, применить к случаю, затем senior-уровень. Открой, попробуй, потом открой ответ.

вспомнитьприменитьуглубить0 из 6 завершено

Что-то непонятно?

Задай вопрос по этому уроку. Вопросы анонимны и попадают напрямую автору — урок станет лучше.

Примени это

Примени этот урок в реальном проекте.

хоткеи развернуть
поиск
K
пред. пьеса
k
след. пьеса
j
тиры
t
это меню
?
sources4
expand
  1. 01
  2. 02
  3. 03
  4. 04

Trademarks belong to their respective owners. Editorial reference only.