Series Navigation
→ 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:
- Detects the broken locator
- Scans the current DOM for the closest matching element
- Suggests the best replacement locator
- Shows a diff: old vs new locator
- 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.
Discussion
Loading...Leave a Comment
All comments are reviewed before appearing. No links please.