What’s actually slowing this PC down?
Pick the symptom - the matching free tool is one click away.
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 await locator.waitFor({ state: 'visible' }) when a test must explicitly wait for a locator state. If the purpose is to verify that the element eventually becomes visible, use the retrying assertion await expect(locator).toBeVisible() instead. Playwright actions such as click() already wait for actionability, so an extra wait is needed only when it expresses a separate condition.
The three correct ways to wait
Playwright offers three related mechanisms. Choose by intent rather than adding delays:
| Intent | Use | Failure meaning |
|---|---|---|
| Perform an action when the target is actionable | await locator.click(), fill(), and similar actions |
The action could not become actionable within the applicable timeout. |
| Synchronize with a DOM state without asserting a business expectation | await locator.waitFor({ state: 'visible' }) |
The requested state was not reached within the timeout. |
| Verify an eventual condition in a test | await expect(locator).toBeVisible() |
The assertion failed after retrying until its timeout. |
Locator APIs are preferred because a locator re-resolves against the current DOM when used. That matters when a framework replaces or re-renders the element between the time you create the locator and the time the condition is met. Prefer user-facing locators such as getByRole, getByLabel, and getByText, then narrow them until they identify one intended target.
Free tools Windows power users keep installed
One-click scans. No signup required.
Wait explicitly with locator.waitFor()
Create a locator and call waitFor with the state your next operation requires:
#1 Best Overall
import { test, expect } from '@playwright/test';
test('shows the saved status', async ({ page }) => {
await page.getByRole('button', { name: 'Save' }).click();
const status = page.getByRole('status');
await status.waitFor({ state: 'visible' });
await expect(status).toHaveText('Saved');
});
The state option accepts four values:
visible: the locator resolves to an element with a non-empty bounding box that is notvisibility:hidden. This is the default.attached: an element matching the locator is present in the DOM, even if it is not visible.hidden: the element is detached or does not meet the visibility criteria.detached: no matching element remains in the DOM.
You can provide a timeout for this particular wait. A value of 0 uses the configured timeout behavior rather than imposing an arbitrary sleep:
await page.getByTestId('results').waitFor({
state: 'attached',
timeout: 10_000,
});
Use attached when code needs the node to exist, such as when a component creates a container before populating it. Use visible when a user must see it. Use hidden or detached when a transition is complete only after a spinner or dialog disappears.
Use expect(locator).toBeVisible() for visibility assertions
When visibility is what the test is checking, the web-first assertion is clearer and more robust:
import { test, expect } from '@playwright/test';
test('reveals account details', async ({ page }) => {
await page.getByRole('button', { name: 'Show details' }).click();
await expect(page.getByRole('region', { name: 'Account details' }))
.toBeVisible();
});
toBeVisible() retries until the condition is met or the assertion timeout expires. The same pattern applies to other eventual states, for example:
await expect(page.getByRole('status')).toHaveText('Saved');
await expect(page.getByRole('list')).toContainText('Invoice 1042');
await expect(page.getByRole('dialog')).toBeHidden();
Playwright’s Locator API specifically recommends the assertion when you need to assert visibility, rather than taking a one-time snapshot. Assertions therefore communicate that a failure is a test expectation failure, not merely a synchronization step.
Rank #2
Understand action auto-waiting
Most locator actions already wait for actionability. Before a click, Playwright waits for the target to be visible, stable, able to receive pointer events, and enabled. This is why the following usually needs no preceding visibility wait:
const submit = page.getByRole('button', { name: 'Submit' });
await submit.click();
An element can be visible yet still be covered by another element, moving during an animation, disabled, or unable to receive pointer events. A manual waitFor({ state: 'visible' }) checks only visibility; it does not replace the actionability checks performed by click(). Add an explicit wait when it represents a distinct condition, such as waiting for a status message before reading its text, waiting for a loading container to attach before observing it, or waiting for a dialog to detach before continuing.
The Tool Desk
Outbyte PC Repair FREERepair Windows errors before they cause bigger problemsFix Now →Outbyte Driver Updater FREEFix the driver behind crashes, sound loss and screen glitchesFind Drivers →Choose resilient locators
A wait is only as reliable as the locator it retries. Prefer semantics visible to users:
const email = page.getByLabel('Email address');
const save = page.getByRole('button', { name: 'Save' });
const notice = page.getByText('Changes saved');
If a role locator matches more than one element, narrow it with a name, filter, or a parent scope. Locator operations that imply a single target are strict and can fail when multiple elements match instead of silently choosing one:
const dialog = page.getByRole('dialog', { name: 'Delete project' });
const confirm = dialog.getByRole('button', { name: 'Delete' });
await expect(confirm).toBeVisible();
Use CSS selectors or test IDs when semantic markup cannot distinguish the target, but avoid selectors tied to generated classes or layout details. Because locators are evaluated when each operation runs, they are safer across re-renders than storing an element handle from an earlier DOM snapshot.
Waiting for attachment, disappearance, and replacement
Wait for a node to be attached
const chart = page.locator('#sales-chart');
await chart.waitFor({ state: 'attached' });
// The node exists; it may still be hidden or empty.
Wait for a loading indicator to disappear
const spinner = page.getByRole('status', { name: 'Loading' });
await spinner.waitFor({ state: 'hidden' });
If your application removes the node rather than hiding it and that distinction matters, use detached:
Crashes, No Sound, or Screen Glitches?
Random freezes, missing sound and display glitches usually trace back to one bad driver. Find and replace yours safely.Free scan · under a minutePC Slower Than It Used to Be?
A free scan shows the junk files, broken settings and background clutter dragging Windows down - then fixes them in one click.Free scan · Windows 10 & 11await page.locator('[data-testid="loading-overlay"]')
.waitFor({ state: 'detached' });
Wait for a replacement to become usable
For a component that first renders a placeholder and then replaces it, wait for the user-facing result rather than the placeholder’s disappearance:
const rows = page.getByRole('row');
await expect(rows).toHaveCount(6);
await expect(page.getByRole('cell', { name: 'Ada Lovelace' }))
.toBeVisible();
Why fixed sleeps are a poor substitute
await page.waitForTimeout(1000) pauses for a fixed duration, not for a condition. If the page is slower, the test remains flaky; if it is faster, the test wastes time. Locator auto-waiting and retrying assertions adapt to the actual state. A delay can be useful for diagnosing a timing issue temporarily, but it should not be the synchronization strategy in a finished test.
isVisible() is an immediate check
locator.isVisible() returns a boolean immediately. It does not retry while an element is becoming visible:
if (await page.getByRole('status').isVisible()) {
// This branch reflects the state at this instant only.
}
For an eventual condition, replace it with an assertion or explicit wait:
Rank #4
await expect(page.getByRole('status')).toBeVisible();
// or, when synchronization rather than assertion is intended:
await page.getByRole('status').waitFor({ state: 'visible' });
Why page.waitForSelector() is discouraged for new code
page.waitForSelector() remains available, but Playwright’s Page API directs new code toward Locator APIs and web-first assertions. A legacy call such as:
await page.waitForSelector('.toast', { state: 'visible' });
can be expressed as:
await page.locator('.toast').waitFor({ state: 'visible' });
// or, when visibility is the expectation:
await expect(page.locator('.toast')).toBeVisible();
The locator form keeps the target and subsequent operations in the same abstraction and makes the test’s intent explicit.
Independent reader supportYour contribution helps us test, update, and keep practical guides available for everyone.Timeouts, diagnostics, and failure fixes
Timeout: the element never becomes visible
- Confirm the locator matches the intended element and is not matching zero or multiple nodes.
- Check whether the element is inside an iframe; use
frameLocator()for frame content. - Determine whether the application intentionally keeps it hidden until another action, permission, or network response occurs.
- Use tracing, screenshots, and the Playwright Inspector to see the DOM and overlays at failure time.
The locator is attached but still cannot be clicked
Attachment is weaker than actionability. Replace an attached wait with the action itself, or use a visibility assertion followed by click(). Investigate overlays, animations, disabled state, and moving layout rather than adding a delay.
Strict-mode violation
The locator matches multiple elements. Narrow it with a role name, label, filter({ hasText }), or a scoped parent. Do not use first() merely to silence the error unless the first match is genuinely the product behavior you intend to test.
Quick wins for a faster PC:
Repair Windows errors before they cause bigger problemsFix Now →Scan for outdated or missing drivers - takes under a minuteDriver Scan →The test passes locally but times out in CI
Use condition-based waits, preserve the trace on failure, and verify that the CI browser and installed Playwright version match the project configuration. Increase a targeted assertion timeout only when the application’s documented behavior requires it; a global increase can conceal a real regression.
The element disappears during a read
Keep the locator rather than caching a stale element handle. A locator re-resolves after a re-render. If a transient message is expected, assert its text or visibility while it exists instead of reading it after a separate delay.
A practical decision checklist
- Ask whether the next operation is a normal user action. If so, call the locator action and let Playwright auto-wait.
- Ask whether the test must verify an eventual state. Use
expect(locator)with a web-first assertion. - If the test must synchronize on attachment, visibility, hidden, or detachment without making that state the assertion, use
locator.waitFor({ state }). - Choose a resilient, user-facing locator and narrow it to one intended target.
- Investigate actionability, frames, overlays, and application state before adding a timeout or fixed delay.
Or skip the browser setup
If your goal is to capture a page after it reaches the right state rather than interact with it in a Playwright test, ScreenshotNeo provides a single HTTP request. It accepts cookie and consent banners before capture and removes more than 60 known consent platforms, newsletter popups, and chat widgets. Bot checks, blank pages, timeouts, failed loads, and cache hits are not billed, and each response reports the page verdict and billing status in headers. Its MCP server lets Claude, Cursor, and other MCP clients call take_screenshot, get_page_info, and capture_pdf.
curl -G "https://api.screenshotneo.com/v1/shot"
-d access_key=YOUR_API_KEY
--data-urlencode url=https://stripe.com
-o shot.webp
See the ScreenshotNeo documentation for options such as full-page capture, selector-based element shots, waits, custom CSS and JavaScript, device presets, PDFs, headers, cookies, geolocation, caching, signed links, asynchronous jobs, and bulk capture. The free plan includes 1,000 screenshots per month with no card; paid plans start at $5 for 3,000 shots. Create a free ScreenshotNeo account.
Recommended Free Tools
Frequently Asked Questions
Does waitFor({ state: 'visible' }) wait for an element to be enabled?
No. It checks visibility only. Actions such as click() separately wait for actionability, including enabled state and the ability to receive events.
Can I wait for a locator inside an iframe?
Create a frame locator for the iframe and then locate and wait for the element within that frame, for example page.frameLocator('iframe').getByRole('button').waitFor().
Which timeout controls a locator wait?
The call accepts its own timeout; when it is zero, Playwright uses the configured timeout behavior. Assertion timeouts are configured separately from action timeouts.
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.

