A Playwright locator is more likely to survive a UI refactor when it identifies an element by what it means to the user, rather than how the DOM happens to be structured. Prefer locators such as getByRole(), getByLabel(), and stable test IDs over long CSS/XPath chains, generated classes, or element position.
For example, instead of:
await page
.locator('.btn-primary.save-button')
.click();
prefer:
await page
.getByRole('button', { name: 'Save' })
.click();
That helps with good locator design, but when a refactor does invalidate a locator, you still have to diagnose and repair the affected test yourself.
Leapwork Play reduces that maintenance work by recording the Playwright interaction with additional locator information and using self-healing when an interaction can no longer find the intended element.
How does Leapwork Play handle locator changes?
When Play records an interaction, the test uses a normal Playwright locator for the element.
For example:
await page
.getByRole('button', {
name: 'Continue to payment'
})
.click();
The Play step can also retain additional locator information, including a relativeXpath:
await leapwork.step(
'Click Continue to payment',
async () => {
await page
.getByRole('button', {
name: 'Continue to payment'
})
.click();
},
{
action: 'click',
relativeXpath: '...'
}
);
If the primary locator can no longer find the intended element, Play can use the additional locator information as part of its recovery workflow instead of immediately requiring the test author to rewrite the interaction.
If the element still cannot be found, Self-healing can attempt to recover the intended interaction.
How to make locators survive a UI refactor
Use locators that describe the action the user sees, not the CSS class or DOM structure behind it.
Step 1: Identify the implementation-based locator
For example, the page may contain this button:
<button class="btn btn-primary save-button">
Save
</button>
A test that depends on those CSS classes can break after a redesign:
await page
.locator('.btn-primary.save-button')
.click();
Step 2: Check what changed for the user
After the redesign, the implementation might change while the visible action stays the same:
<button class="action-button primary">
Save
</button>
The CSS locator breaks, even though the user still sees the same Save action.
Step 3: Replace it with a semantic locator
Use a locator that targets the user-facing role and name instead:
await page
.getByRole('button', { name: 'Save' })
.click();
Tip: Locate an element by what it means to the user, not by how it happens to be implemented.
Which Playwright locators are more resilient?
You don't need a long locator hierarchy for most tests.
|
Element contract |
Prefer |
|---|---|
|
Accessible role and name |
|
|
Form label |
|
|
Meaningful visible text |
|
|
Explicit automation contract |
|
|
Stable attribute with no better semantic option |
|
Avoid tying tests unnecessarily to:
-
generated CSS classes;
-
long DOM paths;
-
element position;
-
dynamic IDs;
-
complex XPath expressions.
The goal is not to make the locator survive every UI change. It should survive implementation changes that do not change the behavior the test is supposed to verify.
How Play helps maintain Playwright locators through UI changes
|
With Playwright |
With Leapwork Play |
|---|---|
|
Choose and maintain the locator in test code |
Record the interaction and generate the underlying Playwright locator |
|
Update affected locators when UI changes break them |
Play can attempt locator recovery before manual repair is required |
|
Define alternative locator strategies when needed |
Play retains additional locator information for recorded interactions |
|
Diagnose and repair locator failures in code |
Work with the interaction as a Play test step while keeping Playwright underneath |
Play builds on Playwright's locator model by adding recording and locator-recovery capabilities that can reduce manual maintenance when the UI changes.
When should self-healing stop?
Self-healing should help when the same user action is still available but the locator broke because of an implementation change. It should not make a test pass when the product behaviour has changed.
Rule of thumb: recover the intended element, but do not hide a genuine change in what the user can do.
Example: do not self-heal to a different action
|
Before |
After |
What it means |
|---|---|---|
|
|
|
These are different user interactions. The test should fail and be updated intentionally. |
If the behaviour the test validates has genuinely changed, review the test and update it deliberately instead of letting self-healing choose a nearby element.
How to handle dynamic IDs
Dynamic IDs are a common reason locators break. If an ID changes between renders, do not use it as the element contract.
Avoid generated IDs
await page
.locator('#input-83921')
.fill('user@example.com');
In this example, 83921 is generated and may change the next time the page renders.
Use a user-facing locator instead
await page
.getByLabel('Email')
.fill('user@example.com');
This keeps the locator tied to the field the user recognises, rather than to an implementation detail. For repeated dynamic-ID problems, Play’s recorded locator and recovery workflow can reduce manual locator maintenance.
Best practices
-
Use Play's recorded locator and self-healing workflow to reduce repeated locator repair after UI changes.
-
Prefer
getByRole()andgetByLabel()for meaningful user-facing elements. -
Use
getByTestId()when the application needs an explicit, stable automation contract. -
Avoid long CSS/XPath chains that depend on DOM structure.
-
Avoid
nth()unless element position genuinely matters. -
Treat generated IDs and classes as unstable unless they are intentionally part of the application's contract.
-
Let tests fail when the actual user-facing behavior has changed.
Troubleshooting
|
Problem |
What to do |
|---|---|
|
CSS classes changed after a redesign |
Prefer a role, label, text, or stable test ID instead of presentation classes. |
|
The element is still present but the locator no longer finds it |
Check whether the locator depended on an implementation detail. In Play, check the recovery/self-healing result before manually rewriting the interaction. |
|
A locator now matches multiple elements |
Add meaningful scope instead of using |
|
A dynamic ID changed |
Replace it with a semantic locator or stable test contract. |
|
The UI behavior genuinely changed |
Update the test intentionally rather than expecting locator recovery to hide the change. |
Frequently asked questions
How does Leapwork Play help when a locator breaks?
Play records the interaction with its Playwright locator and additional locator information. When an interaction can no longer find the intended element, Play's recovery/self-healing workflow can attempt to recover it rather than immediately requiring the author to rewrite the locator.
What makes a Playwright locator resilient?
A resilient locator identifies an element using a stable contract such as its role, accessible name, label, visible text, or explicit test ID rather than temporary DOM structure or styling.
What is the best locator to use in Playwright?
There is no single best locator for every element. getByRole() and getByLabel() are strong defaults when the application's accessible semantics identify the target clearly.
Can self-healing replace good locator design?
No. Start with a locator that expresses the intended user interaction. Self-healing is useful when implementation changes break that locator; it should not conceal genuine changes to the behavior being tested.
Should a test survive every UI refactor?
No. A test should survive irrelevant implementation changes. If the user-facing behavior or contract changes, the test should fail so the change can be reviewed intentionally.