Real-world UIs often nest elements deeply, use repeated components, or contain dynamic lists. In these cases you need more than a single selector; you need to chain locators, filter them, and then act on the correct instance.
Locators, Chaining and Filters
Playwright uses the locator() API to represent a lazy, re-usable handle to elements. You can chain locators and use filters like hasText or has to narrow your target.
// locators-chains-filters.spec.ts
import { test, expect } from '@playwright/test';
test('filters a product list by category', async ({ page }) => {
await page.goto('https://demo.myshop.com/products');
const productList = page.getByTestId('product-list');
const laptopCard = productList
.getByRole('article')
.filter({ hasText: 'Laptop Pro 15' });
await expect(laptopCard.getByRole('heading')).toHaveText('Laptop Pro 15');
await laptopCard.getByRole('button', { name: 'Add to cart' }).click();
});
filter with hasText or has instead of manually iterating over lists of elements in your test code.nth() unless the index is stable and meaningful; prefer filters that describe business meaning.Chaining also works well for scoped searches: start from a high-level container, then drill down to specific controls. This keeps selectors shorter and less coupled to global layout.
Common Mistakes
Mistake 1 β Resolving elements too early
This can cause flakiness.
β Wrong: Grabbing element handles via page.$() and storing them long-term.
β Correct: Use locators and let Playwright resolve them when actions or assertions run.
Mistake 2 β Using only global page selectors
This reduces clarity.
β Wrong: Writing long, global selectors that ignore logical containers.
β Correct: Scope locators to sections or components such as forms and lists.