Статьи

Автоматическая проверка доступности с топором

Дрон a11y зависает через плечо разработчика, проверяя доступность веб-сайта на экране компьютера

Сколько времени и усилий вы потратили на планирование дизайна вашего последнего веб-сайта, чтобы он был доступен для людей с особыми потребностями и ограниченными возможностями? У меня есть предчувствие, что ответом для многих читателей будет «Нет». Но вряд ли кто-то будет отрицать, что существует значительное количество пользователей Интернета, которые испытывают проблемы с доступом к сайтам из-за проблем с распознаванием цветов, чтением текста, использованием мыши или просто навигацией по сложной структуре сайта.

Проблемы доступности часто игнорируются из-за усилий, необходимых для их проверки и реализации решений. Разработчики должны не только знакомиться с основными стандартами, но и постоянно проверять их соответствие. Можем ли мы упростить разработку доступных веб-сайтов, автоматически выполняя стандартные проверки?

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

Представляем топор

Axe — это автоматизированная библиотека тестирования доступности, которая отправилась в путь, чтобы внедрить тестирование доступности в основную веб-разработку. Библиотека ax-core имеет открытый исходный код и разработана таким образом, чтобы ее можно было использовать с различными средами тестирования, инструментами и средами. Например, его можно запустить в функциональных тестах, плагинах браузера или прямо в версии вашего приложения для разработки. В настоящее время поддерживается около 55 правил для проверки веб-сайта на предмет различных аспектов доступности.

Чтобы быстро продемонстрировать, как работает библиотека, давайте создадим простой компонент и протестируем его. Мы не будем создавать целую страницу, а просто заголовок.

При создании заголовка мы приняли несколько блестящих дизайнерских решений:

  1. Мы сделали фон светло-серым, а ссылки — темно-серым, потому что этот цвет стильный и стильный;
  2. Мы использовали крутой значок лупы для кнопки поиска;
  3. Мы установили индекс вкладки входных данных поиска на 1, чтобы при открытии пользователем страницы он мог нажать вкладку и сразу же ввести поисковый запрос.

Аккуратно, верно? Что ж, посмотрим, как это выглядит с точки зрения доступности. Мы можем добавить топор из CDN и записать все ошибки в консоль браузера с помощью следующего скрипта.

axe.run(function (err, results) { if (results.violations.length) { console.warn(results.violations); } }); 

Если вы запустите пример и откроете консоль, вы увидите массив с шестью объектами нарушения, в котором перечислены проблемы, которые у нас есть. Каждый объект описывает нарушенное нами правило, ссылки на виновные HTML-элементы, а также справочную информацию о том, как решить проблему.

Вот пример одного из объектов нарушения, показанный как JSON:

 [ { "id":"button-name", "impact":"critical", "tags":[ "wcag2a", "wcag412", "section508", "section508.22.a" ], "description":"Ensures buttons have discernible text", "help":"Buttons must have discernible text", "helpUrl":"https://dequeuniversity.com/rules/axe/2.1/button-name?application=axeAPI", "nodes":[ { "any":[ { "id":"non-empty-if-present", "data":null, "relatedNodes":[ ], "impact":"critical", "message":"Element has a value attribute and the value attribute is empty" }, { "id":"non-empty-value", "data":null, "relatedNodes":[ ], "impact":"critical", "message":"Element has no value attribute or the value attribute is empty" }, { "id":"button-has-visible-text", "data":"", "relatedNodes":[ ], "impact":"critical", "message":"Element does not have inner text that is visible to screen readers" }, { "id":"aria-label", "data":null, "relatedNodes":[ ], "impact":"critical", "message":"aria-label attribute does not exist or is empty" }, { "id":"aria-labelledby", "data":null, "relatedNodes":[ ], "impact":"critical", "message":"aria-labelledby attribute does not exist, references elements that do not exist or references elements that are empty or not visible" }, { "id":"role-presentation", "data":null, "relatedNodes":[ ], "impact":"moderate", "message":"Element's default semantics were not overridden with role=\"presentation\"" }, { "id":"role-none", "data":null, "relatedNodes":[ ], "impact":"moderate", "message":"Element's default semantics were not overridden with role=\"none\"" } ], "all":[ ], "none":[ { "id":"focusable-no-name", "data":null, "relatedNodes":[ ], "impact":"serious", "message":"Element is in tab order and does not have accessible text" } ], "impact":"critical", "html":"<button>\n <i class=\"fa fa-search\"></i>\n </button>", "target":[ "body > header > div > button" ], "failureSummary":"Fix all of the following:\n Element is in tab order and does not have accessible text\n\nFix any of the following:\n Element has a value attribute and the value attribute is empty\n Element has no value attribute or the value attribute is empty\n Element does not have inner text that is visible to screen readers\n aria-label attribute does not exist or is empty\n aria-labelledby attribute does not exist, references elements that do not exist or references elements that are empty or not visible\n Element's default semantics were not overridden with role=\"presentation\"\n Element's default semantics were not overridden with role=\"none\"" } ] }, ] с [ { "id":"button-name", "impact":"critical", "tags":[ "wcag2a", "wcag412", "section508", "section508.22.a" ], "description":"Ensures buttons have discernible text", "help":"Buttons must have discernible text", "helpUrl":"https://dequeuniversity.com/rules/axe/2.1/button-name?application=axeAPI", "nodes":[ { "any":[ { "id":"non-empty-if-present", "data":null, "relatedNodes":[ ], "impact":"critical", "message":"Element has a value attribute and the value attribute is empty" }, { "id":"non-empty-value", "data":null, "relatedNodes":[ ], "impact":"critical", "message":"Element has no value attribute or the value attribute is empty" }, { "id":"button-has-visible-text", "data":"", "relatedNodes":[ ], "impact":"critical", "message":"Element does not have inner text that is visible to screen readers" }, { "id":"aria-label", "data":null, "relatedNodes":[ ], "impact":"critical", "message":"aria-label attribute does not exist or is empty" }, { "id":"aria-labelledby", "data":null, "relatedNodes":[ ], "impact":"critical", "message":"aria-labelledby attribute does not exist, references elements that do not exist or references elements that are empty or not visible" }, { "id":"role-presentation", "data":null, "relatedNodes":[ ], "impact":"moderate", "message":"Element's default semantics were not overridden with role=\"presentation\"" }, { "id":"role-none", "data":null, "relatedNodes":[ ], "impact":"moderate", "message":"Element's default semantics were not overridden with role=\"none\"" } ], "all":[ ], "none":[ { "id":"focusable-no-name", "data":null, "relatedNodes":[ ], "impact":"serious", "message":"Element is in tab order and does not have accessible text" } ], "impact":"critical", "html":"<button>\n <i class=\"fa fa-search\"></i>\n </button>", "target":[ "body > header > div > button" ], "failureSummary":"Fix all of the following:\n Element is in tab order and does not have accessible text\n\nFix any of the following:\n Element has a value attribute and the value attribute is empty\n Element has no value attribute or the value attribute is empty\n Element does not have inner text that is visible to screen readers\n aria-label attribute does not exist or is empty\n aria-labelledby attribute does not exist, references elements that do not exist or references elements that are empty or not visible\n Element's default semantics were not overridden with role=\"presentation\"\n Element's default semantics were not overridden with role=\"none\"" } ] }, ] , [ { "id":"button-name", "impact":"critical", "tags":[ "wcag2a", "wcag412", "section508", "section508.22.a" ], "description":"Ensures buttons have discernible text", "help":"Buttons must have discernible text", "helpUrl":"https://dequeuniversity.com/rules/axe/2.1/button-name?application=axeAPI", "nodes":[ { "any":[ { "id":"non-empty-if-present", "data":null, "relatedNodes":[ ], "impact":"critical", "message":"Element has a value attribute and the value attribute is empty" }, { "id":"non-empty-value", "data":null, "relatedNodes":[ ], "impact":"critical", "message":"Element has no value attribute or the value attribute is empty" }, { "id":"button-has-visible-text", "data":"", "relatedNodes":[ ], "impact":"critical", "message":"Element does not have inner text that is visible to screen readers" }, { "id":"aria-label", "data":null, "relatedNodes":[ ], "impact":"critical", "message":"aria-label attribute does not exist or is empty" }, { "id":"aria-labelledby", "data":null, "relatedNodes":[ ], "impact":"critical", "message":"aria-labelledby attribute does not exist, references elements that do not exist or references elements that are empty or not visible" }, { "id":"role-presentation", "data":null, "relatedNodes":[ ], "impact":"moderate", "message":"Element's default semantics were not overridden with role=\"presentation\"" }, { "id":"role-none", "data":null, "relatedNodes":[ ], "impact":"moderate", "message":"Element's default semantics were not overridden with role=\"none\"" } ], "all":[ ], "none":[ { "id":"focusable-no-name", "data":null, "relatedNodes":[ ], "impact":"serious", "message":"Element is in tab order and does not have accessible text" } ], "impact":"critical", "html":"<button>\n <i class=\"fa fa-search\"></i>\n </button>", "target":[ "body > header > div > button" ], "failureSummary":"Fix all of the following:\n Element is in tab order and does not have accessible text\n\nFix any of the following:\n Element has a value attribute and the value attribute is empty\n Element has no value attribute or the value attribute is empty\n Element does not have inner text that is visible to screen readers\n aria-label attribute does not exist or is empty\n aria-labelledby attribute does not exist, references elements that do not exist or references elements that are empty or not visible\n Element's default semantics were not overridden with role=\"presentation\"\n Element's default semantics were not overridden with role=\"none\"" } ] }, ] время [ { "id":"button-name", "impact":"critical", "tags":[ "wcag2a", "wcag412", "section508", "section508.22.a" ], "description":"Ensures buttons have discernible text", "help":"Buttons must have discernible text", "helpUrl":"https://dequeuniversity.com/rules/axe/2.1/button-name?application=axeAPI", "nodes":[ { "any":[ { "id":"non-empty-if-present", "data":null, "relatedNodes":[ ], "impact":"critical", "message":"Element has a value attribute and the value attribute is empty" }, { "id":"non-empty-value", "data":null, "relatedNodes":[ ], "impact":"critical", "message":"Element has no value attribute or the value attribute is empty" }, { "id":"button-has-visible-text", "data":"", "relatedNodes":[ ], "impact":"critical", "message":"Element does not have inner text that is visible to screen readers" }, { "id":"aria-label", "data":null, "relatedNodes":[ ], "impact":"critical", "message":"aria-label attribute does not exist or is empty" }, { "id":"aria-labelledby", "data":null, "relatedNodes":[ ], "impact":"critical", "message":"aria-labelledby attribute does not exist, references elements that do not exist or references elements that are empty or not visible" }, { "id":"role-presentation", "data":null, "relatedNodes":[ ], "impact":"moderate", "message":"Element's default semantics were not overridden with role=\"presentation\"" }, { "id":"role-none", "data":null, "relatedNodes":[ ], "impact":"moderate", "message":"Element's default semantics were not overridden with role=\"none\"" } ], "all":[ ], "none":[ { "id":"focusable-no-name", "data":null, "relatedNodes":[ ], "impact":"serious", "message":"Element is in tab order and does not have accessible text" } ], "impact":"critical", "html":"<button>\n <i class=\"fa fa-search\"></i>\n </button>", "target":[ "body > header > div > button" ], "failureSummary":"Fix all of the following:\n Element is in tab order and does not have accessible text\n\nFix any of the following:\n Element has a value attribute and the value attribute is empty\n Element has no value attribute or the value attribute is empty\n Element does not have inner text that is visible to screen readers\n aria-label attribute does not exist or is empty\n aria-labelledby attribute does not exist, references elements that do not exist or references elements that are empty or not visible\n Element's default semantics were not overridden with role=\"presentation\"\n Element's default semantics were not overridden with role=\"none\"" } ] }, ] 

Если вы просто выберете описания нарушений, вот что говорит:

 Ensures buttons have discernible text Ensures the contrast between foreground and background colors meets WCAG 2 AA contrast ratio thresholds Ensures every HTML document has a lang attribute Ensures <img> elements have alternate text or a role of none or presentation Ensures every form element has a label Ensures tabindex attribute values are not greater than 0 

Оказывается, наши дизайнерские решения были не такими уж блестящими:

  1. Два оттенка серого, которые мы выбрали, не имеют достаточного контраста и могут быть трудны для чтения людям с нарушениями зрения
  2. Значок лупы кнопки поиска не указывает на назначение кнопки для пользователя, использующего программу чтения с экрана.
  3. Индекс вкладок входных данных поиска нарушает обычный ход навигации для людей, использующих программы чтения с экрана или клавиатуры, и затрудняет им доступ к ссылкам меню.

Это также отметило несколько других вещей, о которых мы не думали. Всего выполняется около 55 различных проверок, включая правила из разных стандартных руководств и лучших практик.

Чтобы увидеть список ошибок, нам пришлось внедрить скрипт в саму страницу. Хотя это вполне выполнимо, это не очень удобно. Было бы лучше, если бы мы могли выполнять эти проверки для любой страницы, не вводя ничего самостоятельно. Желательно использовать известный бегун для испытаний. Мы можем сделать это, используя Selenium WebDriver и Mocha.

Ходовой топор с Selenium WebDriver

Для запуска axe с помощью Selenium мы будем использовать библиотеку ax-webdriverjs . Он предоставляет топор API, который можно использовать поверх WebDriver.

Чтобы настроить его, давайте создадим отдельный проект и инициализируем проект npm с помощью команды npm init . Не стесняйтесь оставлять значения по умолчанию для всего, что он запрашивает. Чтобы запустить Selenium, вам нужно установить selenium-webdriver . Мы проведем наши тесты в PhantomJS, поэтому нам нужно будет установить и их. Selenium требует Node версии 6.9 или новее, поэтому убедитесь, что он установлен.

Для установки пакетов выполните:

 npm install phantomjs-prebuilt selenium-webdriver --save-dev 

Теперь нам нужно установить axe-core и axe-webdriverjs :

 npm install axe-core axe-webdriverjs --save-dev 

Теперь, когда инфраструктура настроена, давайте создадим скрипт, который запускает тесты снова на sitepoint.com (ничего личного, ребята). Создайте файл axe.js в папке проекта и добавьте следующее содержимое:

 const axeBuilder = require('axe-webdriverjs'); const webDriver = require('selenium-webdriver'); // create a PhantomJS WebDriver instance const driver = new webDriver.Builder() .forBrowser('phantomjs') .build(); // run the tests and output the results in the console driver .get('http://sitepoint.com') .then(() => { axeBuilder(driver) .analyze((results) => { console.log(results); }); }); 

Чтобы выполнить этот тест, мы можем запустить node axe.js Мы не можем запустить его из консоли, так как мы установили PhantomJS локально в нашем проекте. Мы должны запустить его как скрипт npm. Для этого откройте файл package.json и измените запись тестового скрипта по умолчанию:

 "scripts": { "test": "node axe.js" }, 

Теперь попробуйте запустить npm test . Через несколько секунд вы увидите список нарушений, найденных топором. Если вы ничего не видите, это может означать, что SitePoint исправил их после прочтения статьи.

Это более удобно, чем наш первоначальный подход, поскольку нам не нужно изменять страницу, которую мы тестируем, и мы можем легко запустить их с помощью CLI. Недостатком этого, однако, является то, что нам все еще нужно выполнить отдельный скрипт для запуска теста. Было бы лучше, если бы мы могли запустить его вместе с остальными нашими тестами. Посмотрим, как этого добиться с помощью мокко.

Бегущий топор с помощью мокко

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

Нам, очевидно, понадобится сам Mocha и библиотека утверждений. Как насчет Чай? Установите все это с помощью этой команды:

 npm install mocha chai --save-dev 

Теперь нам нужно обернуть код Selenium, который мы написали в тестовом примере Mocha. Создайте файл test/axe.spec.js со следующим кодом:

 const assert = require('chai').assert; const axeBuilder = require('axe-webdriverjs'); const webDriver = require('selenium-webdriver'); const driver = new webDriver.Builder() .forBrowser('phantomjs') .build(); describe('aXe test', () => { it('should check the main page of SitePoint', () => { // a Mocha test case can be treated as asynchronous // by returning a promise return driver.get('http://sitepoint.com/') .then(() => { return new Promise((resolve) => { axeBuilder(driver).analyze((results) => { assert.equal(results.violations.length, 0); resolve() }); }); }) .then(() => driver.quit()) }) // The test might take some 5-10 seconds to execute, // so we'll disable the timeout .timeout(0); }); 

Тест выполнит очень простое утверждение, проверив, равна ли длина массива results.violations 0. Чтобы запустить тесты, измените скрипт теста на вызов Mocha:

 "scripts": { "test": "mocha" }, 

Следующим логическим шагом этого упражнения будет создание более подробного отчета об ошибках, когда тест не пройден. После этого также было бы полезно интегрировать его с вашей любимой средой CI, чтобы правильно отображать результаты страницы. Я оставлю обе эти вещи в качестве упражнений для читателя и перейду к некоторым полезным, дополнительным параметрам конфигурации топора.

Расширенная настройка

По умолчанию ax выполнит все проверки по умолчанию для всей страницы. Но иногда более желательно ограничить область тестируемых веб-сайтов или объем проверок.

Проверка только частей сайта

Вы можете ограничить, какие части веб-сайта должны быть проверены или пропущены, используя методы include и exclude .

 axeBuilder(driver) // check only the main element .include('main') // skip ad banners .exclude('.banner') .analyze((results) => { // ... }); 

Это может быть полезно в том случае, если вы хотите проверить только те части веб-сайта, над которыми вы сейчас работаете, или исключить те части, которые вы не можете исправить или не можете напрямую контролировать.

Выбор правил

Каждое правило в топоре помечается одним или несколькими тегами, которые группируют их вместе. Вы также можете отключить или включить некоторые правила, используя методы withRules или withTags .

  • withRules позволяет вам включать и настраивать определенные правила по идентификатору. Например, следующий пример будет проверять только цветовой контраст и имена ссылок.

     axeBuilder(driver) .withRules(['color-contrast', 'link-name']) .analyze((results) => { // ... }); 
  • withTags позволяет вам включить правила, помеченные определенным тегом (в данном случае wcag2a ):

     axeBuilder(driver) .withTags(['wcag2a']) .analyze((results) => { // ... }); 

Вывод

Axe позволяет частично перенести усилия по тестированию доступности на машину и высвободить время для выполнения других задач, таких как общий дизайн и структура проекта. Хотя вам все еще нужно проделать определенную работу, чтобы аккуратно интегрировать ее в свою среду разработки и CI, это все же лучше, чем делать это вручную. И когда вы делаете это для одного из ваших проектов, интеграция со следующим должна быть легкой.

Есть и другие инструменты, основанные на топоре. Плагин Axe Chrome позволяет быстро просмотреть любую страницу в браузере. Если вы используете Gulp, вы также найдете плагин Gulp Axe . Для проектов на основе React есть плагин для React StoryBook, который позволяет вам проверять доступность ваших компонентов React. Вы можете обнаружить, что один из них лучше подходит для ваших нужд.

Надеемся, что это даст больше команд мотивации, чтобы начать думать о доступности в своих проектах. Как вы думаете: топор кажется полезным инструментом? Вы можете попробовать это в своем следующем проекте? Дай мне знать в комментариях.

Эта статья была рецензирована Мэллори ван Ачтерберг , Доминик Майерс , Ральф Мейсон и Джоан Инь . Спасибо всем рецензентам SitePoint за то, что сделали контент SitePoint как можно лучше!