Unlock the Power of Accessible Testing with React

Crafting inclusive user experiences, one test at a time.

Brandon Dickson
The Tech Collective
6 min readJun 18, 2024

--

Image created by the author

Testing; every developer’s favourite subject! It may seem tedious at times, but writing robust tests is at the core of good software development. With the ever-increasing number of devices and dimensions for Frontend developers to consider, writing tests that ensure good accessibility is more important than ever.

In this article, I will outline a few simple ways to approach testing in React with an accessibility-first mindset. When I mention certain libraries and tools, I will presume some basic understanding and not go into specific details like setup/installation, etc.

React Testing Library

This is one of the most popular libraries for testing in React, with currently over 9 million weekly downloads. It is a lightweight solution for testing React components and has a user interaction-driven approach. React Testing Library recommend using Jest, but it can be paired with other Javascript testing frameworks such as Mocha.

Queries

When writing tests for React components, queries will be one of (if not the most) used features. These are methods that can be called to find elements on the page. There are various methods available, but not all are created equal when it comes to properly testing accessible elements on the page.

Let’s take the below test as an example:

import {render, screen} from '@testing-library/react'

test('should login', () => {
render(<LoginForm/>)
const username = screen.getByTestId('username-input')
...
})

Here we are testing a simple login form component. In order to get the username input field, we have used the getByTestId method. Assuming that this element exists on the page and has the correct test ID assigned to it then the test will be fine and not throw an error. This is, however, not optimal for accessibility because it uses an identifier that can not be seen (or heard) by the user.

Now let’s look at an improved version, with accessibility in mind:

import {render, screen} from '@testing-library/react'

test('should login', () => {
render(<LoginForm/>)
const username = screen.getByRole('textbox', {name:'Username'})
...
})

In this example, we have replaced the use of getByTestId with getByRole. We’ve also passed in additional parameters in order to be specific and identify the element by its role on the page as well as the name associated with it.

This approach is far better for accessibility because it queries elements that are exposed in the accessibility tree, meaning we are testing in such a way that is aligned with how assistive technology, like screen readers, would perceive and interact with the elements. Therefore, we are simulating a more realistic user experience as the end user does not have access to things like test IDs.

The getByRole method should be the go-to method for querying elements on the page. It will be suitable for most cases but, for any situations where it is not, below is the list of methods in order of their priority:

  1. getByRole
  2. getByLabelText
  3. getByPlaceholderText
  4. getByText
  5. getByDisplayValue
  6. getByAltText
  7. getByTitle
  8. getByTestId

However, before running to use one of these other methods, have a deeper look at how the component is written as there may be an opportunity to write it in a more accessible way with better use of roles.

More information on these methods and their priority can be found here.

fireEvent vs userEvent

Another important part of testing React components is having the ability to perform user actions. There are two main ways of achieving this in React Testing Library which are fireEvent & userEvent.

Let’s first look at an example using fireEvent, continuing on from our tests above.

import {render, screen, fireEvent} from '@testing-library/react'

test('should login', () => {
render(<LoginForm/>)
const username = screen.getByRole('textbox', {name:'Username'})
fireEvent.change(username, {target: {value: 'michaelscott'}})
})

Now here’s another example, but utilising userEvent this time.

import {render, screen} from '@testing-library/react'
import userEvent from '@testing-library/user-event'

test('should login', async () => {
const user = userEvent.setup()
render(<LoginForm/>)
const username = screen.getByRole('textbox', {name:'Username'})
await user.type(username, 'michaelscott')
})

Both of these methods achieve the same end result (changing the value of the input field), however, they work differently under the hood. Here’s a quote directly from the documentation:

fireEvent dispatches DOM events, whereas user-event simulates full interactions, which may fire multiple events and do additional checks along the way.

So how does this affect our ability to ensure our components are accessible? Well, although fireEvent can be useful for testing certain behaviours within the component, it doesn’t really reflect how real users, including those using assistive technologies, would interact with the component.

Also, it’s important to consider that, when using fireEvent, it may not trigger all of the side effects that would be triggered by a real user interaction.

In conclusion, it is therefore preferable to use userEvent wherever possible and default to this method for simulating user actions.

jest-axe

This powerful library makes it easy to use axe, which is an accessibility engine, in your React tests. For the purposes of this article, I will be using it with React Testing Library for consistency but it can be used with various other libraries — more details on those can be found here.

We’ll continue with our login form example; below is a simple version of the component which does not follow good accessibility standards.

const LoginForm = () => (
<form>
<input type="text" />
<input type="password" />
<button type="submit">Login</button>
</form>
);

We can then create a test for this component using the jest-axe library:

import { render } from '@testing-library/react';
import { axe, toHaveNoViolations } from 'jest-axe';

test('should have no accessbility violations', async () => {
const { container } = render(<LoginForm />);

const results = await axe(container);

expect(results).toHaveNoViolations();
});

When we run the test, it produces the following output which has identified some violations; we forgot to add labels to our input fields!

Screenshot by the author

From this simple test, we have identified accessibility violations in our component, and can quickly rectify them with the suggested changes. This is a small example but you can see how powerful this could be with much more complex components.

From this feedback, we can then improve our component like so:

const LoginForm = () => (
<form>
<label htmlFor="username">Username</label>
<input type="text" id="username" />
<label htmlFor="password">Password</label>
<input type="password" id="password" />
<button type="submit">Login</button>
</form>
);

It’s important to mention that there are obviously many other ways to further increase the accessibility here such as styling, form validation and additional ARIA attributes; but this is just a small example to show the power of the library and how it can easily help to strengthen the foundations of your components.

Accessibility in Storybook

Storybook is a popular library for building UI components and pages in isolation. It also has great add-ons that can be used to enhance its capabilities, one of these is storybook-addon-a11y, which will show any accessibility violations in the component, similar to how jest-axe does.

You can start by adding the add-on to your Storybook config:

const config: StorybookConfig = {
addons: [
'@storybook/addon-a11y',
],
};
export default config;

Then, once you launch the Storybook environment, there will be an accessibility tab available, where you can view the violations as well as the passes. Below is what it would look like for our not very accessible login form.

Screenshot by the author
Screenshot by the author

This is another powerful way to ensure that your components are fit for purpose accessibility-wise. This can be a great addition, especially if you’re already using Storybook for visual testing.

Conclusion

I have highlighted just a few of the many possible ways that you can start incorporating accessible testing into all of your React applications. It should act as a starting point, and help to build a strong foundation for further development.

This is not a surefire way to ensure that your applications are 100% accessible and, if possible, you should try to test your software with the accessibility technologies that real users use.

Thanks for reading 😊

--

--

The Tech Collective
The Tech Collective

Published in The Tech Collective

Welcome to our publication, where technology and engineering culture take center stage. Delve into the fascinating world of tech, exploring its advancements, challenges, and impact on society. Share your insights and experience!

Brandon Dickson
Brandon Dickson