How to Wait for a Condition in Playwright
Learn when to use Playwright assertions, locator state waits, predicates, and load states—with runnable examples and timeout fixes.
Use the narrowest wait that describes the condition you need. For a user-visible result, use a retrying web assertion such as await expect(locator).toHaveText('Ready'). For a standard locator state, use await locator.waitFor({ state: 'visible' }). For a custom predicate, use locator.waitForFunction() or page.waitForFunction(). Use page.waitForLoadState() only when you need a navigation lifecycle event.
Playwright retries web assertions until they pass or the assertion timeout expires. The documented default assertion timeout in Playwright Test is 5 seconds and can be changed in configuration. See the official assertions guide.
Choose the right Playwright wait
| What you need to observe | Use | Example |
|---|---|---|
| A user-visible result your test verifies | Web-first assertion | await expect(status).toHaveText('Submitted') |
| Attached, detached, visible, or hidden state | locator.waitFor() |
await dialog.waitFor({ state: 'visible' }) |
| A condition involving one element’s properties | locator.waitForFunction() |
await item.waitForFunction(el => el.textContent === 'Ready') |
| A page-level JavaScript condition | page.waitForFunction() |
await page.waitForFunction(() => window.appState?.ready) |
| A navigation lifecycle event | page.waitForLoadState() |
await page.waitForLoadState('networkidle') |
Locator states supported by locator.waitFor() are attached, detached, visible, and hidden; visible is the default. If the requested state already holds, the call returns immediately. See the Locator API reference.
1. Wait for an expected UI result with an assertion
Use an assertion when the condition is part of what the test is checking. The assertion both waits and reports a meaningful failure.
import { test, expect } from '@playwright/test';
test('waits for the submitted status', async ({ page }) => {
await page.goto('https://example.com/form');
await page.getByRole('button', { name: 'Submit' }).click();
await expect(page.getByTestId('status')).toHaveText('Submitted');
});
Other useful retrying assertions include toBeVisible(), toBeHidden(), toHaveAttribute(), toHaveValue(), toContainText(), and toHaveCount(). Prefer these over manually polling the DOM.
Set an assertion timeout
Keep the timeout long enough for the real operation, but do not hide a broken condition with an excessive value.
await expect(page.getByTestId('status')).toHaveText('Submitted', {
timeout: 15_000
});
Set project-wide defaults in playwright.config.ts when the same policy applies across tests:
import { defineConfig } from '@playwright/test';
export default defineConfig({
expect: {
timeout: 10_000
}
});
2. Wait for a locator state
Use locator.waitFor() when the condition is one of Playwright’s standard element states.
const dialog = page.getByRole('dialog');
await dialog.waitFor({ state: 'visible' });
const spinner = page.getByTestId('spinner');
await spinner.waitFor({ state: 'hidden' });
const result = page.getByTestId('result');
await result.waitFor({ state: 'attached' });
Use detached when the node must be removed from the DOM. Use hidden when it may remain attached but must not be visible.
3. Wait for a custom element condition
When no standard assertion expresses the requirement, wait for a predicate tied to a locator. Locator predicates re-resolve the locator during retries, which helps when a framework replaces the element during rendering.
const status = page.getByTestId('status');
await status.waitForFunction(element => {
return element.textContent?.trim() === 'Ready';
});
locator.waitForFunction() was added in Playwright v1.62. Check the Playwright version installed in your project before using it.
You can pass arguments to the predicate:
const progress = page.getByRole('progressbar');
await progress.waitForFunction(
(element, minimum) => Number(element.getAttribute('aria-valuenow')) >= minimum,
100
);
4. Wait for a page-level condition
Use page.waitForFunction() when the condition is not owned by one element, such as application state or a global value.
await page.waitForFunction(() => window.appState?.ready === true);
The function runs in the page context and resolves when it returns a truthy value. Keep the predicate deterministic and cheap because it may run repeatedly.
5. Wait for navigation load states only when needed
page.waitForLoadState() waits for a navigation event. The default state is load; other supported states include domcontentloaded and networkidle. The navigation must already have been committed, and the method resolves immediately if the requested state has already happened.
await page.getByRole('link', { name: 'Reports' }).click();
await page.waitForLoadState('domcontentloaded');
For application readiness, a specific assertion is usually better than network quiet:
await expect(page.getByTestId('report-ready')).toBeVisible();
Playwright notes that load-state waits are often unnecessary because actions auto-wait for their own requirements. Avoid adding one after every click or navigation.
6. Understand auto-waiting around actions
Actions such as click() already wait for actionability. For a click, Playwright checks that the locator is unique, visible, stable, able to receive events, and enabled. These checks make the action safe to perform; they do not prove that the application produced the expected result afterward. Follow the action with an assertion.
await page.getByRole('button', { name: 'Save' }).click();
await expect(page.getByRole('status')).toHaveText('Saved');
Do not replace a result assertion with a fixed delay. waitForTimeout() can be too short on a slow run and wastes time on a fast run. The auto-waiting documentation describes the actionability checks.
7. Complete runnable example
import { test, expect } from '@playwright/test';
test('waits for an asynchronous order update', async ({ page }) => {
await page.goto('https://example.com/orders/123');
const refresh = page.getByRole('button', { name: 'Refresh' });
const status = page.getByTestId('order-status');
await refresh.click();
await expect(status).toHaveText('Processing');
await expect(status).toHaveText('Complete', { timeout: 30_000 });
const receipt = page.getByRole('link', { name: 'Download receipt' });
await receipt.waitFor({ state: 'visible' });
});
8. Common errors and fixes
Timeout exceeded
Cause: The condition never became true, the operation is slower than the timeout, or the locator is wrong.
Fix: Confirm the locator identifies the intended element, verify the preceding click or navigation occurred, and inspect the actual text, attributes, and visibility. Increase the timeout only when the operation legitimately needs more time.
Strict mode violation
Cause: A locator matches multiple elements for an operation that requires one.
Fix: Use a more specific role, label, test id, or chained locator. Avoid selecting by unstable layout details.
Text assertion never matches
Cause: Whitespace, localization, nested text, or a different status string than expected.
Fix: Inspect the rendered text and choose toContainText(), a regular expression, or a normalized expected value when appropriate.
Element is attached but not visible
Cause: The node exists but is hidden by CSS, outside the visible state, or covered by another element.
Fix: Wait for visible instead of attached, then investigate overlays and application state.
Predicate uses stale element data
Cause: The framework re-rendered the element between checks.
Fix: Use a locator predicate so Playwright re-resolves the locator, or use a web assertion.
networkidle never arrives
Cause: Analytics, polling, WebSockets, or other long-lived requests keep the page active.
Fix: Wait for the visible application-ready signal instead of network quiet.
waitForSelector() advice conflicts with current guidance
The Page API marks page.waitForSelector() as discouraged and points users toward locator-based waits and web assertions. Existing code may work, but new code should use the narrower APIs described above. See the Page API reference.
9. Reliability and performance guidance
- Wait on a stable product signal such as a role, test id, status text, or accessibility attribute.
- Keep predicates small and side-effect free.
- Use the shortest timeout that matches the operation, with a deliberate larger timeout for known slow workflows.
- Let actions handle actionability and reserve explicit waits for the result or state you actually need.
- Prefer one meaningful assertion over several arbitrary delays.
- When debugging, capture a trace or screenshot at the failure point and verify whether the action, navigation, and condition each occurred.
10. Or skip the browser setup
If your goal is to capture a page after it is ready, ScreenshotNeo handles the browser session through one API request. It waits for the page capture workflow and returns a PNG, JPEG, WebP, or PDF.
See the ScreenshotNeo API documentation for all options.
cURL
curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://example.com -o shot.webp
Python
import requests
r = requests.get("https://api.screenshotneo.com/v1/shot", params={"access_key": "YOUR_API_KEY", "url": "https://example.com"}, timeout=90)
open("shot.webp", "wb").write(r.content)
Node.js
const q = new URLSearchParams({ access_key: 'YOUR_API_KEY', url: 'https://example.com' });
const res = await fetch(`https://api.screenshotneo.com/v1/shot?${q}`);
Cookie banners, newsletter popups, and chat widgets are removed before the shot. Bot checks, blank pages, failed loads, timeouts, and cache hits are not billed, and response headers identify the page verdict and whether the request was billed. An MCP server lets AI agents such as Claude and Cursor call screenshot tools. The Free plan includes 1,000 screenshots a month with no card; paid plans start at $5 for 3,000 screenshots. Create a free ScreenshotNeo account.
11. FAQ
Is there a Playwright equivalent of Vitest vi.waitUntil?
Yes. Choose a web assertion, locator state wait, locator predicate, or page predicate according to what must become true. The exact API depends on whether the condition is an asserted result, a standard element state, or arbitrary page state.
What is the default Playwright assertion timeout?
Playwright Test documents a 5-second default assertion timeout. Configure it globally or override it for a specific assertion.
Should I wait for load after every navigation?
No. Use a load-state wait only when that navigation event is the condition you need. For most tests, assert on the page’s meaningful ready indicator.
When should I use waitForFunction()?
Use it when a standard state or web assertion cannot express the condition. Keep the predicate focused and use a locator predicate when the condition belongs to one element.


