How to fix Playwright locator failures caused by hidden accessibility names?

Playwright

Playwright's role-based locators (getByRole, getByLabel) match elements using the computed accessible name, which may come from a visually hidden <span>, an aria-label attribute, an aria-labelledby reference, or a title attribute — not necessarily the visible text you see on screen. When the accessible name differs from the visible label, getByRole('button', { name: 'Save' }) may find nothing while the button is clearly visible, or it may match a different element entirely. This is the most common reason a selector that looks obviously correct returns no match.

Common mistake

test('saves settings', async ({ page }) => {
  await page.goto('https://app.example.com/settings');

  // The button visually shows "Save" but its aria-label is "Save account settings"
  await page.getByRole('button', { name: 'Save' }).click();
  // Returns no match — accessible name is longer than 'Save'
});

Also broken in reverse: a visible icon button with hidden text where the visible label is an icon, but the accessible name is the hidden span text:

<button>
  <svg>...</svg>
  <span class="sr-only">Delete project</span>
</button>
// This FAILS — there's no visible text "Delete project"
await page.getByText('Delete project').click();

// This WORKS — 'Delete project' is the accessible name from sr-only span
await page.getByRole('button', { name: 'Delete project' }).click();

The fix

Inspect the accessibility tree to find the computed accessible name, then match it exactly:

import { test, expect } from '@playwright/test';

test('saves account settings', async ({ page }) => {
  await page.goto('https://app.example.com/settings');

  // Check accessibility tree: button's aria-label is 'Save account settings'
  await page.getByRole('button', { name: 'Save account settings' }).click();

  await expect(page.getByRole('alert')).toContainText('Settings saved');
});

For partial matching when exact accessible name is long or dynamic:

// Use regex for partial accessible name matching
await page.getByRole('button', { name: /save/i }).click();

For icon buttons with visually hidden labels:

// The accessible name comes from aria-label or sr-only span, not visible icon
await page.getByRole('button', { name: 'Close dialog' }).click();
// Or by label relationship:
await page.getByLabel('Close dialog').click();

To debug the actual computed accessible name in a test:

test('debug accessible name', async ({ page }) => {
  await page.goto('https://app.example.com/settings');

  const buttons = page.getByRole('button');
  const count = await buttons.count();

  for (let i = 0; i < count; i++) {
    const name = await buttons.nth(i).getAttribute('aria-label')
      ?? await buttons.nth(i).textContent();
    console.log(`Button ${i}: "${name?.trim()}"`);
  }
});

Why it works

Playwright computes accessible names using the W3C accessible name computation algorithm, which considers aria-label, aria-labelledby, title, alt, associated <label> elements, and element text content (including visually hidden text) in priority order. This matches how assistive technologies identify elements, which is why role-based locators are more reliable than visual text matching. Using the accessibility tree inspector in browser DevTools (Accessibility panel) shows exactly what name the browser computes for any element, making it straightforward to determine what name value to pass to getByRole.

Tips

  • Open browser DevTools → Accessibility tree and inspect the element — the computed name shown there is exactly what Playwright's getByRole({ name }) matches against.
  • getByRole('button', { name: /text/i }) uses regex matching, which handles case differences and partial names — useful when the exact accessible name varies by context.
  • When you control the component code, prefer explicit aria-label over relying on computed names from child text — it makes both the accessibility and test code clearer.
  • getByLabel('Field name') matches inputs by their associated <label> element's text, not the input's own attributes — this often resolves mismatches in form locators.