How to Wait in Playwright for Java
Learn Playwright Java’s auto-waiting model, locator waits, assertions, navigation signals, timeouts, dynamic lists, and fixes for flaky tests.
Playwright for Java usually waits for you. Actions such as click() automatically wait until the locator resolves to one element that is visible, stable, able to receive events, and enabled. Add an explicit wait only when the condition you need is not covered by an action or assertion.
For an element-state wait, use a locator:
Locator orderSent = page.locator("#order-sent");
orderSent.waitFor(new Locator.WaitForOptions().setState(WaitForSelectorState.VISIBLE));
This guide explains which wait to choose, how timeouts work, how to wait for navigation and dynamic content, and why fixed sleeps and unconditional networkidle waits make tests slower or flaky.
1. Set up a runnable Playwright Java test
Add the Playwright Java dependency, install the browsers, then run a JUnit test. The current installation commands and API signatures are documented in the Playwright Java documentation.
<dependency>
<groupId>com.microsoft.playwright</groupId>
<artifactId>playwright</artifactId>
<version>1.52.0</version>
</dependency>
mvn exec:java -e -Dexec.mainClass=com.microsoft.playwright.CLI \
-Dexec.args="install"
import com.microsoft.playwright.*;
import com.microsoft.playwright.options.WaitForSelectorState;
public class WaitExample {
public static void main(String[] args) {
try (Playwright playwright = Playwright.create()) {
Browser browser = playwright.chromium().launch(
new BrowserType.LaunchOptions().setHeadless(true));
Page page = browser.newPage();
page.navigate("https://example.com");
Locator heading = page.getByRole(
AriaRole.HEADING,
new Page.GetByRoleOptions().setName("Example Domain"));
heading.waitFor(new Locator.WaitForOptions()
.setState(WaitForSelectorState.VISIBLE));
System.out.println(heading.textContent());
browser.close();
}
}
}
2. Prefer auto-waiting actions
An action is normally the best synchronization point. Playwright re-resolves the locator and checks actionability before acting. If the button is still rendering, covered by an animation, disabled, or attached to more than one element, click() waits until the checks pass or the operation timeout expires.
page.getByRole(
AriaRole.BUTTON,
new Page.GetByRoleOptions().setName("Save"))
.click();
Use user-facing locators such as getByRole, getByLabel, getByText, or a stable test ID. Avoid adding a sleep before every action; a sleep does not prove that the element is actionable.
3. Wait for a specific element state
Locator.waitFor supports four states: ATTACHED, DETACHED, VISIBLE, and HIDDEN. The default is VISIBLE. An element is visible when it has a non-empty bounding box and is not rendered with visibility:hidden. Hidden includes an element that is detached.
import com.microsoft.playwright.options.WaitForSelectorState;
Locator result = page.locator("#order-sent");
result.waitFor(new Locator.WaitForOptions()
.setState(WaitForSelectorState.VISIBLE));
page.locator(".loading-spinner").waitFor(new Locator.WaitForOptions()
.setState(WaitForSelectorState.HIDDEN));
page.locator("#dialog").waitFor(new Locator.WaitForOptions()
.setState(WaitForSelectorState.ATTACHED));
page.locator("#old-panel").waitFor(new Locator.WaitForOptions()
.setState(WaitForSelectorState.DETACHED));
Choose the state that matches the next operation. ATTACHED only means that the node exists; it may still be invisible or disabled. VISIBLE is appropriate before reading what a user can see. DETACHED is useful when a component is removed rather than merely hidden.
4. Use web-first assertions for expected outcomes
When the intent is to verify what a user should observe, use a retrying assertion. Playwright repeatedly fetches the locator and checks the condition until it passes or the assertion timeout expires.
import static com.microsoft.playwright.assertions.PlaywrightAssertions.assertThat;
assertThat(page.getByTestId("status")).hasText("Submitted");
assertThat(page.getByRole(
AriaRole.BUTTON,
new Page.GetByRoleOptions().setName("Save")))
.isEnabled();
The documented default assertion timeout is five seconds. Set it globally or for an individual assertion when the application has a known slower path:
import static com.microsoft.playwright.assertions.PlaywrightAssertions.assertThat;
import com.microsoft.playwright.assertions.PlaywrightAssertions;
PlaywrightAssertions.setDefaultAssertionTimeout(10_000);
assertThat(page.getByTestId("status")).hasText("Processed");
A direct read such as textContent() returns one snapshot. If the value is populated asynchronously, assert it instead of reading once and asserting later.
5. Wait for navigation after a click
For a click that triggers navigation, wait for the URL or assert a page condition that proves navigation completed. waitForURL accepts a glob, regular expression, or URL predicate and can finish at COMMIT, DOMCONTENTLOADED, LOAD, or NETWORKIDLE. The default is LOAD.
page.getByRole(
AriaRole.LINK,
new Page.GetByRoleOptions().setName("Account"))
.click();
page.waitForURL("**/account");
assertThat(page.getByRole(
AriaRole.HEADING,
new Page.GetByRoleOptions().setName("Account")))
.isVisible();
For a form submit or single-page application route, the URL may not change. In that case, wait for the response or assert the resulting status, heading, or success message.
page.getByRole(
AriaRole.BUTTON,
new Page.GetByRoleOptions().setName("Submit"))
.click();
assertThat(page.getByTestId("success-message")).isVisible();
6. Understand load states
page.waitForLoadState() waits for LOAD by default. You can request DOMCONTENTLOADED or NETWORKIDLE. Network idle means no network connections for at least 500 ms, but Playwright labels it discouraged for test readiness because analytics, polling, advertisements, and websockets can keep a page busy.
page.waitForLoadState(); // LOAD
page.waitForLoadState(LoadState.DOMCONTENTLOADED);
Most actions already wait for the conditions they need, so an unconditional load-state wait after every navigation adds time without making the test safer. Prefer a user-visible assertion or a specific response.
7. Dynamic lists and re-rendering
locator.all() returns immediately; it does not wait for a list to finish populating. First wait for a stable count or completion signal.
Locator rows = page.locator("[data-testid='result-row']");
assertThat(rows).hasCount(10);
for (Locator row : rows.all()) {
System.out.println(row.innerText());
}
Locators are re-resolved on each operation, so they tolerate many framework re-renders better than storing an element handle. If the list has a variable size, wait for a visible “Loaded” marker or a specific row rather than guessing a delay.
8. Wait for a custom browser condition
For a condition that is not one of the built-in states, use Locator.waitForFunction. Playwright retries the browser expression and re-resolves the locator on each attempt.
Locator chart = page.locator("#sales-chart");
chart.waitForFunction(
"element => element.dataset.ready === 'true'");
Keep the predicate deterministic and cheap. A predicate that never becomes true will consume the full timeout and produce the same failure as a wrong selector.
9. Configure timeouts at the narrowest useful scope
| Operation | Documented default | Typical setting |
|---|---|---|
| Action and locator operation | 30 seconds | Use the page/context default; override a known slow call |
| Assertion | 5 seconds | Increase for slow back-end workflows |
waitForURL and waitForLoadState |
30 seconds | Set per call when a route has a clear SLA |
BrowserContext context = browser.newContext(
new Browser.NewContextOptions().setTimeout(15_000));
Page page = context.newPage();
page.setDefaultTimeout(10_000);
page.setDefaultNavigationTimeout(20_000);
page.locator("#slow-widget").waitFor(
new Locator.WaitForOptions().setTimeout(45_000));
A longer timeout should represent a legitimate slow path, not hide a wrong locator or a page readiness bug. Diagnose the selector, expected state, and navigation trigger before increasing it.
10. Troubleshoot Playwright Java wait timeouts
“Timeout 30000ms exceeded” on click
- Cause: the locator is ambiguous, hidden, covered, disabled, or never attached.
- Fix: use
getByRoleor a test ID, verify uniqueness withassertThat(locator).hasCount(1), and inspect the trace or screenshot.
The selector exists but VISIBLE never succeeds
- Cause: the node has zero dimensions, is inside a closed state, or uses
visibility:hidden. - Fix: wait for the user-facing container, wait for the correct component state, or use
ATTACHEDonly when visibility is not required.
The test waits forever for NETWORKIDLE
- Cause: polling, analytics, ads, or a websocket keeps creating requests.
- Fix: wait for a specific response or assert the visible result.
all() returns too few rows
- Cause:
all()does not wait for a dynamic list. - Fix: assert a count or completion marker first.
The URL wait passes but the page is not ready
- Cause: routing committed before the application rendered its content.
- Fix: combine
waitForURLwith an assertion for the heading, form, or status the user needs.
A fixed sleep makes tests flaky
- Cause: the chosen duration is shorter than some runs and longer than others.
- Fix: replace it with an action, locator state, assertion, URL, response, or custom predicate.
11. Performance and reliability checklist
- Use one meaningful readiness condition instead of several unconditional waits.
- Prefer locators over element handles so re-rendered nodes are found again.
- Use role, label, text, and stable test IDs before brittle CSS or XPath.
- Keep default timeouts conservative and override only known slow operations.
- Capture a trace, console log, and screenshot on timeout to identify the failing state.
- Use assertions to document the user-visible outcome, not implementation timing.
- Wait for a stable count before iterating dynamic collections.
12. Or skip the browser setup
If your goal is a static screenshot or PDF rather than an interaction test, ScreenshotNeo provides a single request that waits for the page capture workflow and returns PNG, JPEG, WebP, or PDF. Cookie and consent 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 the response identifies the verdict and billing with X-Page-Verdict and X-Billed headers.
See the full parameter list in 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}`);
const buffer = Buffer.from(await res.arrayBuffer());
await import('node:fs/promises').then(fs => fs.writeFile('shot.webp', buffer));
ScreenshotNeo also has an MCP server with take_screenshot, get_page_info, and capture_pdf for Claude, Cursor, and other MCP clients. The Free plan includes 1,000 screenshots per month with no card; paid plans start at $5 for 3,000. Create a free ScreenshotNeo account.
13. FAQ
Should I use waitForSelector or Locator.waitFor?
Use Locator.waitFor for new code. The Page API’s waitForSelector remains supported but is discouraged because the condition is separated from the locator workflow.
What is the default state for Locator.waitFor?
VISIBLE. Specify ATTACHED, DETACHED, or HIDDEN when that is the actual condition.
When should I use waitForFunction?
Use it for a custom readiness rule that cannot be expressed as an element state, assertion, URL, or response.
Can I set one timeout for every wait?
You can set page or context defaults, then override individual calls. Keeping overrides local makes slow paths visible and prevents unrelated waits from becoming unnecessarily long.


