Avoid building Playwright tests around generated IDs. They can change between renders and make locators unreliable. Use locators that describe what the user sees or interacts with instead, such as getByRole(), getByLabel(), visible text, or a deliberate test ID.
Avoid this brittle selector:
await page
.locator('#button-83921')
.click();
Prefer a user-facing locator:
await page
.getByRole('button', {
name: 'Button with Dynamic ID'
})
.click();
How Leapwork Play helps: Record the interaction once, and Play generates the Playwright step and locator information for you.
If the locator later stops finding the intended element, Play's self-healing can use additional locator details to recover the interaction instead of immediately failing the step.
How does Play handle a dynamically generated ID?
Instead of building the test around that generated ID, Play records the interaction using a semantic Playwright locator:
await page
.getByRole('button', {
name: 'Button with Dynamic ID'
})
.click();
The recorded Play step also contains additional locator information through relativeXpath:
await leapwork.step(
'Click the "Button with Dynamic ID" button in the Playground section',
async () => {
await page
.getByRole('button', {
name: 'Button with Dynamic ID'
})
.click();
},
{
action: 'click',
relativeXpath: '...'
}
);
This means the test is not being authored around the changing ID itself.
What happens when the locator changes?
This is where Play's self-healing capability becomes important.
enableSelfHeal:
(leapwork.team.settings.get("enableSelfHeal")
?? leapwork.workspace.settings.get("enableSelfHeal")) !== "false"
When the element can no longer be found using the original locator, Play can attempt to recover the interaction using the additional locator information associated with the step.
In the demonstrated workflow, this includes a relative XPath.
The practical difference is that a locator change does not necessarily mean the QA author has to immediately find the new ID and rewrite the test manually.
What should you use instead of a dynamic ID?
If you are writing Playwright directly, prefer a stable user-facing locator:
|
Element |
Prefer |
|---|---|
|
Button with a meaningful name |
|
|
Form control with a label |
|
|
Stable visible text |
|
|
Explicit automation contract |
|
For example:
await page
.getByLabel('Email')
.fill('user@example.com');
instead of:
await page
.locator('#input-83921')
.fill('user@example.com');
The first locator describes what the user interacts with. The second depends on an implementation detail that may change.
How Play helps maintain Playwright locators
|
With Playwright |
With Leapwork Play |
|---|---|
|
Choose and write the locator |
Record the interaction and generate the Playwright locator |
|
Maintain locator changes in the test |
Play can use self-healing to recover supported interactions when a locator changes |
|
Add alternative locator logic when needed |
Play retains additional locator information, such as the demonstrated |
|
Diagnose and update broken locators in code |
Work with the interaction as a Play test step while keeping Playwright underneath |
Play builds on Playwright's locator model by adding recorded locator information and recovery capabilities that can reduce manual locator maintenance when the UI changes.
What if part of the ID is stable?
Use partial ID selectors only as a fallback.
If one part of the ID is intentionally stable, you can target that stable portion:
page.locator('[id^="customer-"]');
However, this is less reliable than a role, label, or test ID because the naming pattern can still change.
For example, this selector works while IDs start with customer:
customer-18372
But it can fail if the application changes the ID pattern:
customer-field-18372
Use a role, label, or deliberate test ID whenever one is available. These locators describe the user-facing contract instead of depending on an implementation detail.
Best practices
-
Use Play's recorded locator and self-healing workflow when locator changes would otherwise create repeated maintenance.
-
Prefer role, label, text, and test-ID locators over generated IDs.
-
Treat a generated ID as an implementation detail unless its stability is intentional.
-
Use partial IDs only when the stable portion is deliberate and unique.
-
Avoid using
nth()simply to make an ambiguous dynamic-ID locator pass. -
Do not add waits to compensate for an unstable locator; waiting does not make a generated ID stable.
Troubleshooting
|
Problem |
What to do |
|---|---|
|
The ID changes every run |
Use a semantic locator instead of the complete generated ID. |
|
A partial ID matches several elements |
Use role, label, text, or another meaningful scope instead of relying on position. |
|
The recorded interaction stops finding the element |
Check the Play execution/self-healing result before manually replacing the locator. |
|
The UI changed and several locators broke |
Check whether the tests depend on implementation details rather than stable user-facing contracts. |
Frequently asked questions
What should I use instead of a dynamic ID in Playwright?
Prefer getByRole(), getByLabel(), getByText(), or getByTestId() depending on the element.
Can I use part of a dynamic ID?
Yes, if part of the ID is intentionally stable and uniquely identifies the element. Prefer a semantic locator when one is available.
How does Leapwork Play handle dynamic IDs?
Play records the interaction and generates the Playwright locator information. The demonstrated workflow uses a semantic getByRole() locator and also retains a relativeXpath on the Play step.
What happens if the locator later stops working in Play?
For supported interaction steps, Play's self-healing mechanism can attempt to recover the intended element using the additional locator information associated with the step.