Recommended Free Tools
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.
The Tool Desk
Outbyte Driver Updater FREEFix the driver behind crashes, sound loss and screen glitchesFind Drivers →Outbyte PC Repair FREEClear out junk files and repair common Windows errorsFree Scan →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.
#1 Best Overall
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.
Windows Errors? Fix Them Before They Spread
Repair common Windows errors and clear accumulated junk for a smoother, more stable PC - no reinstall needed.Free scan · no reinstallOutdated Drivers Are Slowing You Down
One free scan finds every outdated or missing driver and matches the right update for your exact hardware.Free scan · exact hardware match| 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:
Rank #2
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:
Do these 3 things before closing this tab:
1Scan for outdated or missing drivers - takes under a minute2Repair Windows errors before they cause bigger problems3Fix the driver behind crashes, sound loss and screen glitchesawait 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.
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():
Rank #4
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.
Common mistakes and how to fix them
- Using
isEnabled()as a wait. It returns the current boolean state. Replace it withawait expect(locator).toBeEnabled()when waiting for a transition. - Passing
state: 'enabled'towaitFor(). 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 discouragespage.waitForSelector()in favor of web assertions or locator waits.
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.
- Confirm the locator. Check that the role, accessible name, label, or scope identifies the intended control in the current page.
- 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.
- Inspect disabled semantics. Look for a native
disabledattribute, a disabled ancestor fieldset, or applicablearia-disabledstate. A custom control may not be exposing its state as expected. - 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.
- 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.
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.
Quick Recap
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.

