React Testing Guide
Use Roles First
Most semantic HTML elements have an implicit role. This role is used by accessibility tools such as screen readers. But as we’ll explain, you can also use it to write easy-to-understand tests.
Because the ARIA specs are more recent than HTML 1.0, it clarifies HTML and provides improved names for a lot of the traditional HTML elements. The names used are more what everyday users would use too, such as link
rather than a
.
Roles are better than Tag Names
Roles are better than tag names, as they generalize towards the user behaviour not the nitty-gritty of HTML specifics.
As an example, <input type="text">
and <textarea></textarea>
both have the role textbox. A user probably does not care for the difference — it’s just a text field that they can type into.
These roles allow us to think in the language that our users would use:
- The save button
- The email text field
- The primary navigation
- The notifications icon image
- The search field
- The remember me checkbox
Roles are better than Test IDs
Test IDs are something that allow us to find specific elements on the page. They can seem especially necessary in a component system, since our component structure is not surfaced if we render our component tree to HTML.
The problem is that Test IDs are only used for automated testing — they don’t affect the user’s experience at all.
So something that relies on Test IDs to work might still pass, but still produce an issue for end users! So using them does not give us that much more confidence that what we are implementing actually works.
It would be better if we could test the same experience as our users. If we could take the same path they use when first coming to a page and they begin to understand what’s there.
A screen reader user has this exact experience. They arrive at a page, and are able to hear what sections and elements are available. They are able to jump to a specific section or element and interact with it. And they get feedback telling them exactly what the state of the world is as they interact.
This sounds exactly what we would desire for our automated tests! We want to find elements on the page, interact with them, and get feedback that they are working correctly.
This is what accessibility-first testing allows us to achieve. And as a massive bonus, it lets us create an accessible experience from day one. We can be on a path to creating a fantastic user experience too.
Component test boilerplate
import SignInForm from "./SignInForm";
import React from "react";
import { lazy, freshFn } from "jest-zest";
import { render } from "@testing-library/react";
import user from "@testing-library/user-event";
const onSignIn = freshFn();
const { getByRole } = lazy(() => render(
<SignInForm onSignIn={onSignIn} />
));
it("renders an email textbox", () => {
expect(getByRole('textbox', { name: /Email/i })).toBeInTheDocument();
});
describe("when Sign In button is clicked", () => {
beforeEach(() => {
user.click(getByRole('button', { name: /Sign In/i }));
});
it("calls onSignIn prop", () => {
expect(onSignIn).toHaveBeenCalled();
});
})
Available Roles
Role name | HTML element |
---|---|
link | <a href=…> |
none | <a> |
button | <button> |
button | <input type=button> |
textbox | <textarea> |
textbox | <input type=text> |
radio | <input type=radio> |
heading | <h1> |
heading | <h2> |
heading | <h3> |
document | <body> |
Role name | HTML element |
---|---|
main | <main> |
navigation | <nav> |
banner | <header role=banner> |
contentinfo | <footer role=contentinfo> |
search | <form role=search> |
form | <form> |
complementary | <aside> |
region | <section> |
Role name | HTML element |
---|---|
link | <a href=…> |
none | <a> |
heading | <h1> , <h2> , <h3> , etc |
list | <ul> , <ol> |
listitem | <li> |
term | <dt> |
definition | <dd> |
img | <img alt="Some description"> |
none | <img alt=""> |
figure | <figure> |
separator | <hr> , <li role=separator> |
none | <p> |
none | <div> |
none | <span> |
group | <details> |
button | <summary> |
Role name | HTML element |
---|---|
form | <form> |
group | <fieldset> |
search | <form role=search> |
button | <button> |
button | <input type=button> |
button | <button type=submit> , <input type=submit> |
textbox | <textarea> |
textbox | <input type=text> |
textbox | <input type=email> |
textbox | <input type=tel> |
textbox | <input type=url> |
searchbox | <input type=search> without list attribute |
radiogroup | <fieldset role=radiogroup> |
radio | <input type=radio> |
checkbox | <input type=checkbox> |
combobox | <select> without multiple attribute |
listbox | <select> with multiple attribute |
option | <option> |
slider | <input type=range> |
none | <input type=password> |
progressbar | <progress> |
status | <output> |
Role name | HTML element |
---|---|
table | <table> |
rowgroup | <tbody> , <thead> , <tfoot> |
rowheader | <th> |
columnheader | <th> |
row | <tr> |
cell | <td> |
Role name | HTML element |
---|---|
tablist | <ul role=tablist> |
tab | <button role=tab> |
tabpanel | <section role=tabpanel> |
Should manage focus with JavaScript. |
Role name | HTML element |
---|---|
menu | <ul role=menu> |
menuitem | <button role=menuitem> |
menuitemcheckbox | <button role=menuitemcheckbox> |
menuitemradio | <button role=menuitemradio> |
menubar | <nav role=menubar> |
Should manage focus with JavaScript. |
Accessible names
Accessible elements don’t just have a role. They can have a ‘name’ too, which helps the user tell elements with the same role apart.
These names are provided by HTML in a number of ways:
-
<label>
relationship -
aria-labelledby
attribute -
aria-label
attribute - The displayed value
- The text content
The algorithm is specified in W3C’s Accessible Name and Description Computation.
Examples of accessible names
<button>Save</button>
<label>Email: <input type=email></label>
<label><input type=checkbox> Receive email alerts</label>
<fieldset>
<legend>Alert settings</legend>
<label><input type=checkbox> Receive push notifications</label>
<label><input type=checkbox> Receive email alerts</label>
<label><input type=checkbox> Receive text messages</label>
</fieldset>
<article aria-labelledby="faq-heading">
<h2 id="faq-heading">Frequently Asked Questions</h2>
</article>
<nav aria-label="Primary">
…
</nav>
<svg role="img">
<title>New document</title>
…
</svg>
You could query these elements using Testing Library:
getByRole('button', { name: 'Save' });
getByRole('textbox', { name: /Email/ });
getByRole('checkbox', { name: /Receive email alerts/i });
getByRole('fieldset', { name: /Alert settings/i });
getByRole('article', { name: /Frequently asked questions/i });
getByRole('navigation', { name: 'Primary' });
getByRole('img', { name: 'New document' });
Accessibility-first testing: a standards-based approach
- Build components
- Test components work as expected
- Test-drive components
- Learnable & deterministic
Test components work as expected
Roles > Tag Names
Roles > Test IDs
- Test IDs are fragile. They are not part of behaviour.
- Easier to write tests first.
- Reduce coupling to a certain implementation.
- Can swap out third-party components.
- Good accessibility from day one.
Use accessible names
Role name | Responsibility | HTML example |
---|---|---|
Button | Perform action here | <button> |
Checkbox | Enable something | <input type=checkbox> |
Textbox | Type in something | <input type=text> or <textarea> |
Radio & Radiogroup | Choose from a list | <input type=radio> |
Combobox | Choose or type from a list | <select> or <div role=combobox> <input> |
Slider | Choose from a range | <input type=range aria-valuemin=1 …> |
Menu & Menuitem | Choose action | <ul role=menu> <li role=menuitem>… |
Dialog | Focus on this separate content | <div role=dialog> |
Alert | Alert to live information, errors | <div role=alert> |
Reduce coupling to specific implementations
- Reach UI or React Modal?
- React Select or Downshift?
- Emotion or CSS Modules?
- React or Vue?
- HTML and ARIA are stable, consistent specifications
- Third-party libraries are unstable, discrepant implementations