How to Check if an Element Exists in Playwright (and Why “Exists” Means Four Different Things)

In Playwright, does this element exist? can mean several different things. An element can match a locator, be attached to the DOM, be visible, or be completely absent. The right Playwright API depends on which state your test actually needs to verify.

Leapwork Play reduces how much of this element-finding logic you have to manage manually. Play records the interaction and generates the Playwright code with the locator. If that locator later stops finding the intended element, Play's self-healing mechanism can use additional locator information including the XPath fallback to recover the element and continue the interaction.

Try Leapwork Play →

What does “exists” mean in Playwright?

Before checking whether an element exists, decide what you actually mean by exists. There are four common questions:

What you mean

What you are checking

Does the locator match an element?

Whether one or more elements match the locator

Is the element attached?

Whether the matching element is currently in the DOM

Is the element visible?

Whether the matching element is currently visible to the user

Is the element absent?

Whether the locator matches no elements

These states are not interchangeable.

An element can exist in the DOM while being hidden. An application can also temporarily remove and recreate an element while rendering.

The first step is therefore to decide which state matters to the test.

How do you check if an element is visible?

If the test needs to verify that an element is visible to the user, use a web-first assertion:

await expect(
page.getByRole('button', { name: 'Submit' })
).toBeVisible();

This is appropriate for questions such as:

  • Is the submit button displayed?

  • Has the success message appeared?

  • Is the login form visible?

  • Is the dialog open?

The assertion retries until the expected condition is met or the assertion timeout expires.

How does Leapwork Play find the element differently?

With standard Playwright, you choose and maintain the locator used to find the element:

await page
  .getByRole('button', { name: 'Continue to payment' })
  .click();

In Leapwork Play, recording the interaction creates the Playwright step and its locator information for you.

The bigger difference appears when the application changes. If the configured locator can no longer find the intended element, Play can use its Self-healing mechanism to attempt to recover the interaction instead of immediately failing the step.

In the supplied Play example, the self-healing flow uses additional locator information, including a relative XPath, to identify the intended element and continue execution.

 

In Playwright

In Play

Find an element

Author chooses the locator

Play records the interaction and locator

Locator changes

Author updates the locator

Play can attempt self-healing

Fallback

Author implements another locator strategy

Play can use additional locator information, including the demonstrated XPath fallback

Validate element state

Use Playwright assertions

Standard Playwright assertion can be represented as a Play Validate step

How do you check if an element exists in the DOM?

If the test specifically needs to verify that a locator matches an element, use a count assertion.

await expect(
  page.getByTestId('status-message')
).toHaveCount(1);

This verifies that exactly one element matches the locator.

If two elements are expected:

await expect(
  page.getByRole('listitem')
).toHaveCount(2);

How do you check that an element does not exist?

If no matching element should be present, use a zero-count assertion:

await expect(
page.getByTestId('error-message')
).toHaveCount(0);

For example:

await page
  .getByRole('button', { name: 'Submit' })
  .click();
await expect(
  page.getByTestId('validation-error')
).toHaveCount(0);

This explicitly verifies that no validation-error elements match the locator.

In Playwright, this is the same underlying assertion concept you can place inside a Play validation step when you want the absence check represented as a named validation.

await leapwork.step(
  "Verify no validation error",
  async () => {
    await expect(
      page.getByTestId('validation-error')
    ).toHaveCount(0);
  },
  {
    action: "validate"
  }
);

The exact Recorder/AI workflow for creating such a negative validation should be confirmed with the product team; the important distinction is that the validation itself remains a Playwright assertion.

What is the difference between toHaveCount() and toBeVisible?

The toHaveCount() checks how many elements match a locator. toBeVisible() checks whether the matched element is visible to the user.

Assertion

What it verifies

Use it when

await expect(locator).toHaveCount(1);

Exactly one element matches the locator.

You need to confirm that a matching element exists in the DOM.

await expect(locator).toBeVisible();

The matched element is visible to the user.

You need to confirm that the user can see or interact with the element.

For example, an application may render a menu in the DOM but keep it hidden until the user opens it:

await expect(
  page.getByRole('menu')
).toHaveCount(1);
await expect(
  page.getByRole('menu')
).toBeHidden();

How do you check if an element is attached?

If attachment itself is the condition the test needs, use locator.waitFor():

await page
  .getByTestId('result')
  .waitFor({
    state: 'attached'
  });

This waits until the element is attached to the DOM.

When the real requirement is that the user should be able to see the element, a visibility assertion is usually clearer:

await expect(
  page.getByTestId('result')
).toBeVisible();

The distinction is: Attached means present in the DOM. Visible means displayed to the user.

How do you check if an optional element exists?

Some UI elements are expected to appear only in certain situations. For example, a welcome banner may be shown to some users but not to others.

When an element is genuinely optional, check the current state first and only interact with it if it is present.

const banner = page.getByTestId('welcome-banner');
if (await banner.count() > 0) {
  await banner
    .getByRole('button', { name: 'Dismiss' })
    .click();
}

Code

Meaning

When to use it

await banner.count() > 0

The banner may or may not be present.

Use this for genuine branching logic, where the test should continue either way.

await expect(banner).toHaveCount(0);

The banner must not exist.

Use this when absence is part of the expected result.

By contrast, use an assertion when the test result depends on the banner being absent:

await expect(banner).toHaveCount(0);

Use count() for genuine branching logic. Use an assertion when the state is part of the expected result.

What if a locator matches more than one element?

“Exists” can also be misleading when your locator matches several elements.

For example:

const deleteButtons = page.getByRole(
  'button',
  { name: 'Delete' }
);

There may be several Delete buttons.

If the test expects exactly one:

await expect(deleteButtons).toHaveCount(1);

Or, better, make the locator more specific so it identifies the intended button.

A locator matching something does not necessarily mean it is the correct locator.

Why should you use locators instead of manually querying the DOM?

You may see code such as:

const element = await page.$('#status');
if (element) {
  // ...
}

There are cases where direct DOM inspection is useful, but normal test expectations are usually clearer with locator assertions.

For example:

await expect(
  page.getByTestId('status')
).toHaveCount(1);

The second version tells the reader exactly what the test expects and uses Playwright's retrying assertion mechanism.

What if the element appears asynchronously?

Do not add a fixed timeout just because the element appears later. Instead use the following

await expect(
  page.getByTestId('success')
).toBeVisible();

The assertion waits for the actual condition.

How do you check for an element before interacting with it?

Usually, you do not need a separate existence check before an interaction. Playwright's actionability checks already handle the conditions required for the click. It is useful when the test specifically needs to verify that exactly one button exists.

If the only reason is to make the click work, the extra assertion is usually unnecessary.

await expect(
  page.getByRole('button', { name: 'Submit' })
).toHaveCount(1);
await page
  .getByRole('button', { name: 'Submit' })
  .click();

How does Leapwork Play represent this kind of validation?

Leapwork Play keeps the assertion as Playwright code but wraps it in a named validation step.

The product example demonstrated in the supplied video validates the cart state after removing a product:

await leapwork.step(
  "Verify cart count",
  async () => {
    await expect(
      page.locator('#cartCount')
    ).toContainText('0');
  },
  {
    action: "validate"
  }
);

The important part is that the actual assertion is still normal Playwright.

The validate action gives the test a distinct validation step rather than changing the underlying Playwright assertion model.

Best practices

  • Decide which element state your test needs to verify, then choose the matching assertion: toBeVisible() for visibility, toBeHidden() for hidden state, toHaveCount(1) when exactly one element should match, and toHaveCount(0) for true absence.

  • Use locator.waitFor({ state: 'attached' }) only when DOM attachment itself matters, and use count() only when the element is genuinely optional.

  • Avoid fixed waits before element-state assertions; keep validation logic explicit and close to the behaviour being tested.

  • Use the same Playwright assertion inside a Play Validate step when the Play workflow calls for explicit validation.

  • Do not confuse state validation with locator self-healing.

Troubleshooting

Issue

What to check

Recommended fix

toHaveCount(1) passes but the element is not visible

The element exists in the DOM but may be hidden.

Use toBeVisible() when visibility is what matters.

toHaveCount(0) passes but I can see an error

The locator may be checking the wrong element, such as proving that .error does not exist.

Verify that the locator targets the application’s actual error state.

The element appears later in the test

The test may be relying on timing rather than the final UI condition.

Use a retrying assertion instead of a fixed delay:

await expect(
  page.getByTestId('success-message')
).toBeVisible();

A validation passes locally but fails in CI

The application state may differ in CI because of environment, authentication, timing, test data, or a real application failure.

Inspect the actual application state at the point of validation.

A validation step seems to be looking for a locator that no longer exists

Validate and self-healing are separate concerns.

Do not assume self-healing will repair the validation; investigate the validation locator itself.

Frequently asked questions

What does action: "validate" mean in Leapwork Play?

It identifies a named Play step as a validation step while allowing the step to contain standard Playwright assertion code.

How does Leapwork Play find an element?

Play records the interaction and generates the corresponding Playwright code and locator information.

What happens if the element's locator changes in Play?

For supported interaction steps, Play can attempt self-healing using additional locator information to recover the intended element. The supplied Play example demonstrates an XPath fallback as part of this recovery.

How do I check if an element exists in Playwright?

First define “exists.” Use toBeVisible() for visibility, toHaveCount() for DOM presence or absence, and locator.waitFor({ state: 'attached' }) when attachment itself is the condition.

How do I check if an element is not present?

Use:

await expect(locator).toHaveCount(0);

Can an element exist but not be visible?

Yes. An element can remain attached to the DOM while being hidden.

What is the difference between toHaveCount() and toBeVisible()?

toHaveCount() checks how many elements match a locator. toBeVisible() checks whether the matching element is visible.

How do I check whether an optional element exists?

Use locator.count() and branch when the element is genuinely optional.

Should I use waitForTimeout() before checking whether an element exists?

No. Use the assertion or locator wait that represents the actual condition.