Accessibility Aware Component Testing

Vishal Sharma
5 min readJul 2, 2024

--

Imagine you are building a global user-facing application. All the major features are ready and you are eagerly waiting to take the application to production. But wait, the team suddenly realised that the application accessibility score was too low and wouldn't be usable to an extensive set of users.

BOOM……. We cannot release the app now and the go-to-market timeline will take a big hit.

This is a prevalent problem when we don't give much importance to the NFRs (Non-Functional Requirements) and Accessibility is one of those. In this article, I will discuss how we can Shift Left Accessibility Testing to the unit level by leveraging some good practices and tools available in the market.

- ByRole query in React Testing Library

We often try to use an escape hatch by adding test IDs to the HTML elements and then using getByTestId query to get hold of elements in the unit tests.

Lets say that we have an inaccessible form where inputs are not tied with their label.

<>
{isFormSubmitted ? (
<section>
<img className="logo" src={reactLogo} />
<h4 data-testid="welcome-message">
Welcome to React Conf 2023 {firstName} {lastName}
</h4>
</section>
) : (
<form>
<div className="mb-3">
<label className="form-label">
First Name
</label>
<input
className="form-control"
data-testid="first-name"
value={firstName}
onChange={onChangeFirstName}
/>
</div>
<div className="mb-3">
<label className="form-label">
Last Name
</label>
<input
className="form-control"
data-testid="last-name"
value={lastName}
onChange={onChangeLastName}
/>
</div>

<div className="d-grid gap-2">
<div data-testid="submit-form" onClick={submitForm} className="btn btn-primary" type="submit">
Submit
</div>
<button className="btn btn-outline-primary" onClick={resetForm}>
Reset
</button>
</div>
</form>
)}
</>

Since we are adding testIds, it is very easy to add the unit test for this inaccessible form

test("welcomes the user after submitted the form", async () => {
render(<UnaccessibleApp />);

const firstNameInput = screen.getByTestId("first-name");
fireEvent.change(firstNameInput, { target: { value: 'Harry' } })

const lastNameInput = screen.getByTestId("last-name");
fireEvent.change(lastNameInput, { target: { value: 'Potter' } })

const submitButton = screen.getByTestId("submit-form");

fireEvent.click(submitButton);

expect(screen.getByTestId("welcome-message").textContent).toEqual(
"Welcome to React Conf 2023 Harry Potter"
);
expect(screen.getByText("Welcome to React Conf 2023 Harry Potter")).not.toBeNull();
});

Even the div with click listener which behaves like a button to submit the form works as expected but is totally inaccessible.

To overcome this problem, React Testing Library does provide byRole queries to get the element by their accessible role.

So the same form with proper accessible role should use the accessible roles to get hold of the element.

{isFormSubmitted ? (
<section>
<img className="logo" src={reactLogo} alt="React Logo" />
<h4>
Welcome to React Conf 2023 {firstName} {lastName}
</h4>
</section>
) : (
<form onSubmit={onSubmit}>
<div className="mb-3">
<label className="form-label" htmlFor="firstName">
First Name
</label>
<input
className="form-control"
id="firstName"
value={firstName}
onChange={onChangeFirstName}
/>
</div>
<div className="mb-3">
<label className="form-label" htmlFor="lastName">
Last Name
</label>
<input
className="form-control"
id="lastName"
value={lastName}
onChange={onChangeLastName}
/>
</div>

<div className="d-grid gap-2">
<button className="btn btn-primary" type="submit">
Submit
</button>
<button type="reset" className="btn btn-outline-primary" onClick={resetForm}>
Reset
</button>
</div>
</form>
)}
</>
test("welcomes the user after submitted the form", async () => {
render(<AccessibleApp />);

const firstNameInput = screen.getByLabelText("First Name");
fireEvent.change(firstNameInput, { target: { value: 'Harry' } });

const lastNameInput = screen.getByLabelText("Last Name");
fireEvent.change(lastNameInput, { target: { value: 'Potter' } });

const submitButton = screen.getByRole("button", { name: "Submit" });

fireEvent.click(submitButton);

expect(screen.getByRole("heading", { level: 4 }).textContent).toEqual(
"Welcome to React Conf 2023 Harry Potter"
);
});

The test will fail if:

  • we remove the htmlFor attribute from label or event ids from the input elements.
  • we use any other element than a button with the name Submit to submit the form.
  • the submit button is disabled while clicking on it.
  • we change the heading level of the welcome message.

Thus making the form more accessible by checking it at the unit level only.

- userEvent instead of fireEvent

user-event is a companion library for the testing library that simulates user interactions by dispatching the events that would happen if the interaction took place in a browser.

The problem is that the browser usually does more than just trigger one event for one interaction. For example, when a user types into a text box, the element has to be focused, and then keyboard and input events are fired and the selection and value on the element are manipulated as they type.

fireEvent dispatches DOM events, whereas user-event simulates full interactions, which may fire multiple events and do additional checks along the way. It adds visibility and interactability checks along the way and manipulates the DOM just like a user interaction in the browser would.

One interesting example of the accessibility error caught by userEvent but not fireEvent is pointer-events: none CSS property. If you try to click a button having this CSS property using userEvents, the test will fail unlike using fireEvent.click

- Use packages to add accessibility assertions

Plugins like jest-axe or vite-axe which is a custom Jest matcher for axe for testing accessibility help to run automated tests at unit level. Just wrap the container inside axe utility provided by the package to check for accessibility issues.

test('accessibility checks', async () => {
const { container } = render(<Form />);

expect(await axe(container)).toHaveNoViolations();
});

If the input does not have a label with it, the tests will throw an error.

What else could we do?

One important thing to note is that ~30% of access barriers are missed by the accessibility tools. One such example is colour contrasts. So, we need more than relying on automated tests only.

  • Test your interface with Assistive technologies like Voice Over in Mac etc.
  • Always test for keyboard access and manual accessibility testing.
  • Involved people with disabilities in accessibility testing.
  • To be always production-ready wrt accessibility, include accessibility checks in your automation pipelines. You can use the lighthouse score for accessibility as a check with every build

I would like to end this post by saying accessibility is a very important aspect of a successful product. Make sure we test for it in the early stages of the software development life cycle rather than keeping it for the end.

--

--