Wait for a URL in Playwright
Use Playwright’s page.waitForURL() to synchronize navigation safely, match dynamic URLs, handle frames, and avoid flaky navigation races.
page.waitForURL() waits for the main frame to navigate to a URL that matches your pattern. Start the wait before the click or action that causes navigation, then choose an exact URL, glob, regular expression, URLPattern, or predicate that describes the destination.
For child frames, use frame.waitForURL(). For a test assertion rather than synchronization, use expect(page).toHaveURL(). Avoid page.waitForNavigation(); Playwright documents it as deprecated and inherently racy.
Basic usage
import { test, expect } from '@playwright/test';
test('opens the account page', async ({ page }) => {
await page.goto('https://example.com');
await page.getByRole('link', { name: 'Account' }).click();
await page.waitForURL('https://example.com/account');
await expect(page).toHaveTitle(/Account/);
});
A plain string without wildcards is an exact URL match. The wait resolves when the main frame reaches the matching URL. URL arrival does not prove that every element on the destination page is ready, so follow it with a web assertion for the UI you need.
Start the wait before the action
Navigation can complete very quickly. Register the wait and perform the action together so the event cannot be missed.
await Promise.all([
page.waitForURL('**/dashboard'),
page.getByRole('button', { name: 'Continue' }).click(),
]);
This pattern also works with links, form submissions, keyboard actions, and JavaScript that triggers a redirect.
URL matching options
Exact URL
await page.waitForURL('https://example.com/account');
Use exact matching when the origin, path, and query string are stable.
Glob patterns
await page.waitForURL('**/login');
await page.waitForURL('https://example.com/orders/**');
Globs are useful when the host, route prefix, or trailing path varies. Keep the pattern specific enough to avoid accepting an unintended destination.
Regular expressions
await page.waitForURL(/\/orders\/\d+$/);
This matches an order route whose final path segment is one or more digits. Anchor the expression when query strings or extra path segments should not be accepted.
URLPattern
await page.waitForURL(new URLPattern({ pathname: '/projects/:id' }));
URLPattern expresses structured path patterns without manually parsing a string.
Predicate functions
await page.waitForURL(url =>
url.pathname === '/search' && url.searchParams.has('q')
);
A predicate receives a URL object. Use it when the destination depends on query parameters, hashes, or several conditions.
Lifecycle options
waitForURL accepts a waitUntil lifecycle value:
| Value | Meaning | When to use |
|---|---|---|
commit |
Response is committed and the document starts loading. | When you only need navigation to begin. |
domcontentloaded |
The initial HTML has been parsed. | When early DOM access is sufficient. |
load |
The load event has fired. | For the usual page-ready boundary. |
networkidle |
No network connections for at least 500 ms. | Generally avoid in tests; web assertions are more reliable. |
await page.waitForURL('**/reports', {
waitUntil: 'domcontentloaded',
timeout: 15_000,
});
The timeout is in milliseconds. Set it per wait when a particular route is slower, or configure a project-wide default in Playwright. A longer timeout should reflect a known slow dependency, not hide a broken navigation.
Waiting after common actions
Clicking a link
await Promise.all([
page.waitForURL('**/pricing'),
page.getByRole('link', { name: 'Pricing' }).click(),
]);
Submitting a form
await page.getByLabel('Email').fill('dev@example.com');
await page.getByLabel('Password').fill('correct-horse-battery-staple');
await Promise.all([
page.waitForURL('**/dashboard'),
page.getByRole('button', { name: 'Sign in' }).click(),
]);
Navigation caused by JavaScript
await Promise.all([
page.waitForURL(url => url.pathname === '/complete'),
page.evaluate(() => window.location.assign('/complete')),
]);
Redirect chains
Match the final URL when an action passes through authentication or tracking redirects. If intermediate URLs matter, wait for each one explicitly or observe the request/response separately.
Waiting for a URL in an iframe
Find the child frame, then call its URL wait. A page-level wait does not observe navigation inside an iframe.
const frame = page.frame({ name: 'checkout' });
if (!frame) throw new Error('checkout frame was not found');
await Promise.all([
frame.waitForURL('**/embedded/complete'),
frame.getByRole('button', { name: 'Pay' }).click(),
]);
For frames created dynamically, wait for the frame to appear before using it, or use a locator inside the frame and then obtain the frame with locator.contentFrame().
Assertions with toHaveURL
Use expect(page).toHaveURL() when the purpose is to verify the final URL in a test. It retries until the assertion timeout and supports the same style of exact, glob, regular-expression, URLPattern, and predicate matching.
import { test, expect } from '@playwright/test';
test('redirects unauthenticated users', async ({ page }) => {
await page.goto('https://example.com/private');
await expect(page).toHaveURL('**/login');
});
test('keeps the search query', async ({ page }) => {
await page.goto('https://example.com');
await page.getByRole('textbox', { name: 'Search' }).fill('playwright');
await page.getByRole('button', { name: 'Search' }).click();
await expect(page).toHaveURL(url =>
url.pathname === '/search' && url.searchParams.get('q') === 'playwright'
);
});
Use a wait to synchronize the next operation with navigation; use an assertion to state what the test must prove. You can use both when navigation and page content are separate requirements.
Complete runnable examples
Node.js with Playwright Test
import { test, expect } from '@playwright/test';
test('waits for a dynamic order URL', async ({ page }) => {
await page.goto('https://example.com/orders');
await Promise.all([
page.waitForURL(/\/orders\/\d+$/),
page.getByRole('link', { name: 'Latest order' }).click(),
]);
await expect(page.getByRole('heading', { name: /Order/ })).toBeVisible();
});
Node.js script
import { chromium } from 'playwright';
const browser = await chromium.launch();
const page = await browser.newPage();
await page.goto('https://example.com');
await Promise.all([
page.waitForURL('**/account'),
page.getByRole('link', { name: 'Account' }).click(),
]);
console.log(page.url());
await browser.close();
Python
from playwright.sync_api import sync_playwright
with sync_playwright() as p:
browser = p.chromium.launch()
page = browser.new_page()
page.goto('https://example.com')
with page.expect_navigation():
page.get_by_role('link', name='Account').click()
# Prefer wait_for_url for URL-based synchronization:
page.wait_for_url('**/account')
print(page.url)
browser.close()
For new Python code, use the language binding’s page.wait_for_url() equivalent directly around the action. The same matching concepts apply: exact strings, globs, regular expressions, and predicates.
cURL
cURL cannot wait for a browser’s client-side navigation. It can request a URL and inspect redirects, but it does not replace Playwright’s page and frame synchronization.
curl -I -L https://example.com/account
Choosing the right readiness signal
| Goal | Recommended API |
|---|---|
| Wait for the main frame to reach a destination | page.waitForURL() |
| Wait for a child frame to reach a destination | frame.waitForURL() |
| Assert the final URL in a test | expect(page).toHaveURL() |
| Verify a heading, button, or data row is ready | A locator web assertion such as toBeVisible() or toHaveText() |
| Wait for network activity to stop | Prefer a specific web assertion; avoid relying on networkidle for tests. |
Common errors and fixes
Timeout exceeded
Cause: the action did not navigate, the matcher does not describe the final URL, the page is still on an intermediate redirect, or the route is slower than the timeout.
Fix: log page.url(), inspect the browser trace, verify the click target, and match the actual final URL. Increase the timeout only when the slower route is expected.
The wait misses a fast navigation
Cause: the action ran before waitForURL was registered.
Fix: use Promise.all with the wait first, as shown above.
Exact matching fails because of a query string or hash
Cause: the actual URL contains extra parameters or a fragment.
Fix: use a glob, regular expression, URLPattern, or predicate that checks only the parts that matter.
The page URL changes but the test still fails
Cause: the expected URL belongs to an iframe, or the page changes history state without a full navigation.
Fix: use frame.waitForURL() for a child frame. For client-side route changes, wait for the URL pattern and then assert a destination-specific locator.
networkidle never arrives
Cause: analytics, WebSockets, polling, or other background requests keep the network active.
Fix: use domcontentloaded, load, or a web assertion for the element that signals readiness.
Multiple pages or popups
Cause: the click opens a new tab, so the original page never reaches the expected URL.
Fix: wait for the new page and its URL together.
const popupPromise = page.waitForEvent('popup');
await page.getByRole('link', { name: 'Open report' }).click();
const popup = await popupPromise;
await popup.waitForURL('**/report');
Reliability and performance practices
- Match the stable part of the route, but keep patterns narrow enough to catch wrong redirects.
- Register URL waits before actions that can navigate.
- Use role and label locators so the action itself is deterministic.
- After URL synchronization, assert the page state your test actually needs.
- Keep the default timeout conservative and set a larger timeout only for known slow environments.
- Capture traces or console logs when diagnosing intermittent redirects.
- Do not use arbitrary sleeps to replace navigation synchronization.
Or skip the browser setup
If your goal is to collect a stable image of a destination page rather than drive an interactive browser, ScreenshotNeo provides a single screenshot request. It accepts consent banners before capture and removes more than 60 known consent platforms, newsletter popups, and chat widgets; each step can be disabled. Bot checks, blank pages, timeouts, failed loads, and cache hits are not billed, and the response identifies the result with X-Page-Verdict and X-Billed headers. It also provides an MCP server with take_screenshot, get_page_info, and capture_pdf tools for AI clients.
See the ScreenshotNeo API documentation for all options.
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}`);
The free plan includes 1,000 screenshots each month with no card. Paid plans start at $5 for 3,000 shots, and every feature is available on every plan. Create a free ScreenshotNeo account.
FAQ
Is waitForURL an assertion?
No. It synchronizes with navigation. Use expect(page).toHaveURL() when you need a test assertion.
Can I wait for a URL without clicking?
Yes. Start page.waitForURL() before any action that may change the route, including evaluated JavaScript or a form submission.
How do I ignore changing query parameters?
Use a glob, regular expression, URLPattern, or predicate that checks only the pathname and the parameters relevant to the test.
Why should I avoid waitForNavigation?
Playwright documents it as deprecated and inherently racy. URL-based synchronization should use waitForURL().


