QA Engineer Skills 2026QA-2026Advanced Locators

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 }) over nth() — 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