Playwright Wait for Navigation: Methods and Examples
Use page.waitForURL() or a web-first URL assertion for action-triggered navigation. See runnable examples, timeout guidance, troubleshooting, and the deprecated method’s replacement.
For current Playwright code, use page.waitForURL() when an action should change the main page URL. In Playwright Test, await expect(page).toHaveURL(...) is often the clearest way to assert the destination. For direct navigation to a known URL, use page.goto(). The older page.waitForNavigation() is deprecated and documented as inherently racy; Playwright recommends page.waitForURL() instead. Playwright Page API
Choose the right navigation wait
| Situation | Use | Why |
|---|---|---|
| Open a known URL directly | page.goto(url) |
Explicit navigation with lifecycle options. |
| A click or submit should change the main page URL | page.waitForURL(pattern) |
Waits for the intended URL transition. |
| A Playwright Test should verify the destination | expect(page).toHaveURL(pattern) |
A web-first assertion retries until the expected state or timeout. |
| A frame’s URL should change | frame.waitForURL(pattern) |
Waits for that frame rather than the main page. |
| The page URL is correct but the app may not be ready | Assert a visible element or other required state | A URL transition alone does not prove the interface is ready. |
Playwright automatically waits for actionability before actions such as clicks, and its web-first assertions retry. Add a separate wait only when it expresses a real expected event or outcome. Writing tests
Wait for a URL change after clicking
Start the URL wait before the action. This ensures the wait is listening before the click can navigate.
import { test, expect } from '@playwright/test';
test('continue to the target page', async ({ page }) => {
await page.goto('https://example.com/start');
const destination = page.waitForURL('**/target.html');
await page.getByRole('link', { name: 'Continue' }).click();
await destination;
await expect(page).toHaveURL('**/target.html');
});
The final assertion is optional if awaiting destination is sufficient. Keeping the assertion can make the intended outcome explicit in a test.
Use a web-first URL assertion
When the goal is simply to verify the post-click URL, the assertion can stand on its own:
import { test, expect } from '@playwright/test';
test('continue to the target page', async ({ page }) => {
await page.goto('https://example.com/start');
await page.getByRole('link', { name: 'Continue' }).click();
await expect(page).toHaveURL('**/target.html');
});
Playwright’s assertions wait for the expected state, so there is no need to add a fixed sleep before the assertion. The writing tests guide
Match the destination carefully
page.waitForURL() accepts a string glob, regular expression, URL pattern, or predicate. A string without wildcard characters is an exact URL match. Use a pattern specific enough that an unrelated route cannot satisfy it.
// Glob pattern
await page.waitForURL('**/orders/complete');
// Regular expression
await page.waitForURL(/\/orders\/complete(?:\?.*)?$/);
// Predicate over the URL object
await page.waitForURL(url =>
url.pathname === '/orders/complete' && url.searchParams.get('paid') === 'true'
);
Navigate directly with page.goto()
When the destination is known and no user action causes the navigation, call page.goto() directly:
import { test, expect } from '@playwright/test';
test('open the account page', async ({ page }) => {
await page.goto('https://example.com/account');
await expect(page.getByRole('heading', { name: 'Account' })).toBeVisible();
});
By default, page.goto() waits for the load lifecycle state. Its waitUntil option can be 'commit', 'domcontentloaded', or 'load'. Choose based on the document event you need, then assert the application state the test depends on. Page API
Wait for the application state, not network silence
A URL change, document load event, and application readiness are different signals. A page can continue making requests after the relevant interface is usable, while a quiet network does not prove that the expected content appeared.
Playwright documents networkidle as discouraged for testing. Prefer a user-visible assertion that matches the requirement, such as a heading, confirmation message, or enabled control.
await page.getByRole('button', { name: 'Submit order' }).click();
await expect(page.getByRole('heading', { name: 'Order confirmed' })).toBeVisible();
Use waitUntil to choose a navigation lifecycle point where necessary; use a web assertion to establish that the page is ready for the test’s next step. Lifecycle and load-state guidance
Wait for a frame URL
When an embedded frame navigates, wait on the frame rather than the main page. The Frame API provides frame.waitForURL(), with the same URL matching options.
const frame = page.frame({ name: 'checkout' });
if (!frame) throw new Error('Checkout frame was not found');
const destination = frame.waitForURL('**/confirmation');
await frame.getByRole('button', { name: 'Pay' }).click();
await destination;
Frame-specific selectors depend on the frame being present and correctly identified. frame.waitForNavigation() is also deprecated; use the URL wait for a frame URL transition. Frame API
What happened to page.waitForNavigation()?
page.waitForNavigation() waited for main-frame navigation and returned the main resource response. The documentation notes that History API URL changes count as navigation; anchor or History API navigation can resolve with null, while redirects resolve with the final non-redirect response.
Playwright’s Page API says: “This method is inherently racy, please use page.waitForURL() instead.” Migrate new code to page.waitForURL() or a web-first URL assertion. The Frame API makes the same recommendation for frame.waitForNavigation(). Page API deprecation note · Frame API
Configure navigation timeouts
Set a timeout that reflects the environment, and use a per-call timeout when one navigation has a different expected duration. A longer timeout will not fix a wrong URL pattern or an unsuitable readiness condition.
// playwright.config.ts
import { defineConfig } from '@playwright/test';
export default defineConfig({
use: {
navigationTimeout: 30_000,
},
});
await page.goto('https://example.com', { timeout: 30_000 });
await page.waitForURL('**/complete', { timeout: 15_000 });
page.setDefaultNavigationTimeout() applies to navigation methods including goto(), reload(), goBack(), goForward(), setContent(), and waitForURL(). It takes priority over general default timeout settings. Page API timeout reference · Playwright timeouts
Common errors and fixes
| Symptom | Likely cause | Fix |
|---|---|---|
waitForURL times out |
The action did not navigate, the route differs from the pattern, or navigation was blocked. | Check the resulting URL and action outcome. Make the pattern match the actual destination, and assert a visible error or success state when no navigation is expected. |
| The wait misses a fast navigation | The action ran before the separate wait was registered. | Create the waitForURL() promise first, trigger the action second, then await the promise. |
| The test passes URL waiting but fails on the next interaction | The URL changed before the required UI was ready. | Follow the URL wait with a web-first assertion for the specific element or state. |
The test hangs waiting for networkidle |
Long polling, analytics, or other ongoing requests prevent network silence. | Wait for the required visible state instead of network inactivity. |
| Exact string URL does not match | The actual URL includes a trailing slash, query string, hash, or redirect destination not present in the expected string. | Inspect the actual URL and choose an exact match, glob, regular expression, URL pattern, or predicate that captures the intended destination. |
| Navigation timeout remains after increasing it | The expected condition may be wrong, or the page never reaches the selected lifecycle event. | Confirm the event and route the test truly needs. Increase the timeout only for a legitimately slower environment. |
| Frame wait never resolves | The frame lookup returned no frame, or the frame navigates to a different URL than expected. | Verify the frame name or selector, ensure it exists, and inspect its URL after the action. |
Performance, reliability, and cost considerations
- Use the narrowest meaningful wait. Waiting for a destination URL and then the required element avoids coupling the test to every resource on the page.
- Avoid fixed sleeps. Playwright discourages
waitForTimeout()in production tests because timer-based tests are flaky. An explicit assertion responds as soon as the expected state is reached. - Keep patterns stable. Match a meaningful path or condition rather than volatile query parameters unless they are part of the requirement.
- Diagnose before extending timeouts. A generous timeout can reduce false failures in slow environments, but can also make real failures take longer to report.
- Cost. Playwright navigation waits have no separate per-call price in the cited API guidance; account for the compute and runtime of the environment executing the browser tests.
Or skip the browser setup
If the task is to capture a page image or PDF rather than test its navigation behavior, ScreenshotNeo provides a website screenshot API and MCP server. A single GET request returns a PNG, JPEG, WebP, or PDF. 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}`);
if (!res.ok) throw new Error(`Screenshot request failed: ${res.status}`);
await Bun.write('shot.webp', res);
ScreenshotNeo accepts cookie and consent banners like a visitor and removes more than 60 known consent platforms, newsletter popups, and chat widgets before capture; each step can be turned off. Bot checks and CAPTCHAs, blank pages, timeouts, failed loads, and cache hits cost nothing, with the outcome shown in X-Page-Verdict and X-Billed response headers. Its MCP server gives AI agents tools for screenshots, page information, and PDF capture. The free plan includes 1,000 screenshots a month with no card; paid plans start at $5 for 3,000.
Sign up free for 1,000 screenshots a month, with no card required.
FAQ
Should I use waitForNavigation or waitForURL?
Use waitForURL() for an expected URL transition. The older navigation wait is deprecated and documented as inherently racy.
Does waitForURL wait for every image and script?
It waits for the URL condition. Assert the application state your test needs separately.
Can waitForURL detect a History API route change?
Use it to wait for the resulting URL. The deprecated navigation method’s documentation specifically notes History API changes as navigation; new code should follow the documented URL-wait recommendation.
When should I use page.goto()?
Use it to navigate directly to a known starting or destination URL. Use a URL wait or assertion when an action causes the transition.


