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
| Locator | Finds |
|---|---|
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 |
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.
exactis true by default: the whole string, case and all.name: 'Save'never matches "Save draft" or "save".exact: falsematches any part, in any case.name: 'save', exact: falsematches "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 noexact.
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: hiddenandinert. getByTextreads the text in the page and skipsscript,style,templateandnoscript. It finds hidden elements too, and reports them as not visible.- Chrome names a table row or a list item only from
aria-labeloraria-labelledby. Find a row by its place, as ingetByRole('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.
page.getByTestId('inbox').getByRole('button', { name: 'Delete' })page.locator('li').getByRole('button')page.getByRole('listitem').nth(1).getByRole('button')first(),last()andnth(index)keep one match, by its place in the document.nthcounts from 0, and from the end when negative, sonth(-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 astext=Saveordiv >> 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
querySelectorAlldoes.
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.