In Part One of this series, I talked through some of the ethical and moral rationales for making a website accessible to as many people as possible. It's good for your users, and it's good for your code, so it's a good idea. But I didn't actually lay out the process of adding accessibility checks to an application. Or show any code. So that seems like a good thing to cover in this series on adding accessibility checks to an application, huh?
So how do we do it? The rest of this post will talk about specific tooling in Ruby and Rails for building and maintaining automated accessibility checking, but there are some general principles we can apply in other languages:
axe
(which I'll talk about in a second).<p>
tags, lists just group <li>
tags, buttons use <button>
tags, and so on.These guidelines highlight the ultimate goal: make it simple to add accessibility checks and run them when you run your test suite. In a new Rails project, a good initial setup for automating your tests will make it WAY easier to keep your site accessible. You'll need the following:
axe
, which we'll use to run automated checks. The rules reference list for axe
version 4.4 can be found here. If you need more context, I've also found the MDN docs for Accessibility to be really, really good. Again, those are here.System testing is a big, big topic, and beyond the scope of this post. There's a lot of great writing on the subject out there, and I'd encourage you to check some of it out. Start with these (and maybe this, because it's a nice pattern to use).
If you need to set up system tests from scratch, start by installing these gems:
group :system_tests, :test do
gem 'capybara'
gem 'selenium-webdriver'
end
After configuring system testing, add something like the following spec to one of your page specs. We'll use this as our main example, so just imagine something that works for your site layout whenever you see this spec referenced here.
it "can load the root", :system do
visit root_path
page_body = find("body")
expect(page_body).to have_css(".body-class")
end
In our system, the :system
tag tells RSpec that we want to use a headless Selenium Chrome instance as the browser for system specs. We can configure that in a new file: spec/support/system_test_configuration.rb
.
RSpec.configure do |config|
config.before(:each, type: :system) do
driven_by :selenium_chrome_headless
end
end
This can also be inferred from the file location by calling infer_spec_type_from_file_location!
during RSpec config, if you prefer that. One reason I prefer tagging it explicitly is the ability to tell RSpec to only run a specific tag:
bundle exec rspec --tag type:system
If you're working on an existing app that needs to be accessible, this'll come in REALLY handy in Part Three (link forthcoming on publication).
If the call to RSpec above did not return any errors and succeeded (green dot in RSpec), all of the following should be true:
If it failed (big red X), congratulations! You either found a bug, or my example led you astray and won't work for your application. You'll need to fix it before proceeding.
This section will demonstrate a basic approach to checking accessibility in a new project. After configuring system testing, you need a way to add axe
to run the Deque checks. Add these gems to the Gemfile (gems also exist for other system test configurations, see here):
group :accessibility, :test do
gem "axe-core-capybara"
gem "axe-core-rspec"
end
If you're writing spec cases that test accessibility, it's generally wise to avoid code that isolates accessibility. As briefly discussed above, the major concerns are A) increasing the number of times we have to interact with Chromedriver (which is slow), and B) missing accessibility errors because we didn't trigger a state that violates our accessibility standards. For instance, the following pattern is still relatively common in some corners of the codebase I currently work on:
it "is accessible", system: true do
visit users_path
# This is a matcher provided by axe for accessibility.
expect(page).to be_axe_clean
end
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
Note the use of be_axe_clean
. This is a custom RSpec matcher provided by installing our accessibility gems. When you include it in a system spec, axe
loads the relevant standard (WCAG 2.1 in this case), and runs a given page through accessibility checks. When dealing with a legacy application, modifying the behavior of this matcher is REALLY important. We'll cover that in Part Three (link again forthcoming).
While it's relatively unlikely in this example, there may be states and behavior that are not actually accessible. For instance, when I was working on upgrading accessibility in my application, I fought a long, bitter battle with a JS-backed drop-down list that just completely fails to follow accessibility principles. The dropdown menu won. We don't talk about the dropdown menu.
There's no valid reason to treat accessibility as a separate concern from system testing in general. If you're going to do it, do it as part and parcel of the system specs you're already running:
it "can visit the user creation page from the users index", :system do
visit users_path
initial_page = page
click("Create New User")
expect(initial_page).to be_axe_clean
expect(page).to have_current_path(new_user_path)
expect(page).to be_axe_clean
end
If this spec succeeds, we can be confident that:
axe
(because it should raise a violation otherwise).And that's it. You just add be_axe_clean
to specific specs. If it fails, you'll see an error, with details, and can fix it by using the tools provided above.
Using axe
gives you really detailed error messages by default. In the example below, axe
needs you to wrap a page element within an HTML5 landmark element.
1) Index Page Load
Failure/Error: expect(page).to be_axe_clean
Found 1 accessibility violation:
1) region: All page content should be contained by landmarks (moderate)
https://dequeuniversity.com/rules/axe/4.4/region?application=axeAPI
The following 1 node violate this rule:
Selector: body > span
HTML: <span style="font-size: 14px;">All</span>
Fix any of the following:
- Some page content is not contained by landmarks
Invocation: axe.run({:exclude=>[]}, {}, callback);
[Screenshot]: <a path to a screenshot stored on your system>
As long as you consistently add be_axe_clean
to your system specs, and don't neglect them (don't neglect them!), you should be able to keep your code up to date with modern accessibility standards.
That, generally, is it. Assuming that you've gotten system testing set up correctly, and can run them without accessibility checks, you can add an accessibility library, run checks at specific points in your test suite, and use the output to guide your code workflow. Automated testing won't get you 100% of the way to an accessible site, but it can surface, document and monitor known issues and help you resolve them BEFORE they impact your users.
But this guide assumes that you're starting from scratch, with a fresh codebase. That's not the situation most of us encounter in our daily lives. In most of the projects and repos I've worked on, I've primarily dealt with code that I didn't write myself. Working in a live codebase means that it's really unlikely that you'll be able to take a sledgehammer to every possible accessibility issue you encounter.
In that scenario, the goal is mitigation and triage, not perfection. Making your codebase fully accessible is completely doable, but slogging through every single HTML file by hand is a recipe for sprint bloat, missed project deadlines, scope creep and stressed-out developers. In Part Three (link also forthcoming, surprise!), we'll cover a set of helpful tricks you can apply to organize and upgrade your app's adherence to accessibility standards without completely losing your mind at a poor little select tag that REALLY just wants to be invisible.
Learn more about how The Gnar builds Ruby on Rails applications.