On writing testable markup


With the 2025 European Accessibility Act sharpening minds and sparking the formation of working groups in companies across the continent, developers need to find ways to make sure their applications are accessible.

While I would never claim to be an expert in accessibility, I’ve written a lot of unit, integration and end-to-end-ish front-end tests over the last few years, and I think the way we write tests can directly influence the baseline of accessibility in our applications

While the tooling has changed (in my case, from Jest to Vitest, Enzyme to Testing Library and Cypress to Playwright), I think it’s brought more clarity in the right way and the wrong way of writing tests. And yes, I spent years doing it the wrong way, but after starting a greenfield project last year, I’m firmly committed to writing better, more accessible tests.

So what’s the difference between this element:

Seals spotted in Milton Keynes

and this one:

False alarm - it was a stray bin-bag

Did the second heading leave you disappointed? Well, it should have. It’s not just the lack of adorable seals in my hometown of Milton Keynes, but writing tests for it leaves a lot to be desired.

Let’s look at the markup for the second one:

// look, it's a div element for some reason
<div class="slate-500 m-8 text-2xl font-bold leading-5">
  False alarm - it was a stray bin-bag
</div>

I see this often in React applications and it makes no sense. The developer has contracted a terrible case of div-itis. They’ve consumed too much div-soup. Everything is a div now. It’s like HTML5 never happened. And when everything is a div, testing becomes so much more difficult.

When testing-library came out 5 or so years ago, it blessed us with data-testid, and things were never the same. We added it to everything and it was warmly embraced by Cypress and their data-cy attributes, and they still recommend using it to this day. A special testing attribute is ‘stable’ and resilient to changing your application, but if we really think about it, it makes no sense - users cannot see or interact with HTML attributes, so why should our tests?

How might a developer test that component? Well, they could just slap a data-testid on it like this:

it("is being tested with data attributes", () => {
  render(
    <div
      class="slate-500 m-8 text-2xl font-bold leading-5"
      data-testid="find-me"
    >
      False alarm - it was a stray bin-bag
    </div>,
  );

  const title = screen.getByTestId("find-me");
  expect(title).toBeVisible();
});

Thankfully, Testing Library became more opinionated about finding elements by their role and Playwright has followed suit. Identifying elements via their accessibility roles more accurately reflects how users will use your application. If your markup doesn’t let you access what you want to test via accessibility roles, maybe your markup is wrong?

Let’s go back to the first heading about seals, here is the markup:

<h2 class="slate-500  m-8 text-2xl font-bold leading-5">
  Seals spotted in Milton Keynes
</h2>

This time it is an h2 element. We can write a test for this like so:

it("is being tested with data attributes", () => {
  render(
    <h2 class="slate-500  m-8 text-2xl font-bold leading-5">
      Seals spotted in Milton Keynes
    </h2>,
  );

  const title = screen.getByRole("heading");
  // or screen.getByRole("heading", {name: 'Seals spotted in Milton Keynes'})
  expect(title).toBeVisible();
});

If we open our devtools and navigate to the Accessibility tab, we can identify in the accessibility tree that it has a role attribute of ‘heading’ and a name attribute of ‘Seals spotted in Milton Keynes’. By using getByRole, we are forced to write markup that has a role we can identify it by.

So what should you take from this? Next time you want to test a component that you’ve written, find it in the accessibility tree and see if you can find it by its role for testing. If you can’t, maybe re-examine your markup?

Further reading