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>
Landmarks
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>
Content
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>
Forms
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>
Tables
Role name HTML element
table<table>
rowgroup<tbody>, <thead>, <tfoot>
rowheader<th>
columnheader<th>
row<tr>
cell<td>
Tabs
Role name HTML element
tablist<ul role=tablist>
tab<button role=tab>
tabpanel<section role=tabpanel>
Should manage focus with JavaScript.
Menus
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

A list of roles from the wai-aria spec.

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
ButtonPerform action here<button>
CheckboxEnable something<input type=checkbox>
TextboxType in something<input type=text> or <textarea>
Radio & RadiogroupChoose from a list<input type=radio>
ComboboxChoose or type from a list<select> or <div role=combobox> <input>
SliderChoose from a range<input type=range aria-valuemin=1 …>
Menu & MenuitemChoose action<ul role=menu> <li role=menuitem>…
DialogFocus on this separate content<div role=dialog>
AlertAlert 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

Specs