IKivan-kozenko -aqa
Try yourself as a QA tester
All topics·Advanced·15 / 24

Other locators

When getByRole, getByLabel, getByText don't cut it — usually with legacy apps or unusual DOM structures — I reach for CSS pseudo-classes, XPath, layout-based selectors, or nth= indexing. These are escape hatches, not first choices. The most useful ones in practice: :has() to get a parent by child, :visible to filter hidden duplicates, and nth= to pick one from a list.

When I use these locators

The priority order: getByRole > getByLabel > getByText > getByTestId > everything in this file. I only reach for CSS/XPath when the preferred locators literally can't target the element — usually legacy apps without semantic HTML, or complex table/grid structures.

The two I use most often from this list: :has() to get a parent by child, and :visible to exclude hidden duplicates.

CSS locator — and Playwright's additions

Playwright's page.locator('css=...') accepts standard CSS selectors, but also adds pseudo-classes that don't exist in native CSS:

`:has-text()` — matches any element that contains the text somewhere inside (case-insensitive substring). Combine it with a tag or class to avoid matching the whole document. `:text()` — matches the smallest element containing the text. `:text-is()` — exact, case-sensitive text match. `:visible` — filters to only visible elements. Useful when there are hidden duplicate elements with the same selector.

ts
// :has-text() — знайти article що містить "Playwright" десь всередині
await page.locator('article:has-text("Playwright")').click()
// Неправильно — матчить весь <body> теж:
// await page.locator(':has-text("Playwright")').click()

// :visible — коли є і видима і прихована кнопки
// page.locator('button').click() — кине помилку: знайдено 2 збіги
await page.locator('button:visible').click()  // тільки видима

// :text() — найменший елемент з текстом "Home" у #nav-bar
await page.locator('#nav-bar :text("Home")').click()

// :text-is() — точний збіг, чутливий до регістру
await page.locator('#nav-bar :text-is("Home")').click()
// Не збігатиметься з "home" або "Homepage"

Getting a parent element by its child

This is the most practical use case I have for CSS pseudo-classes. I have a list of items, each with a label and a button. I want to click the button in the row that has label 'Acme Corp'. The preferred way is locator.filter({ has: child }):

The CSS :has() pseudo-class does the same thing directly:

If neither works, xpath=.. as a last resort goes up one DOM level. Avoid it in stable tests — DOM structure changes will break it.

ts
// Кращий спосіб: locator.filter({ has: child })
const row = page.getByRole('listitem').filter({
  has: page.getByText('Acme Corp')
})
await row.getByRole('button', { name: 'Edit' }).click()

// Через CSS :has()
await page.locator('li:has(:text("Acme Corp"))').getByRole('button', { name: 'Edit' }).click()

// Останній засіб: xpath=.. (підняться на рівень вгору)
const label = page.getByText('Acme Corp')
const parent = label.locator('xpath=..')

Layout-based locators

When an element has no unique attributes or text but is positioned relative to another element, I can use layout pseudo-classes. These work on pixel positions — not DOM structure.

I always combine layout pseudo-classes with a selector — using them alone often matches empty wrapper elements instead of the actual target.

ts
// Заповнити поле праворуч від мітки "Username"
await page.locator('input:right-of(:text("Username"))').fill('admin')

// Клікнути кнопку поруч із promo-карткою (в межах 50px)
await page.locator('button:near(.promo-card)').click()

// Вибрати найближчий radio до "Option 3"
await page.locator('[type=radio]:left-of(:text("Option 3"))').first().click()

// Всі layout псевдокласи:
// :right-of()  :left-of()  :above()  :below()  :near()
// Всі підтримують опціональну максимальну відстань у px:
await page.locator('button:near(:text("Save"), 120)').click()

nth= indexing and XPath

nth= picks a specific element by zero-based index from a locator result. Useful when there are multiple identical elements and I want a specific one by position.

XPath locators are the last resort. They work, but they're fragile — any DOM restructuring breaks them. I use XPath only when CSS approaches have failed.

ts
// nth= — нульовий індекс
await page.locator('button').locator('nth=0').click()   // перша кнопка
await page.locator('button').locator('nth=-1').click()  // остання кнопка

// Чекати поки з'явиться третя кнопка
await page.locator(':nth-match(:text("Buy"), 3)').waitFor()

// XPath
await page.locator('xpath=//button[@data-action="save"]').click()

// XPath об'єднання (| для "або")
await page.locator('xpath=//span[@class="spinner"]|//div[@id="confirmation"]').waitFor()