skip to content

In Playwright, why can page.getByRole reach no element for a clickable div in an issue board's row menu?

level: seniorimportance: should knowfreq 44%

answer

  1. Some tags expose nothing at all
  2. Two separate failures, not one
  3. A role attribute alone is half a fix
  4. Some roles forbid an author-supplied name
  5. Count on role alone to confirm

basics

~20 s

A div exposes no ARIA role, so no role query can select it. Playwright derives roles from an explicit role attribute or an implicit tag mapping, and div, span and an anchor without href have none.

solid answer

~40 s

`getByRole` queries the accessibility view, and a bare `<div>` or `<span>` has no role there — neither an explicit `role` attribute nor an implicit tag mapping supplies one. Other elements are quietly roleless too: an `<a>` without `href`, `<img alt="">` which maps to `presentation`, and a `<section>` or `<form>` that has no accessible name. Adding a role is only half a fix, because the `name` option needs a computed accessible name, and for roles such as `generic`, `paragraph`, `strong` and `presentation` naming is prohibited, so an `aria-label` on them is ignored. The durable fix is in the component: use `<button>` or `<a href>`, or add `role` plus a real name via text, `aria-label` or `aria-labelledby`, and add `tabindex` and key handling. Falling back to another Playwright query builder is a stopgap, not the answer.

code

typescript · 12 lines
typescript
import { test, expect } from '@playwright/test';

test('row menu items must expose a role and a name', async ({ page }) => {
  await page.goto('/board');

  // <div class="menu-item" onclick="...">Delete issue</div> exposes no role.
  await expect(page.getByRole('menuitem')).toHaveCount(0);

  // After the component ships <button role="menuitem">Delete issue</button>:
  await page.getByRole('button', { name: 'Issue actions' }).click();
  await expect(page.getByRole('menuitem', { name: 'Delete issue' })).toBeVisible();
});

go deeper

for a junior

Remember that divs and spans have no role, so getByRole cannot see them. When a locator finds nothing, look at the tag before you doubt the role name you passed.

for a middle

Explain both failure modes: an element with no role at all, and an element with a role but no computable accessible name, including the roles for which author naming is prohibited.

for a senior

Diagnose with a role-only count first, then decide between fixing the component and taking a recorded stopgap. Treat a locator you cannot write as evidence about the product, not the suite.

for a principal

Own the accessibility contract that makes these locators possible at all. Decide where role and name are enforced, in review or in a component test, so the gap is caught before it reaches an end-to-end suite.

`page.getByRole` can only see what the accessibility view exposes. An issue board's row menu built from `<div class="menu-item" onclick="...">` exposes nothing: Playwright derives a role from an explicit `role` attribute or from an implicit tag mapping, and **`<div>` and `<span>` have neither**. There is no role string that reaches them, so this is not a matter of choosing a better role name. ## Which elements have no role - `<div>` and `<span>` — no implicit role at all. - `<a>` **without** `href` — a link only becomes `link` when it is navigable. - `<img alt="">` — deliberately mapped to `presentation`, i.e. decorative. - `<input type="hidden">` — no role. - `<section>` and `<form>` without an accessible name — no `region` / `form` role until they are named. - Anything marked `role="none"` or `role="presentation"`, unless a conflict-resolution rule restores the implicit role. ## The second failure: no accessible name Adding a role is not always enough, because the `name` option matches the computed accessible name and some elements cannot have one from the author. For roles such as `generic`, `paragraph`, `strong`, `emphasis`, `code`, `presentation`, `time` and `term`, **naming is prohibited**: an `aria-label` on them is ignored and the computed name is empty. So `<div aria-label="Delete">` gains nothing — the div has no role to hang the label on, and even a role that permits no name would drop it. Elements that legitimately have a role can still end up nameless: - An icon-only button with no text, no `aria-label` and no `title`. - An `<input>` whose visible label is a nearby `<div>` never associated with it. - A `<button>` whose only child is an `<img alt="">`, which contributes nothing. For those, `page.getByRole('button')` matches, but `{ name: ... }` cannot narrow it, and the page is equally unusable for a screen-reader user. ## Fixing it at the source The cheapest fix is almost always in the component, not the test: 1. Use the semantic element — `<button type="button">` for a click target, `<a href>` for navigation. The role, keyboard behaviour and focus handling come with it. 2. If the markup genuinely cannot change tag, add both halves: `role="button"` **and** a name, via visible text, `aria-label`, or `aria-labelledby` pointing at the visible label. 3. Give icon-only controls an `aria-label` that repeats the tooltip text, so the name is stable and human-readable. 4. Add `tabindex="0"` and key handling alongside `role="button"`; a role attribute alone is a lie to assistive technology, and a test that passes against it is asserting something a user cannot do. ## What to do when the markup cannot change today | Situation | Practical move | |---|---| | Component is yours, ships this sprint | fix the markup; the a11y bug is real | | Third-party widget, no role | locate with a different Playwright query builder | | Role present, name empty | scope to a named container and query inside it | | Name exists but is wrong | fix the label; the announced name is user-facing | Falling back to another Playwright locator — a test id, a text lookup, a raw selector — is a legitimate stopgap, but record why. A `getByRole` call that cannot be written is usually a signal about the product, not about the test. ## Verifying the diagnosis quickly The check that settles it is a count, not an inspection: query the role alone and see whether anything matches at all. ```ts // Does the tracker expose ANY button here? await expect(page.getByRole('button')).toHaveCount(3); // fails: the row menu items expose no role // After the component is fixed: await page.getByRole('button', { name: 'Issue actions' }).click(); await page.getByRole('menuitem', { name: 'Delete issue' }).click(); ``` If the role-only query already returns zero, no name, `exact` value or state option will rescue it — the element is simply absent from the accessibility view, and that is the bug to file.

  • Does adding aria-label to a div make it findable by getByRole?
    No. The div still has no role, so no role query reaches it, and for a handful of roles including generic and paragraph an author-supplied name is prohibited and ignored outright. A name without a role is unusable; you need the role first, then a name that the role is allowed to carry.
  • What do you do when the roleless widget comes from a third-party library?
    Use a different Playwright query builder as a stopgap — a test id or a text lookup — and record why in the test or the backlog. Then push the gap upstream or wrap the widget in your own component, because the same missing role also makes the control unusable with a screen reader.
  • Is role="button" on a div enough to make a test meaningful?
    It makes the element findable and no more. Without `tabindex="0"` and Enter or Space handling the control is not keyboard-operable, so a passing click test asserts behaviour a keyboard user cannot reproduce. Treat the role attribute as a claim the component must actually honour.

saying these in an interview costs you the question

  • Thinks every visible element has some ARIA role
  • Believes aria-label alone makes a div findable by role
  • Says an anchor without href still has the link role
  • Treats role="button" as making a div keyboard-operable
  • Blames the test framework rather than the missing semantics