(Photo by Bundo Kim on Unsplash)
Before diving into this post, you should know what accessibility is. A good place to start could be this "What is accessibility" article by MDN.
Usually the biggest and most common selling point that I see for writing accessible web applications is, in short, making your apps usable to users that rely on assistive technologies. That short statement alone can be split into multiple other very elaborate reasons, like the ones you'll see listed in the article I linked above. They are all true, but they all revolve around the user-facing benefits of accessibility, and this will be the case as well with most other documentation you stumble upon online.
This can be a problem because my professional experience shows that most companies and enterprises will bail out of investing in engineering efforts for accessibility claiming that the user-base percentage that will actually need it is too small to justify the expense. They will probably not use those harsh words though, or probably don't even address the issue in the first place. And you know what, although it might sound cruel, it can make total business sense in many scenarios, i.e. a software that is only used internally, and the company is 100% certain that none of its employees are handicapped in some way, therefore, will not need it.
However, despite this, I've always tried to write my code as accessible and semantic as possible within the budget my teams are allowed, as I feel it is my ethical duty as a professional of the web to not only deliver the highest quality code to my employers, but also the highest quality apps to its users. I like to think of it as an unofficial unspoken oath that I've made, similar to those doctors do on movies and T.V. shows, if you know what I mean.
In doing so, I've noticed certain unexpected developer facing benefits that are hardly ever discussed and might shift the mentality of development teams and drive them to do the same. Let's go through some examples to illustrate my point.
Case One
In many teams and OSS projects that I've worked on I see this style of UI tests, or similar:
const submitBtn = document.querySelector('.btn-primary')
Simulate.click(submitBtn)
expect(submitBtn.classList).toInclude('btn-pimrary__disabled')
expect(submitBtn.classList).toInclude('btn-pimrary__loading')
// ...
In short, using CSS class names or selectors to find elements and write the assertions of the tests. To some of you reading it might be obvious that this is an anti-pattern and not the best of practices, but I assure you, it is not so obvious to everyone. Just this week alone I changed a class name that broke a multitude of tests unnecessarily that I later wasted the rest of my day fixing, that incident alone was enough motivation for me to write this post.
The HTML standard is rich enough that you can do all of this and more, more semantically and resiliently, without relying on any style related attributes or rules at all? Heck, if you are using a CSS-in-JS solution or similar that scrambles your class names this might not even be possible to you in the first place, and in that case people fall back to rely in implementation details of their UI components to achieve the same thing, which is also a bad practice.
Let's look at my proposed alternative:
const submitBtn = getByText('Submit')
Simulate.click(submitBtn)
expect(submitBtn.hasAttribute('disabled')).toBe(true)
expect(submitBtn.hasAttribute('aria-busy')).toBe(true)
With WAI-ARIA and regular HTML attributes, you can represent almost any possible (if not all) state that your elements can be in, including active/inactive tabs, expanded/collapsed panels, loading/ready elements, disabled/enabled inputs or buttons, valid/invalid forms, visibility... you name it. You will not only be making your tests much more easier to write, but also much more robust, readable and semantic, not to mention that you would be making your app more accessible in the process, it's a win-win scenario in my book. I'm usually hesitant to talk about "readability" because I've noticed that it is hugely sensitive and subjective, but I think I'm confident with using it in this case. Click here for a full list of state related ARIA attributes.
If you use Jest and the Testing Library suite of testing tools, you can get even higher quality tests by updating the above to:
const submitBtn = getByText('Submit')
Simulate.click(submitBtn)
expect(submitBtn).toBeDisabled()
expect(submitBtn).toHaveAttribute('aria-busy', 'true')
And if your assertions fail, you'll get errors like:
Received element is not disabled:
<button>Submit</button>
and
Expected the element to have attribute:
aria-busy="true"
Received:
aria-busy="false"
Which I think we can all agree is better than just Expected false to be true
.
Case Two
Let's say you have to implement a table with checkboxes that looked like this:
The checkboxes are kind of "floating" around in this table with no immediate indication as to what might be their purpose. By looking at the entire picture though, you can probably infer that each checkbox value is associated with the combination of the column and the row names. Just for the sake of an example, let's say we replace the column names with days of the week, let's go with Monday, Wednesday and Friday, and the rows with activities or chores, if we see a checkbox checked in the "Wednesday" and "Mow lawn" intersection, we could say that that is an activity that either has to be done on that day, or was done on that day.
But what if you had to only rely on the contents of the markup to figure that out, without seeing any layout? Regardless of whether this is a good design and representation for that type of data or not, let's use it for this exercise. Minimalistically speaking this could be the HTML behind it:
<table>
<thead>
<tr>
<th></th>
<th>Col1</th>
<th>Col2</th>
<th>Col3</th>
</tr>
</thead>
<tbody>
<tr>
<td>Row1</td>
<td><input type="checkbox" /></td>
<td><input type="checkbox" /></td>
<td><input type="checkbox" /></td>
</tr>
<!-- Row2, Row3... -->
</tbody>
</table>
Would you be able to figure out the purposes of this table and the checkboxes from that markup as quickly and easily? What if you are a developer coming into this screen for the first time to fix a bug, and maybe you are looking at this markup directly in the code or in a failed test, would it be immediately obvious to you how this UI component works? In a real scenario this could be a table rendering several dozen columns and rows, and have a lot of added markup for styling, making it even more difficult to inspect. As a side note, although we already stated that this is not a user-facing oriented post, imagine being a blind user relying on a screen reader to decipher this UI... it wouldn't go smoothly, to say the least.
We can improve this greatly by just adding:
<table>
<thead>
<tr>
<th></th>
<th>Col1</th>
<th>Col2</th>
<th>Col3</th>
</tr>
</thead>
<tbody>
<tr>
<td>Row1</td>
<td><input type="checkbox" aria-label="Col1 + Row1" /></td>
<td><input type="checkbox" aria-label="Col2 + Row1" /></td>
<td><input type="checkbox" aria-label="Col3 + Row1" /></td>
</tr>
<!-- Row2, Row3... -->
</tbody>
</table>
Feel free to format or phrase the label anyway you like, but now, in a huge wall of HTML it is perfectly clear what the purpose of the checkbox is, without the need to see the visual layout of the elements. This small detail can save a developer a lot of time and headaches when working with this component in the future, debugging an issue or adding new functionality.
What about writing tests?
const checkbox = getByLabelText('Col2 + Row1') as HTMLInputElement
expect(checkbox.checked).toBe(true)
Without this label, you would have to rely on very flaky CSS selectors that would leak implementation details of your component into your tests, and end up breaking with the smallest change to the markup, when refactoring or changing only styling. Not going to bother providing a snippet for how that would look like since there could be a million ways to do it, and they would all be bad.
You can go one step further to improve these inputs by also providing a tooltip of some form to the input element. A quick-fix there would be reaching out for the title
attribute as well and mirroring the value of the label in it. Keep in mind though that title
attributes have certain limitations that are clearly described in this article by Heydon Pickering: Tooltips & Toggletips. Or check out Reach UI's Tooltip component, even if you're not using React, you can learn a lot from its implementation if you'd like to roll out your own. You will notice it's not trivial.
Final Thoughts
Although it may not seem like much, this approach evolves into robust and readable tests that serve as not only as bug barriers but more importantly as easy to digest coded documentation on how the components work in a way that other types of test don't, which greatly increases the productivity of developers on the team. The ones that are going to notice the most are unfamiliar developers coming into sections of the codebase and quickly being able to get up to speed.
This is extremely valuable in companies with dozens of developers that contribute across the whole platform. And that's without mentioning the implementation code itself, that will be a clearer reflection of the intent of the developer who wrote it.
Top comments (4)
I'm pretty inexperienced with JS testing & just bad about including accessibility tags, so this may be an uneducated question: Could you kill two birds with one stone by using accessibility tags in your query selector?
If it's readable enough for you to write your tests wouldn't that also help you make sure screen readers, etc are getting the correct information?
Yes you can, in fact, Testing Library's
getByLabelText
will actually check thearia-label
andaria-labelledby
attributes of elements when searching for nodes with the given label (see part of the magic in the source here)In some cases it can make perfect sense to include the ARIA attributes in your query selectors, in others not so much. Think of the assertion error message you would be getting in your terminal when running the test and evaluate if it is a clear representation of what's going on.
Also think of what it is that you are trying to assert, if we go back to one of the examples in the post, you could try to find an element with the
aria-busy
attribute with just one query selector and kill two birds with one stone, like you said, but in reality, the presence of the element, and whether the element is busy or not are two separate points of failure that should have two separate expectations, instead of a combined one.Does it make sense?
This is a great list!! I never knew about the aria-busy attribute. Definitely going to be using that more in my apps instead of my classic “loading” class.
Do you have any good resources for the widely used aria attributes? I’ve tried to wade through docs before and really the only things I can find are the w3c docs...and those are pretty intimidating.
Hey Adam, great question! Here's the "official" list of state related ARIA attributes: w3.org/WAI/PF/aria-1.1/states_and_... (scroll down to see an actual bullet list) that maybe you already bumped into 😅.
As for a list of widely used attributes, I haven't seen any myself but I assume it must exist. Maybe I would suggest scanning through the full list and taking notes of the ones that sound like attributes you would use often. That would vary depending on the types of apps each developer writes.
And on another note, MDN has a lot of great posts on Accessibility that I think you'll find pretty helpful and you can start there before diving into the W3 docs:
developer.mozilla.org/en-US/docs/L...
Let me know how it goes! Also if you find any resource that helped you reply here and I'll add it to the post, I'm sure it'll be helpful to others as well.