Это третья часть серии из трех частей о том, «как на самом деле выглядит доступность, когда вы реализуете ее в Rails-приложении». В первой части мы рассмотрели некоторые моральные, этические и профессиональные обоснования необходимости создания сайта, который будет адекватно обслуживать как можно больше людей. Во второй части рассматривалась реализация проверок доступности в совершенно новом приложении, а также минимальный обзор настройки системных спецификаций. Если вы новичок в системном тестировании в целом, я бы настоятельно рекомендовал изучить некоторые ссылки в этой статье, чтобы получить хорошее представление о системных спецификациях, прежде чем переходить к следующей части.
Это потому, что в третьей части этой серии мы поговорим о монстре под кроватью: что делать с существующим кодом? Что, если до сих пор доступность не была приоритетом в нашей кодовой базе? Как нам это исправить?
- Доступность в старых проектах, или «О боже, посмотрите на все эти провалы».
- Обзор подхода
- Некоторые допущения и руководящие принципы
- Заставить все провалиться 😀
- Избегание «ползучести объема
- Опирайтесь на RSpec
- RSpec Stamp Is Neat
- Исправляйте по одной вещи за раз
- Что делать, если проблему нельзя исправить?
- Подведение итогов
Доступность в старых проектах, или «О боже, посмотрите на все эти провалы».
Если мы хотим сделать существующий сайт доступным и добавляем некоторые автоматизированные проверки, как описано во второй части, мы можем внезапно столкнуться с океаном неудачных спецификаций. Планирование и выполнение модернизации доступности без потери рассудка проще, если использовать несколько приемов и тщательное планирование. Вот один из подходов.
Обзор подхода
Наша цель — взять существующее Rails-приложение, которое не соответствует стандартам доступности, и организовать рабочий процесс проекта, который позволит нам итерационно решать проблему и работать над достижением прочного, стабильного соответствия стандартам доступности.
Мы сделаем следующее:
- Создадим неудачные спецификации в любой спецификации, которая касается страницы, которая должна быть доступной.
- Задокументируйте эти сбои в электронной таблице и разбейте большой набор сбоев на более мелкие, более управляемые фрагменты.
- Настройте RSpec на запись и нацеливание файлов с отказами с помощью пользовательского помощника спецификации или общего контекста.
- Задокументируйте все известные проблемы в режиме реального времени с помощью тегов RSpec и пользовательского сопоставителя.
Подход, изложенный в этом посте, может быть применен к любому масштабному рефактору, требующему множества мелких изменений.
Некоторые допущения и руководящие принципы
Мы сделаем следующие предположения о текущем состоянии нашей кодовой базы, нуждающейся в обновлении доступности:
- Системные или страничные тесты были настроены, и для вашего приложения существует достойное покрытие.
- Проверки доступности выполняются в соответствии с вашими системными спецификациями и способны выявить достаточное количество проблем с доступностью.
- У вас есть чистая копия текущей ветки develop или main, которую можно использовать в качестве отправной точки. Процесс, описанный здесь, будет включать в себя значительное количество мелких исправлений, и его не следует совмещать с другими работами.
- Очень важно, чтобы у вас была достаточная пропускная способность и поддержка, чтобы запланировать время и ресурсы, необходимые для переписывания большого количества HTML. Попытка переписать весь фронтенд, чтобы исправить доступность, за один раз, скорее всего, выведет ваших коллег из себя и не позволит добиться существенного прогресса в достижении цели (что выведет из себя Product и QA). Не делайте этого (УВЕРЬТЕ МЕНЯ В ЭТОМ).
Но это код, и все всегда может стать немного сложнее. Вы, вероятно, столкнетесь со страницами и поведением, которые потребуют больше работы, чем вы можете оправдать, чтобы быть полностью доступными. Мы хотим избежать увеличения объема, и нам, возможно, придется пожертвовать идеальными, чистыми проверками доступности в пользу того, чтобы все сделать в разумные сроки. Как разработчику, мне очень трудно это сделать, потому что я всегда хочу, чтобы проблема была решена идеально, как только она будет выявлена.
Но совершенство — враг пути, пути, пути к лучшему, и мы можем многое сделать, чтобы вывести старые проекты на более качественный путь.
Поэтому давайте также установим основные правила для этого проекта:
- Мы не пытаемся исправить все сразу.
- Мы хотим использовать меньшие, более чистые PRS везде, где это возможно, чтобы сделать задачу выполнимой.
- Мы хотим отслеживать текущее состояние доступности из кодовой базы, включая проблемы, которые мы не можем исправить.
- Мы хотим исправить как можно больше проблем с доступностью, учитывая эти ограничения.
Для целей этой статьи мы также можем предположить, что наша спецификация из второй части в настоящее время не проверяет проблемы доступности и выглядит следующим образом:
it "can visit the user creation page from the users index", :system do
visit users_path
click("Create New User")
expect(page).to have_current_path(new_user_path)
end
Объект page представляет состояние текущей страницы, загруженной Capybara после выполнения указанного поведения.
Отлично. Мы можем приступить к работе. Что дальше?
Заставить все провалиться 😀
Первым шагом здесь будет определение ваших неудач. Не имеет значения, с чего начинать, потому что в конечном итоге вы захотите проверить доступность любой страницы, которую может посетить пользователь, использующий экранное устройство чтения, но, скорее всего, имеет смысл начать с самых важных маршрутов вашего приложения. Помните, что мы не хотим добавлять одну проверку доступности в каждый файл; мы хотим проверить доступность для любого состояния, которое может быть вызвано, потому что если мы не проверим сложное поведение, мы можем получить ложное срабатывание.
Например, допустим, у нас настроены проверки для базовой структуры индекса User:
RSpec.describe "User index page links", type: :system do
it "can load the index" do
visit users_path
expect(page).to have_css(".users-index-table")
expect(page).to have_content("Users")
expect(page).to have_css(".users-index-links-div")
expect(page).to have_content("Create New User")
end
it "can visit the user creation page from the users index" do
visit users_path
click("Create New User")
expect(page).to have_current_path(new_user_path)
end
end
Мы хотим обновить их для запуска проверок axe
:
RSpec.describe "User index loading + links", type: :system do
it "can load the index" do
visit users_path
expect(page).to be_axe_clean
expect(page).to have_css(".users-index-table")
expect(page).to have_content("Users")
expect(page).to have_css(".users-index-links-div")
expect(page).to have_content("Create New User")
end
it "can visit the user creation page from the users index" do
visit users_path
click("Create New User")
expect(page).to be_axe_clean
expect(page).to have_current_path(new_user_path)
end
end
Запустите его и посмотрите на вывод RSpec. Если есть axe
сбои, они должны появиться (подробнее об этом ниже). Прежде чем мы перейдем к другому файлу, нам нужно обсудить небольшую проблему: есть ли вообще сейчас смысл во втором спецификаторе в этом файле? Кажется, что она была бы более уместна в системной спецификации, нацеленной на форму отправки пользователя. Я бы утверждал, что это не самое подходящее место для внесения изменений. При проведении подобных масштабных рефакторингов важно не допустить сползания объема, а сопротивление желанию исправить все и сразу в конечном итоге облегчит управление всеми необходимыми изменениями в проекте.
Избегание «ползучести объема
Как уже говорилось, спецификация, которую мы используем, начинает выглядеть немного странно. Разве не логичнее было бы проверить доступность из системного теста, проверяющего поведение новой пользовательской формы? Если бы я был разработчиком, который смотрит на это, я бы, вероятно, захотел провести рефакторинг, заменив вторую спецификацию тестом, который подтверждает, что все ссылки в индексе пользователей загружаются и отображаются без проблем с доступностью. Нам нужно продублировать содержимое page
здесь, чтобы избежать повторного тестирования одного и того же объекта Capybara снова и снова (но только если мы следуем 4-фазному тестированию). Если мы не продублируем page
, Capybara обновит значение, хранящееся в этой переменной, и оба new_user_page
и destroy_user_page
ниже будут указывать на один и тот же объект в памяти.
it "can load all required links from the users index" do
visit users_path
click("Create New User")
new_user_page = page.dup
click("Back")
click("Remove User")
destroy_user_page = page.dup
expect(new_user_page).to be_axe_clean
expect(new_user_page).to have_current_path(new_user_path)
expect(destroy_user_page).to be_axe_clean
expect(destroy_user_page).to have_current_path(destroy_user_path)
end
Не делайте этого. По крайней мере, не делайте этого здесь. Этот тест более полезен для будущего читателя, но это также расширение границ. Время для внесения подобных изменений наступит, когда вы будете исправлять проблемы доступности в этом файле в отдельном коммите. Пока же все, что вам нужно для начала работы, — это найти свои сбои; улучшение кода, который их вызывает, произойдет в ближайшее время, но не должно выполняться здесь. Позже будет время улучшить ваш набор тестов, и это того стоит, но сейчас это опасно. Просто добавьте be_axe_clean
к каждой из ваших системных спецификаций и запустите их.
Это, вероятно, сломает большинство спецификаций. Некоторые страницы могут быть в порядке, что является отличной новостью для нас, потому что эти страницы, скорее всего, уже соответствуют требованиям. Мы можем пока проигнорировать их и сосредоточиться на неудачных случаях. Когда позже мы достигнем полной доступности, эти страницы должны быть полностью функциональными. В промежуточный период нам необходимо использовать некоторые средства автоматизации, чтобы сделать возможным переход кодовой базы на соответствующий стандарт.
Опирайтесь на RSpec
На данном этапе у вас должен быть список отказов доступности в выводе RSpec. Нам нужно задокументировать эти сбои и подготовиться к разделению работы на несколько небольших PR. Для этого нам нужен уникальный список всех сбоев, присутствующих в нашем приложении.
Когда мы делали это недавно, мы создали огромный лист Google, где каждый сбой доступности был задокументирован следующим образом:
Spec File | Строка | Путь к просмотру | Сбой |
---|---|---|---|
spec/system/users_spec.rb | 8 | путь_пользователей | page-has-heading-one: Страница должна содержать заголовок первого уровня (умеренный). |
Ведение подробного списка отказов поможет вам разделить работу на отдельные задачи. Мы решили разделить нашу работу по конкретным axe
неудачам, потому что это позволило получить PR, содержащие похожие изменения, и воспользоваться функциональностью RSpec matcher.
Как бы вы это ни организовали, вам нужно собрать список различных сбоев, которые присутствуют в вашем приложении. Каждый сбой представляет собой название нарушения, присутствующего в правилах Deque axe
.
В моем приложении после выполнения этого процесса мы получили следующий список:
known_failures = [
:label,
:list,
:listitem,
:region,
"aria-allowed-attr",
"aria-dialog-name",
"aria-required-children",
"aria-required-parent",
"color-contrast",
"heading-order",
"label-title-only",
"nested-interactive",
"page-has-heading-one",
"scrollable-region-focusable"
]
Каждый из этих сбоев представляет собой имя правила axe
и может быть знаком вам по выводу спецификации, возвращаемой после добавления be_axe_clean
.
Собрав подобный список для своего приложения, вы сможете создать вспомогательный метод spec, который позволит вам запускать набор spec без сбоев из-за известных проблем, что означает, что вы сможете добиться прогресса в решении проблем доступности, не утопая в неудачных тестах.
def be_axe_clean_for_upgrade
known_failures = [
:label,
:list,
:listitem,
:region,
"aria-allowed-attr",
"aria-dialog-name",
"aria-required-children",
"aria-required-parent",
"color-contrast",
"heading-order",
"label-title-only",
"nested-interactive",
"page-has-heading-one",
"scrollable-region-focusable"
]
be_axe_clean.skipping known_failures
end
Если вы настраивали spec/support/system_test_configuration.rb
во второй части, это может быть хорошим местом для этого. После того, как вы добавили этот метод куда-нибудь, используйте глобальную функцию find-replace для замены всех экземпляров be_axe_clean
на be_axe_clean_for_upgrade
. Когда вы повторно запустите вашу спецификацию, она должна стать зеленой (если вы не пропустили ни одного случая :D).
Это место, где вы разбиваете вашу текущую работу на ветки фич, которые будут использоваться как цель для всех PR по доступности. Сделайте это сейчас.
RSpec Stamp Is Neat
Есть еще один инструмент, который может оказаться полезным. RSpec Stamp — это инструмент, предоставляемый TestProf, который добавляет пользовательский тег к неудачным спецификациям при запуске. Он «штампует» их, чтобы вы могли запускать их в изоляции.
Этот шаг необязателен! Если вы не хотите возиться с дополнительными сложностями, вы можете добиться того же эффекта, добавив вручную axe:true
к каждой спецификации, которая проверяет доступность. Это выглядит следующим образом:
it "can visit the user creation page from the users index", axe: true do
visit users_path
click("Create New User")
expect(page).to have_current_path(new_user_path)
end
Это позволит вам запустить все соответствующие спецификации одновременно с помощью bundle exec rspec --tag axe:true
.
Однако, добавление этого везде раздражает, и вы можете использовать RSpec Stamp для автоматизации этого с помощью общего контекстного файла. Чтобы настроить это:
Установите TestProf (который также включает в себя множество других полезных вещей):
group :rspec_tests, :test do
gem 'test-prof'
end
Создайте общий контекст для сбоев доступности в spec/support/shared_contexts/axe_violation.rb
. Переместите текущий метод-помощник spec, содержащий список известных сбоев, внутрь этого контекстного файла.
RSpec.shared_context "axe violations", axe: :violation do
around(:each) do |ex|
def be_axe_clean_for_upgrade
known_failures = [
:label,
:list,
:listitem,
:region,
"aria-allowed-attr",
"aria-dialog-name",
"aria-required-children",
"aria-required-parent",
"color-contrast",
"heading-order",
"label-title-only",
"nested-interactive",
"page-has-heading-one",
"scrollable-region-focusable"
]
be_axe_clean.skipping known_failures
end
end
Вам нужно сказать RSpec включить этот общий контекст в любой файл, который включает определенный тег. В настоящее время такого тега нет ни у чего, поэтому мы добавляем его с помощью RSpec Stamp. Добавьте его в начало только что созданного файла контекста:
RSpec.configure do |rspec|
rspec.include_context "axe violations", axe: :violation
end
Вам нужно сохранить поведение передаваемых спецификаций, поэтому вам нужно будет определить значение для be_axe_clean_for_upgrade
в spec_helper.rb
или system_test_configuration.rb
. Это будет выполняться для всех спецификаций, которые не помечены axe: :violation
. Должно быть только это:
def be_axe_clean_for_upgrade
be_axe_clean
end
Запустите RSpec, и все должно быть снова сломано. Мы только что добавили общий контекст, который будет срабатывать только при наличии определенного тега в спецификации. Каждый сбойный спек использует более простую версию be_axe_clean_for_upgrade
, и корректно сообщает, что он не справляется с запущенными вами проверками. Чтобы сделать этот пример зеленым, выполните следующую команду:
RSTAMP=axe:violation bundle exec rspec
Это изменит каждую неудачную спецификацию, добавив axe: :violation
в качестве тега. Если вы повторно запустите RSpec, все должно пройти (если нет, возможно, RSpec Stamp что-то пропустил). Теперь вы можете запустить все спецификации, включающие нарушения, используя bundle exec rspec --tag axe:violation
, и изменить ваш общий контекст для запуска и устранения конкретных сбоев без изменения поведения всего остального. Кроме того, любые новые спецификации, добавленные в репозиторий, будут проходить полный набор проверок axe
, что позволит избежать необходимости играть в «Whack-A-Mole» с проблемами, которые были решены в предыдущем PR.
Исправляйте по одной вещи за раз
Если вы следили за развитием событий, то теперь у вас должны быть воспроизводимые случаи неудачных системных спецификаций, ссылки на ресурсы, объясняющие, почему они неудачны, ветка функций, настроенная на разрушение всех проблемных системных спецификаций, и метод, который вы можете изменить для выполнения конкретных нарушений. Вероятно, у вас также есть набор спецификаций с метками, которые выглядят следующим образом:
# The axe tag here may be axe: true, or just :axe, depending on your approach.
it "can visit the user creation page from the users index", :system, axe: :violation do
visit users_path
page_before_click = page
click("Create New User")
expect(page_before_click).to be_axe_clean_for_upgrade
expect(page).to have_current_path(new_user_path)
expect(page).to be_axe_clean_for_upgrade
end
Теперь вы можете исправить их атомарно. Обновите общий контекст или вспомогательный метод (если вы не использовали штамп RSpec), заменив тело be_axe_clean_for_upgrade
на следующее:
def be_axe_clean_for_upgrade
be_axe_clean.checking_only("whatever-error-you're-working-on-currently")
# known_failures = [
# :label,
# :list,
# :listitem,
# :region,
# "aria-allowed-attr",
# "aria-dialog-name",
# "aria-required-children",
# "aria-required-parent",
# "color-contrast",
# "heading-order",
# "label-title-only",
# "nested-interactive",
# "page-has-heading-one",
# "scrollable-region-focusable"
# ]
# be_axe_clean.skipping known_failures
end
Обновите HTML и спецификации, обращаясь к документации по мере необходимости, и продолжайте до тех пор, пока конкретный сбой не будет исправлен повсеместно. Верните be_axe_clean_for_upgrade
к исходному значению, удалите ошибку, которую вы исправили, из списка известных нарушений и запустите набор спецификаций заново. Теперь он должен стать зеленым.
Повторяйте этот процесс до тех пор, пока у вас не останется нарушений. Это может занять много времени и много коммитов, но оно того стоит. Простой код — это чистый код, а устройства чтения с экрана хотят, чтобы вы писали простую и понятную разметку. Просматривая свое приложение и исправляя все нарушения, которые вы можете найти, вы обычно удаляете ненужные сложности из ваших представлений, что очень важно для их долгосрочного здоровья и простоты понимания. Как только вы определили проблему, решение обычно сводится к «удалению ненужных вещей».
Что делать, если проблему нельзя исправить?
Исправить все своевременно может оказаться невозможным, особенно если вы работаете с большим приложением или ошибка вызвана специфической зависимостью. Например, у нас есть плагин jQuery на странице поиска, который управляет несколькими полями select
. Он не дружелюбен к читателям с экрана, но «переписывание пользовательского интерфейса и поведения поиска» не было разумной целью.
Если у вас возникают подобные ситуации, лучшее, что вы можете сделать, это отследить их. Лучший способ сделать это — использовать пользовательский RSpec matcher. Мы используем следующее:
RSpec::Matchers.define :match_known_axe_violations do |violations, violation_selectors|
include Capybara::DSL
match do |page|
passes_if_violations_skipped?(page) && !passes_when_targeting_violation?(page)
end
failure_message do
base_message = "Known violations: #{Array.wrap(violations).join(', ')}n"
"Violation selector: #{Array.wrap(violation_selectors).join(', ')}n"
if !passes_if_violations_skipped?(page)
"#{base_message}Audit failed outside of known violation(s)!n"
"#{audit_skipping_violation.failure_message}"
elsif passes_when_targeting_violation?(page)
"#{base_message}Audit did not fail when targeting violations, has this been fixed?"
else
super
end
end
define_method :audit_skipping_violation do
@audit_skipping_violation ||= be_axe_clean.skipping(violations)
end
define_method :audit_targeting_violation do
@audit_targeting_violation ||= be_axe_clean.checking_only(violations)
end
def passes_if_violations_skipped?(page)
audit_skipping_violation.matches?(page)
end
def passes_when_targeting_violation?(page)
audit_targeting_violation.matches?(page)
end
end
Сопоставитель запускает axe
дважды. На первой итерации он пропускает определенное нарушение, основанное на аргументах, переданных ему. На второй итерации он убедится, что нарушение, переданное матчеру, возвращено текущей версией страницы. Он принимает селектор для целей документации, хотя и не нацелен конкретно на проблему.
Сохранив его в spec/support/matchers/
, вы можете заменить be_axe_clean_for_upgrade
в тех местах, где что-то не может быть исправлено. В данном примере страница имеет нарушение в scrollable-region-focusable
, расположенном по указанному пути селектора.
it "can visit the user creation page from the users index", :system, axe: :violation do
visit users_path
index_page = page.dup
click("Create New User")
new_user_page = page
expect(index_page).to be_axe_clean
expect(new_user_page).to have_current_path(new_user_path)
expect(new_user_page).to match_known_axe_violations(
[:region, "scrollable-region-focusable"],
"body > div:nth-child(4), select.selectpicker",
)
end
Итак, теперь мы знаем, что это проблема, можем обнаружить ее, когда будем исправлять, и имеем запись о том, что не получилось. Поэтому мы можем исправить простые вещи, убедиться, что новый код остается доступным, и включить более значительные рефакторы в планирование проекта.
После того, как вы все исправили, вы можете использовать глобальный поиск и замену, чтобы найти теги и матчеры, которые мы добавили здесь, и удалить их из ветки функций, содержащей ваши изменения доступности, перед объединением.
Подведение итогов
По сути, реализовать доступность проще всего, если работать небольшими, дискретными частями, которые можно разделить на части и оценить по отдельности. Внедрение стандарта доступности — серьезная задача, и она должна быть продумана и задокументирована, чтобы не сойти с ума от медленных и неудачных тестов.
Надеюсь, эта серия статей будет полезна следующим людям, которые наберут в гугле «rails how to accessibility» и найдут подавляющее количество информации. С помощью простой организации и нескольких трюков RSpec вполне возможно сделать полностью совместимое веб-приложение (или, по крайней мере, приблизиться к этой цели достаточно близко).
Благодарность за внимание: существует открытый вопрос, связанный с переназначением автора на этой платформе. Этот пост был написан единственным и неповторимым @nialbima и опубликован контент-менеджером The Gnar Company.