Skip to Content
Write Accessible Unit Tests with RTL

Write Accessible Unit Tests with RTL

If you’re looking to learn how to quickly, and easily write unit tests in React Testing Library (RTL) that test and maintain accessibility standards in your code base, then stop what you’re doing and read this! This post will walk you through just how to write accessible unit tests with RTL.

One of the React Testing Library’s guiding principles is Testing for Accessibility. “[RTL] should enable you to test your app the way your users use it, including through accessibility interfaces like screen readers.”

Testing with getByRole:

Why we use getByRole

Roles tell screen readers what an element is for. In other words, “ARIA roles provide semantic meaning to content, allowing screen readers and other tools to present and support interaction with the object in a way that is consistent with user expectations of that type of object.” Learn more about aria roles from MDN here.

Remember that semantic HTML elements such as <button> and <header> tags, already have an implicit aria role. I highly recommend using semantic HTML whenever possible. When not possible, be sure to research what roles should belong on what components you’re using.

A component’s role describes to screen readers what the point of the component is. For instance, a div with the role “heading” accompanied by an aria-level=”1″ attribute is equivalent to an <h1> tag. This heading level one role alerts to the screen reader the most important heading in the document. Similarly, a button role tells the screen reader what the component is to the user.

Love button accessibility as much as I do? Interested in working with Material UI? Check out how to build accessible toggle buttons with Material UI here.

Creating components that use appropriate roles allows us to ensure that screen reader and other assistive tech users are able to interact with our components as we’d expect. Creating unit tests from this basis of accessible creation allows us to maintain this functionality and prevents us from going backwards. If you want to find out any HTML element’s intrinsic role, check out this awesome table from W3C.

We want to use getByRole to test for a components meaning. Is it a button? A landmark? Or is it an alert notice? If we test by what it is, we ensure consistent user experience for assistive tech users.

How to use getByRole effectively

getByRole, and getAllByRole can grab an element by its intrinsic (semantic HTML) or ARIA role (role=””). When we write accessible unit tests with RTL, it’s important to utilize these built in accessibility features.

Want an easy getAllByRole example? Check out our post on the subject here.

By using getByRole you can locate bugs in how your components will be ‘seen’ and read out by screen readers. Check out more about byRole here.

An example of a unit test that would fail:

// The React component
const = Button = () => {
return <div>I'm a button, click me</div>
}

// Testing the component

test("that my button will render", () => {
  render(<Button />)
  const button = screen.getByRole('button', { name: 'I\\'m a button, click me' }) //this will fail

})

Here are two ways to write your button component that would allow it to pass:

// The React component
const = Button = () => {
return <div role="button">I'm a button, click me</div>
}
// The React component
const = Button = () => {
return <button>I'm a button, click me</button>
}

If multiple components with the same role exist within a document but have different button text, use the name property to grab the accessible label of the component:

expect(screen.getByRole('button', { name: 'Save' })

Let’s say you want to test a buttons aria attribute state.

Here is a button that when clicked sets the aria-pressed attribute to true.

// The React component
const = Button = () => {
const [pressed, setPressed] = useState(false)
const handleClick = () => {
setPressed=(true)
} 
return <button onClick={handleClick} aria-pressed={pressed}>I'm a button, click me</button>
}

I want to test that the button is pressed when the user clicks the button:

test("that my button will be pressed when clicked", () => {
  render(<Button />)
  const button = screen.getByRole('button', { name: 'I\\'m a button, click me', pressed: false })
	fireEvent.click(button)
	// this tests the state change of the aria-pressed attribute
	expect(screen.getByRole('button', { name: 'I\\'m a button, click me', pressed: true }))

})

We can also test out Aria-Landmarks using the getByRole function from RTL

Aria landmarks are used by screen readers to allow users to skip to the chunks of content that are important to them. Use Aria Landmark roles in all of your applications. Providing users the ability to quickly glean the content of a web app, or web page is essential to pass WCAG 2.0 Level A conformance. Bypass blocks, WCAG 2.4.1, is the rule you should familiarize yourself with when it comes to landmark roles.

The following code creates a region landmark and then we use getByRole to test that this region exists.

// The React region
const = Region = () => {
return <div role="region" aria-labelledby="region1">
<h2 id="region1">I'm a region</h2>
I'm a portion of a web page 
that covers the main concepts in a news article</div>
}

// Testing the component

test("that my button will render", () => {
  render(<Region />)
  const region = screen.getByRole('region') //this will pass

})

An aria region must use a label , so to cover the accessibility needs of the component, use the name attribute to also test that a accessible label exists. Note that in the component code above we have added an id to the heading and linked that id to the region using aria-labelledby.

test("that my button will render", () => {
  render(<Region />)
  const region = screen.getByRole('region', { name: 'I\\'m a region' }) //this will pass

})

Testing the accessibility of error notices

Fun fact, you can test whether your error notices are accessible to screen readers by testing the role=alert attribute:

-me

In the code below, we’re conditionally rendering a component with the aria role alert. This component does not need to be present when the screen initially renders for the assistive tech to recognize it when it does render because of its aria role.

// The App Component

function App() {
  const [pressed, setPressed] = useState(false);
  const [buttonText, setButtonText] = useState("Hello");
  const [alert, setAlert] = useState("");
  const onButtonClick = () => {
    setPressed(true);
    setButtonText("goodbye");
    setAlert("the button was clicked!");
  };
  return (
    <div className='App'>
      <main>
          <button aria-pressed={pressed} onClick={onButtonClick}>
            {buttonText}
          </button>
	// the alert conditionally renders:
          {alert !== "" && <div role='alert'>{alert}</div>}
      </main>
    </div>
  );
}

// Testing the app component

test("checks the aria-pressed and role=alert functionality", () => {
  render(<App />);
  const button = screen.getByRole("button", { pressed: false });
  fireEvent.click(button);
  const buttonPressed = screen.getByRole("button", { pressed: true });
  const newButtonText = screen.getByText("goodbye");
  const roleAlert = screen.getByRole("alert");
  expect(buttonPressed).toBeInTheDocument();
  expect(newButtonText).toBeInTheDocument();
  expect(roleAlert).toBeInTheDocument(); // this passes
});

The same is not necessarily true for aria-live components. Some assistive tech may not alert users of state changes to the aria-live region if it doesn’t exist on initial render.

To test an aria-live region, you’ll note that the aria role of an aria-live="polite" component is role="status".

Testing Aria Live Polite regions

In the code below, we’re rendering the aria live region on the initial render, and inserting text when a button is clicked. Below that we have the test written to pass:

const Live = () => {
const [live, setLive] = useState('')

return (
<>
<button onClick={() => setLive('An update has occurred')}>Click to hear update</button>
<div aria-live="polite">{live}</div>
)
}

//Test that the aria live content updates on event fire
test("checks that the aria live region exists on load and updates", () => {
  render(<Live />);
  const roleStatus = screen.getByRole("status");
  expect(roleStatus).toBeInTheDocument(); // this passes
  const button = screen.getByRole("button", { name: 'Click to hear update' });
  fireEvent.click(button);
  const updatedText = screen.getByText('An update has occurred')
  expect(updatedText).toBeInTheDocument();

});

Testing with getByLabelText:

We can test whether our form labels are actually passing Accessibility checks by using ByLabelText. Here is an example of a failing test. The label for attribute doesn’t match the id, therefore the test will fail.

// A label component

const Grid = () => {
return (
<div className={grid}>
<label for="footer_copyright">Footer copyright text</label> 
//notice the for attribute
<div>some description of the setting</div>
</div>
)}

// A text input
const TextInput = () => {
return <input id="footer-copyright" type="text" />
}

const App = () => {
return (
<>
<Grid />
<TextInput />
</>
)
}

// Test that the label isn't orphaned

test("that the label is not orphaned", () => {
  render(<App />)
  const label = screen.getByLabelText('Footer copyright text') //this will fail

})

To fix the above code, we’d simply need to change the for attribute of the label from footer_copyright to footer-copyright

Conclusion

Now you’ve seen how to use getByRole to effectively test buttons, regions, role labels, and notices. You’ve also seen how to test form label’s with getByLabelText. Hopefully you’ve also learned more about accessibility and how to improve your unit tests to maintain the usability of your components. Thanks for stopping by!

Photo by Jakub Pabis on Unsplash