← Blog

"Self-Healing Locators in Playwright Studio — Keep Tests Passing When UIs Change"

The biggest maintenance cost in test automation is broken locators. Learn how Playwright Studio's locator stability scoring works and how to use it to write locators that survive UI changes.

reading now
views
comments

Series Navigation

Network Mock Studio

From Recording to CI/CD Pipeline


The Locator Fragility Problem

Test maintenance cost is dominated by broken locators. A UI refactor renames a class, moves an element, or changes an ID — and 30 tests fail overnight. This is the #1 reason QA teams abandon automated tests.

Fragile locator (breaks constantly):
page.locator('div.sc-bdXxxt.fGarBS > button:nth-child(2)')

Stable locator (survives UI changes):
page.getByRole('button', { name: 'Submit Order' })
page.getByTestId('submit-order-btn')

How Playwright Studio Scores Locators

When you click an element during recording or use the Assertion Builder, Playwright Studio evaluates every possible way to select that element and assigns a stability score:

Scoring criteria:
├── Uses data-testid / data-qa         → +40 pts  (most stable)
├── Uses ARIA role + accessible name    → +35 pts
├── Uses ARIA label                     → +30 pts
├── Uses unique text content            → +25 pts
├── Uses stable id attribute            → +20 pts
├── Uses name attribute                 → +15 pts
├── Uses non-generated CSS class        → +10 pts
├── Uses positional/structural selector → -20 pts  (fragile)
├── Uses generated class names          → -30 pts  (breaks on rebuild)
└── Uses nth-child / nth-of-type        → -25 pts  (order-dependent)

The top-scoring locator is used in generated code. The panel shows the top 3–5 alternatives ranked by score.


Using the Locator Panel

After recording, click any action in the code panel to open its locator options:

Action: await page.click(???)

Locator options:
★ 95pts  page.getByTestId('login-btn')           ← selected
★ 82pts  page.getByRole('button', {name:'Login'})
★ 71pts  page.locator('#login-btn')
★ 45pts  page.locator('.btn.btn-primary')
★ 12pts  page.locator('form > div:last-child > button')

Click any alternative to swap the locator in the generated code.


When a Test Breaks — Using Self-Healing

When a test fails with Locator not found, Playwright Studio's self-healing feature:

  1. Detects the broken locator
  2. Scans the current DOM for the closest matching element
  3. Suggests the best replacement locator
  4. Shows a diff: old vs new locator
  5. One-click to accept the fix
Self-healing suggestion:

BROKEN:  page.locator('.submit-button')
         (class removed during CSS refactor)

HEALED:  page.getByRole('button', { name: 'Submit' })
         (found by ARIA role — more stable)

Accept? [✓ Apply] [✗ Ignore]

Adding data-testid to Your App

The most effective thing your QA team can do is work with developers to add data-testid attributes to interactive elements. This permanently eliminates locator fragility.

The agreement to propose to your dev team:

Every interactive element (button, input, link, form)
gets a data-testid attribute following this pattern:

<component>-<action>
e.g:
  login-submit-btn
  email-input
  forgot-password-link
  product-add-to-cart
  checkout-confirm-btn

Implementation — it's one attribute:

// React
<button data-testid="login-submit-btn" onClick={handleLogin}>
  Sign In
</button>

<input 
  data-testid="email-input"
  type="email" 
  value={email}
  onChange={e => setEmail(e.target.value)}
/>
<!-- Plain HTML -->
<button data-testid="checkout-btn" class="btn btn-primary">
  Complete Purchase
</button>

Generated Playwright code with testids:

// Clean, stable, readable
await page.getByTestId('email-input').fill('user@example.com');
await page.getByTestId('password-input').fill('password123');
await page.getByTestId('login-submit-btn').click();
await expect(page.getByTestId('dashboard-welcome')).toBeVisible();

This code will never break due to a CSS refactor, class rename, or structural change — only if the data-testid itself is removed, which should be a deliberate decision.


ARIA Locators — When You Can't Add testids

If you're testing a third-party component or can't modify the source:

// Role + accessible name (reads the button's text/aria-label)
await page.getByRole('button', { name: 'Add to cart' }).click();
await page.getByRole('link', { name: 'Documentation' }).click();
await page.getByRole('textbox', { name: 'Email address' }).fill('...');
await page.getByRole('checkbox', { name: 'I agree to terms' }).check();

// Label text (finds input associated with a <label>)
await page.getByLabel('Email address').fill('user@example.com');
await page.getByLabel('Password').fill('password123');

// Placeholder text
await page.getByPlaceholder('Search products...').fill('widget');

// Visible text
await page.getByText('Forgot your password?').click();

These are all more stable than CSS class or structural selectors.


What's Next

Discussion

Loading...

Leave a Comment

All comments are reviewed before appearing. No links please.

0 / 1000