supaguardsupaguardDocs
Playwright

Resilient Locators: Best Practices for Stable Monitors

Learn how to use Playwright locators effectively to build synthetic monitors that survive UI changes. Master getByRole, getByLabel, and data-testid.

The most common cause of flaky synthetic monitors is brittle selectors. If your test relies on CSS classes like .btn-blue-large or complex XPath strings, it will likely break when your frontend team does a minor refactor.

Playwright's Locators are designed to be resilient, semantic, and closer to how users interact with the page.

The Priority Hierarchy

When choosing a locator, follow this priority list to ensure maximum stability:

This is the gold standard. It reflects how assistive technology (screen readers) perceives the page.

// ✅ Resilient: Even if the styling changes, it's still a "Sign In" button.
await page.getByRole('button', { name: 'Sign In' }).click();

2. page.getByLabel()

Ideal for form fields. It ensures your labels are correctly associated with inputs.

await page.getByLabel('Email Address').fill('user@example.com');

3. page.getByTestId()

Use this when semantic locators aren't enough or when the UI is highly dynamic. You should add data-testid attributes to your application code.

// In your React/HTML: <button data-testid="submit-order">...</button>
await page.getByTestId('submit-order').click();

Locators to Avoid

To maintain a Premium Emerald standard of reliability, avoid these patterns:

  • Brittle CSS Classes: page.locator('.css-1v9j8p3') — These change every time you rebuild your styles (Tailwind, Styled Components).
  • Deep XPaths: page.locator('//div[3]/div/ul/li[2]/button') — Any structural change in the DOM will break this.
  • Arbitrary Tag Names: page.locator('button') — Fails if you add a second button to the page.

Chaining & Filtering

Often, you need to find an element inside another. Use chaining to narrow down the scope.

// Find the 'Delete' button specifically inside the 'Settings' card
const settingsCard = page.locator('.settings-card');
await settingsCard.getByRole('button', { name: 'Delete' }).click();

You can also filter by text or another locator:

await page.getByRole('listitem')
  .filter({ hasText: 'Product A' })
  .getByRole('button', { name: 'Add to Cart' })
  .click();

Pro-Tip: Visible Only

Synthetic monitors sometimes fail because Playwright finds multiple elements, some of which are hidden. Use the visible filter:

await page.getByRole('button', { name: 'Submit' }).and(page.locator(':visible')).click();

Next Steps

On this page