Skip to content

Build1 publisher3 min readPublished Updated

Role-based locators tie test reliability to your button's accessible name

A dev.to post argues the DOM is an interface for screen readers, crawlers, extensions and browser agents. The load-bearing consequence is narrower: a Playwright query by role and name makes your visible label the contract.

The Engineer · Build desk

Illustration accompanying Role-based locators tie test reliability to your button's accessible name

What happened

  • A dev.to post argues that the DOM is effectively an interface for screen readers, browser automation, end-to-end tests, search crawlers, extensions, monitoring tools and AI browser agents alike.
  • Its central example is a div with a click handler styled to look exactly like a button, which a machine inspecting the DOM still reads as a generic container with a click handler.
  • For forms it separates the four jobs placeholder text is doing, binding an explicit label to an input that carries id, name, type, autocomplete and required.
  • It contrasts a Playwright query by role and accessible name with a positional CSS selector, arguing that semantic markup produces tests closer to how users understand a page.

Compiled by The EngineerSomething wrong?How this is made

Why it matters

  • constraint Once a locator resolves on an accessible name, product copy is inside the test contract, and the markup offers no deprecation window on a renamed label.
  • exposure A control made unavailable only by opacity and pointer-events still presents itself to anything reading the DOM as a control with no disabled state at all.
  • capability Naming controls carefully pays more than one consumer, since the string a screen reader announces is the same handle a role-based locator grabs.

A role-based locator has exactly two inputs. It needs a role, and it needs an accessible name. In the post's own example, `page.getByRole('link', { name: 'Explore integrations' })` matches the link role and the string a screen reader would read out [12]. The CSS alternative it is set against, `page.locator('.card:nth-child(3) .footer a')`, couples to four separate facts about the markup: a class name, a sibling position, a descendant class, and an element type [12][13]. Two couplings survive more refactors than four.

That trade moves the fragile part into the copy. Rename the link to "Integrations" and the role query stops resolving, and there is no deprecation window on an accessible name [14]. Product owns that string. The suite, and anything else driving the page by role, now depends on it.

The state patterns are where the gap gets concrete. The post's `.disabled` class sets `opacity: 0.5` and `pointer-events: none`, so the unavailability exists only in the stylesheet [9]. A human reads a faded control. Anything reading the DOM reads a control with no disabled state on it. The recommendation is to put the state on the node: the `disabled` attribute or `aria-disabled="true"`, with `aria-pressed` for selection and `aria-expanded` plus `aria-controls` for a panel that is `hidden` [9][10]. CSS then describes the state instead of being the state.

The button case is the same argument with less ARIA. The post's own CSS is padding, background, colour and `cursor: pointer` [4], and `cursor: pointer` is the entire extent of the button-ness a `div` inherits for free. A real `<button>` carries keyboard focus and button semantics, and the post says automation frameworks can identify it more reliably [5].

What is not in the post is a measurement. The support for the reliability claim is the claim plus code examples [16]. Two things have to be true for it to transfer to your codebase. Your tests have to address controls by role and accessible name rather than by CSS path, because a suite written entirely in class selectors gains nothing from `aria-pressed`. And the agent you care about has to read the accessibility tree rather than pixels. Fail both and semantic markup still buys you keyboard focus and screen reader behaviour [5], but not test stability.

Adoption is priced per control. The React change is `div` to `button` at each call site [6]. Each state becomes an attribute at each control [9][10]. Each form field needs a label bound by `id`, plus `autocomplete` and `required`, and an error node wired through `aria-describedby` and `aria-invalid` [7][8]. There is no framework flag; the bill scales with how many controls you already shipped [15].

So the honest version of "your HTML is an API" is narrower than seven consumers reading the DOM [2][3]. The public part is what a locator can address: role, accessible name, and state attributes. Treat those as published, and the rest of the markup stays yours to move around.

What to watch

  • Whether browser agent vendors document reading the accessibility tree rather than pixels, which decides if ARIA state attributes pay off for agents at all.
  • Whether teams start pinning accessible names in shared test fixtures once a copy change breaks a role-based suite.
  • The rest of the post's seven patterns: the supplied text breaks off inside the fifth, on dynamic content announcing its state.
Loading claim ledger
Loading source directory links
Loading share composer
Loading topic controls
Loading related stories