Locators

Find elements by test id, role, label, text, placeholder or CSS, and narrow a match down.

A locator is a recipe, not an element. Retest finds the element again for every action and every check. A page that redraws itself does not leave a test holding a stale element.

Find an element Link to Find an element

LocatorFinds
getByTestId(id)Elements whose data-testid equals id exactly
getByRole(role, { name, exact })Elements with this ARIA role and, when given, this accessible name
getByLabel(text, { exact })Form controls whose accessible name matches: text boxes, search boxes, combo boxes, list boxes, checkboxes, radios, switches, sliders and spin buttons
getByText(text, { exact })The innermost elements whose text matches
getByPlaceholder(text, { exact })Elements whose placeholder attribute matches
locator(selector)Elements a CSS selector matches, as querySelectorAll matches it
TypeScript
await page.getByTestId('task-title').fill('Release checklist')await page.getByRole('button', { name: 'Save' }).click()await page.getByLabel('Password').fill(secret('password'))await expect(page.getByText('Signed in as alice')).toBeVisible()

How text matches Link to How text matches

  • Both sides are trimmed, and each run of spaces or line breaks reads as one space.
  • exact is true by default: the whole string, case and all. name: 'Save' never matches "Save draft" or "save".
  • exact: false matches any part, in any case. name: 'save', exact: false matches "Save", "save" and "Save draft".
  • A regular expression is searched for anywhere in the text, with its own flags, as in getByText(/^Saved \d+ tasks$/). It takes no exact.

Where names come from Link to Where names come from

getByRole and getByLabel read the accessibility tree the browser computes. A name comes from aria-label, aria-labelledby, a <label>, a title, a placeholder or the content, as the browser decides.

  • Elements the browser leaves out of that tree are not found: aria-hidden, display: none, hidden, visibility: hidden and inert.
  • getByText reads the text in the page and skips script, style, template and noscript. It finds hidden elements too, and reports them as not visible.
  • Chrome names a table row or a list item only from aria-label or aria-labelledby. Find a row by its place, as in getByRole('row').nth(1), or by a test id.

Narrow a match Link to Narrow a match

Every finder is also a method of a locator. It looks inside the elements that locator found, never at those elements themselves.

TypeScript
page.getByTestId('inbox').getByRole('button', { name: 'Delete' })page.locator('li').getByRole('button')page.getByRole('listitem').nth(1).getByRole('button')
  • first(), last() and nth(index) keep one match, by its place in the document. nth counts from 0, and from the end when negative, so nth(-1) is the last.
  • A locator chooses once: first().nth(1) is refused. Choose, then look inside.
  • An index past the matches keeps nothing, and an action waits as it does for no match.

One match per action Link to One match per action

An action needs exactly one element. When nothing matches, it waits until its time runs out, then fails not_found. When several match, it fails at once as ambiguous and acts on none of them.

A failure names the step that kept nothing, such as "getByRole('listitem') matched 4 elements, and nth(9) keeps none of them."

CSS Link to CSS

  • locator(selector) takes CSS only. XPath and Playwright's selector engines, such as text=Save or div >> span, are refused before anything is sent.
  • A selector the browser cannot read, such as one with :has-text(), fails at once with the browser's reason.
  • Inside a locator, a selector matches as that element's own querySelectorAll does.

Not supported yet Link to Not supported yet

Locators search the top-level document only. They do not look into shadow DOM or frames. There is no filter(), and(), or(), getByAltText() or getByTitle() yet, and no XPath.

Native apps take a smaller set of locators. Mobile apps lists them.