Advanced Locators
Real-world applications have complex UIs: tables with hundreds of rows, nested components, dynamic lists, iframes, and shadow DOM. Playwright's locator API provides composition tools — filtering, chaining, and scoping — that let you target precise elements without fragile selectors.
Filtering Locators
filter() narrows a locator by additional criteria — text content, child elements, or visibility.
// Find the row that contains "John Doe" and click its edit button
await page.getByRole('row')
.filter({ hasText: 'John Doe' })
.getByRole('button', { name: 'Edit' })
.click();
// Filter by child element
await page.getByRole('listitem')
.filter({ has: page.getByRole('heading', { name: 'Premium Plan' }) })
.getByRole('button', { name: 'Select' })
.click();
// Negative filter: rows that do NOT contain "Archived"
await page.getByRole('row')
.filter({ hasNotText: 'Archived' });
Chaining Locators
Chaining scopes a locator within another locator — like finding an element inside a container.
// Find the sidebar, then find a link inside it
const sidebar = page.getByRole('navigation', { name: 'Sidebar' });
await sidebar.getByRole('link', { name: 'Settings' }).click();
// Find a specific card, then interact with elements inside
const productCard = page.getByTestId('product-card').filter({ hasText: 'Laptop' });
await productCard.getByRole('button', { name: 'Add to Cart' }).click();
const price = await productCard.getByTestId('price').textContent();
Nth Element Selection
When multiple elements match and you need a specific one:
// First matching element
await page.getByRole('button', { name: 'Delete' }).first().click();
// Last matching element
await page.getByRole('button', { name: 'Delete' }).last().click();
// Specific index (0-based)
await page.getByRole('listitem').nth(2).click();
// Counting elements
await expect(page.getByRole('listitem')).toHaveCount(5);
Prefer Filtering Over nth()
nth() is positional and breaks when item order changes. Prefer filtering by content:
// FRAGILE: relies on position
await page.getByRole('row').nth(3).getByRole('button').click();
// RESILIENT: targets by content
await page.getByRole('row')
.filter({ hasText: 'Order #1234' })
.getByRole('button', { name: 'View' })
.click();
Frame Locators
Iframes are common in payment forms, third-party widgets, and legacy applications. Playwright provides frameLocator() to scope into frames.
// Scope into an iframe by CSS selector
const paymentFrame = page.frameLocator('#payment-iframe');
await paymentFrame.getByLabel('Card number').fill('4242424242424242');
await paymentFrame.getByLabel('Expiry').fill('12/28');
await paymentFrame.getByRole('button', { name: 'Pay' }).click();
// Scope into a named frame
const adFrame = page.frameLocator('iframe[name="ad-banner"]');
// Nested frames
const inner = page.frameLocator('#outer').frameLocator('#inner');
await inner.getByRole('button').click();
Shadow DOM
Playwright pierces shadow DOM by default — unlike Selenium, where shadow DOM traversal requires JavaScript injection.
// Playwright automatically reaches into shadow DOM
await page.getByRole('button', { name: 'Toggle' }).click();
// For CSS locators, use >> to cross shadow boundaries explicitly
await page.locator('my-component >> .inner-element').click();
Locator Best Practices for Complex UIs
Data Tables
// Find a specific cell in a table
const row = page.getByRole('row').filter({ hasText: 'user@example.com' });
const status = await row.getByRole('cell').nth(3).textContent();
// Verify an entire row
await expect(row).toContainText(['user@example.com', 'Active', 'Admin']);
Dynamic Lists
// Wait for list to load, then interact with a specific item
await expect(page.getByRole('listitem')).toHaveCount(10);
const item = page.getByRole('listitem').filter({ hasText: 'Important Task' });
await item.getByRole('checkbox').check();
await expect(item).toHaveClass(/completed/);
Modal Dialogs
// Scope interactions to the modal
const modal = page.getByRole('dialog');
await modal.getByLabel('Name').fill('New Project');
await modal.getByRole('button', { name: 'Create' }).click();
await expect(modal).not.toBeVisible();
Key Takeaways
- Use
filter()to narrow locators by text content or child elements — more resilient than positional selectors - Chain locators to scope interactions within containers (navigation, cards, modals)
- Prefer
filter({ hasText })overnth()— content-based selection survives reordering frameLocator()scopes into iframes without JavaScript hacks- Playwright pierces shadow DOM automatically — no special handling needed
- Combine filtering, chaining, and scoping to target precise elements in complex UIs