Accessibility from test-driven development

As well as the more well-known benefits of Test-Driven Development (TDD) such as modular designs and high test coverage, I can add one more which I experienced unexpectedly. In this post, I will share how using a TDD approach led me to a user interface (UI) well aligned with accessibility best practices.

The simple web app I’m building is a media player which plays video and music files served by my home server. I realise there are off-the-shelf products and open-source projects that would save me the trouble, but that wouldn’t be as fun, would it?

The UI will have a main panel containing a heading and <video> player, and a sidebar listing the available media files. Like most web developers, I tend to overuse <div> elements. I was planning to use a div for the main panel and another div for the sidebar, adding CSS with Tailwind to make them look right.

I’m building my UI with React, using React Testing Library with Vitest for unit testing. I’ve been enjoying using Vitest because after its first run, it continues running at idle, listening for changes in my source code. Once it detects that I saved a change, it runs the tests again for immediate feedback.

While building out the skeleton of the UI, I used the TDD workflow of writing a test, seeing it fail, then writing the code for the test to pass. I immediately had a problem: how can I access the DOM elements in my tests?

Testing Library (which React Testing Library extends) provides many ways of accessing DOM elements, but in a strict order of priority. The best-practice way to access a DOM element is with getByRole. If that isn’t possible, there is a list of less desirable alternatives, ending with the worst option, getByTestId.

I was keen to use best practices in my project, so I began by trying to make everything work with getByRole.

The test for the heading was easy.

it('renders the title', () => {
  render(<App />);
  expect(
    screen.getByRole('heading', { name: 'My App' }),
  ).toBeInTheDocument();
});

After making that test pass with a <h1> element, I turned to the sidebar. This presented a real problem because a div has no role. Looking through the list of alternative DOM query methods, I didn’t see anything I could safely use. I didn’t want the sidebar to have a title, and its contents were to be populated with whatever was returned by the backend. There was no particular text or label that would always be there. I would have to resort to the dreaded getByTestId.

However, looking through the list of possible ARIA roles, I found one that described my div precisely: the navigation role. In the documentation for this role, it said:

It is preferable to use the HTML5 <nav> element to define a navigation landmark.

Now it was easy. My test was

it('renders the sidebar', () => {
  render(<App />);
  expect(screen.getByRole('navigation')).toBeInTheDocument();
});

and in the React component, I rendered a <nav> element instead of a div. Now I not only had a test for the sidebar element which used best practices – I also had an accessible sidebar in semantic HTML!

Looking through the other ARIA roles and HTML elements, I found the main element and its corresponding role. I realised I could use this to improve the accessibility of my main panel. I added a test,

it('renders the main panel', () => {
  render(<App />);
  expect(screen.getByRole('main')).toBeInTheDocument();
});

changed the main panel’s <div> tag to a <main> tag, and the test passed!

Next I needed to create the tree view inside the sidebar showing the available media files. There is a <menu> HTML element which is functionally the same as a list but is intended for interactive items. One of the permitted roles for this element is tree, which describes my use case exactly.

The following test asserts that an element with a role of tree is in the DOM, and that it is a <menu> element:

it('renders the menu tree', () => {
  render(<App />);
  expect(screen.getByRole('tree').nodeName).toBe('MENU');
});

To make the test pass, in my React component I rendered a <menu role='tree'>.

Summary

Thanks to my TDD approach, and the emphasis which Testing Library places on accessibility, I went from an inaccessible DOM made of divs to a fully accessible DOM skeleton written in semantic HTML.

Leave a Reply

Your email address will not be published. Required fields are marked *