Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Some links on this page are affiliate links: if you buy through them we may earn a commission, at no extra cost to you.

Use Playwright Test’s retrying assertion: await expect(locator).toBeEnabled(). For example, await expect(page.getByRole('button', { name: 'Submit' })).toBeEnabled() waits for that button to become enabled, or fails when the assertion times out. If you only need to click the button, await locator.click() already waits for enabled state as part of its actionability checks.

Wait for enabled state with toBeEnabled()

In a Playwright Test using TypeScript, first create a locator, then await the web-first assertion:

import { test, expect } from '@playwright/test';

test('submits the form after it becomes ready', async ({ page }) => {
  await page.goto('https://example.com/form');

  const submit = page.getByRole('button', { name: 'Submit' });
  await expect(submit).toBeEnabled();
  await submit.click();
});

toBeEnabled() retries the assertion while the element is disabled. It passes when the locator resolves to an enabled element; if that does not happen before the assertion’s timeout, the test fails. Because it is asynchronous, keep the await. Without it, the test can move on without waiting for the assertion to finish.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

This is the right choice when the enabled transition itself is part of what the test needs to verify—for example, when a form should enable Submit after valid input. The assertion synchronizes the test with the state rather than guessing how long the page needs.

Set a timeout for a slower transition

If this specific state change is expected to take longer than the project’s configured assertion timeout, pass a timeout to the assertion:

await expect(submit).toBeEnabled({ timeout: 10_000 });

This waits for up to 10 seconds for this assertion. Choose a limit that reflects the transition the test is meant to allow; a long timeout can make a genuine failure slower to diagnose. If the assertion times out, inspect the page state and the control’s disabled conditions rather than increasing the limit automatically.

Choose between asserting, checking, and clicking

These APIs serve different purposes. A current-state check is not interchangeable with a retrying assertion, and waiting for the click is not always the same test as asserting enabled state.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
Approach Waits for enabled state? Use it when
await expect(locator).toBeEnabled() Yes. The assertion retries until it passes or times out. The test must verify or synchronize on enabled state.
await locator.isEnabled() No. It returns the state at the time of the check. You need an immediate boolean for a deliberate conditional or observation.
await locator.click() Yes, as part of the click’s actionability checks. The next step is to click, and a separate enabled-state assertion is not needed.

When the test only needs the action

If the behavior under test is simply “the user can submit,” click directly:

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

Playwright waits for the target to be unique, visible, stable, able to receive events, and enabled before clicking. A separate toBeEnabled() is useful when it makes the test’s requirement explicit or gives a more focused failure. Avoid adding it mechanically before every click: the click already performs the enabled check.

When a boolean is actually needed

isEnabled() answers “is this enabled right now?” It does not wait for a future transition. A one-off check can be appropriate if the test intentionally branches on the current UI state, but using it to synchronize asynchronous behavior can be flaky:

// Immediate observation: this may still be false while the page is updating.
const enabledNow = await submit.isEnabled();

// Synchronization: waits for the expected transition.
await expect(submit).toBeEnabled();

Why waitFor() cannot wait for enabled state

locator.waitFor() supports the states attached, detached, visible, and hidden. It does not have an enabled state. This is not valid:

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
await submit.waitFor({ state: 'enabled' });

Use await expect(submit).toBeEnabled() for an enabled-state wait. A visible element can still be disabled, so waiting for visible does not establish that it can be clicked.

What Playwright treats as enabled

Playwright’s enabled-state rules account for native form controls and disabled semantics. Native buttons, selects, inputs, textareas, options, and optgroups can be disabled with the HTML disabled attribute. A control inside a disabled fieldset is also treated as disabled. Playwright also recognizes descendants of an element marked [aria-disabled=true] as disabled.

The HTML disabled attribute has browser meaning on native controls; putting it on an arbitrary element such as a <div> does not make that element a disabled native control. Custom widgets should expose their disabled state with appropriate semantics, including ARIA where applicable. Test the UI contract the application actually provides rather than assuming a visual style or arbitrary attribute changes enabled state.

Enabled is only one condition for an interaction. A control can be enabled but covered by an overlay, hidden, moving, or otherwise unable to receive a click. The enabled assertion establishes only enabled state; the click action checks the broader conditions needed to perform the action.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Choose a locator that survives page updates

Prefer a locator based on how a user or assistive technology identifies the control. For a button with an accessible name, use getByRole():

const submit = page.getByRole('button', { name: 'Submit' });
await expect(submit).toBeEnabled();

For a labeled form control, getByLabel() may be a better fit. Other deliberate choices include getByText(), getByPlaceholder(), or getByTestId(), depending on the contract the test is intended to protect. A role-and-name locator makes the expected control explicit and can expose accessible-name or role regressions.

Locators are resolved against the current DOM when used. If a framework rerenders the form and replaces a button, a locator can resolve to the replacement element on a retry. This is generally more reliable than retaining an element handle from before the rerender.

Make the target unambiguous

The locator should identify the intended control. If a page has two buttons named “Submit,” scope the locator to the relevant form or region rather than allowing the assertion to act on an ambiguous target. Playwright actions require a unique target; an assertion against a locator that does not resolve as intended will not verify the button you meant to test.

Free tools Windows power users keep installed

One-click scans. No signup required.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Common mistakes and how to fix them

  • Using isEnabled() as a wait. It returns the current boolean state. Replace it with await expect(locator).toBeEnabled() when waiting for a transition.
  • Passing state: 'enabled' to waitFor(). Enabled is not one of its documented wait states. Use the retrying assertion.
  • Waiting for visibility and assuming the control is enabled. Visibility and enabled state are distinct. Assert the state the test actually requires.
  • Adding a fixed sleep. A delay only waits for a chosen duration; it does not establish that the control changed state. Prefer a condition-based assertion.
  • Asserting enabled and then seeing a click timeout. Enabled does not prove that the element is visible, stable, unique, or receiving events. Check for overlays, animation, or a locator that matches the wrong target.
  • Using older page-level waiting patterns in new code. Playwright discourages page-level isEnabled() in favor of locator-based APIs, and discourages page.waitForSelector() in favor of web assertions or locator waits.
Independent reader supportYour contribution helps us test, update, and keep practical guides available for everyone.Support on Ko-Fi

Diagnose a timeout

If toBeEnabled() times out, the assertion has not observed the requested state within its allowed time. Work through the state and locator rather than treating the timeout as proof that the application is simply slow.

  1. Confirm the locator. Check that the role, accessible name, label, or scope identifies the intended control in the current page.
  2. Check the application condition. The control may remain disabled until required fields are valid, a request finishes, or another prerequisite is met. Verify that the test performs the action that should satisfy that prerequisite.
  3. Inspect disabled semantics. Look for a native disabled attribute, a disabled ancestor fieldset, or applicable aria-disabled state. A custom control may not be exposing its state as expected.
  4. Distinguish state from clickability. If the enabled assertion passes but the click fails, investigate visibility, overlays, movement, event interception, and target uniqueness; those are separate actionability checks.
  5. Adjust timeout only with a reason. If the expected transition legitimately takes longer, set an assertion timeout appropriate to that transition. Do not mask a broken prerequisite or wrong locator with a large timeout.

Or skip the browser setup

ScreenshotNeo is a screenshot API, not a Playwright assertion or a way to wait for an element to become enabled. Use Playwright’s locator assertion above when that is the requirement. If you separately need a website screenshot without setting up a browser capture flow, ScreenshotNeo accepts a URL in one GET request. See the ScreenshotNeo API documentation.

curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://stripe.com -o shot.webp
import requests
r = requests.get("https://api.screenshotneo.com/v1/shot", params={"access_key": "YOUR_API_KEY", "url": "https://stripe.com"}, timeout=90)
open("shot.webp", "wb").write(r.content)
const q = new URLSearchParams({ access_key: 'YOUR_API_KEY', url: 'https://stripe.com' });
const res = await fetch(`https://api.screenshotneo.com/v1/shot?${q}`);

ScreenshotNeo removes cookie and consent banners, newsletter popups, and chat widgets before capture; each of those steps can be turned off. Bot checks, blank pages, failed loads, timeouts, and cache hits are not billed, and response headers indicate the page verdict and billing status. Its MCP server offers screenshot and PDF tools for AI agents. The free plan includes 1,000 screenshots a month with no card; paid plans start at $5 for 3,000 shots.

Sign up for ScreenshotNeo’s free plan.

Frequently Asked Questions

How do I assert that a control is disabled instead?

Use Playwright Test’s corresponding web-first assertion, await expect(locator).toBeDisabled(), when disabled state is the behavior you need to verify.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Does an enabled assertion guarantee that clicking will work?

No. It checks enabled state only. A click also requires a unique, visible, stable target that can receive events.

Can Playwright wait for a custom JavaScript condition?

A locator’s waitForFunction(fn) can wait for a custom condition, with the locator re-resolved on retries. For ordinary enabled-state checks, the dedicated toBeEnabled() assertion is clearer.

Product prices and availability are accurate as of the date/time indicated and are subject to change. Any price and availability information displayed on Amazon at the time of purchase will apply.