Use page.waitForURL() for URL-based synchronization in Playwright. Create the wait before the click or other action that can navigate, then pass an exact URL, glob, regular expression, URLPattern, or predicate describing the destination. For an iframe, use frame.waitForURL(). If the URL is the test assertion rather than merely a synchronization step, use expect(page).toHaveURL().
Use page.waitForURL() for the main frame
page.waitForURL() waits for the main frame to navigate to a matching URL. A plain string without wildcards is an exact match, so the path, query string, hash, scheme, host, and trailing slash must match what Playwright observes.
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.getByRole('heading', { name: 'Account' })).toBeVisible();
});
The URL wait answers “did navigation reach this address?” It does not prove that a particular heading, API response, or application state is ready. Add a web assertion for the user-visible condition you actually need.
Choose the URL matcher that fits the destination
| Matcher | Example | Use it when |
|---|---|---|
| Exact string | 'https://example.com/account' |
The complete destination is stable and known. |
| Glob | '**/login' |
The host, scheme, or preceding path can vary but the route is stable. |
| Regular expression | //orders/d+$/ |
A path segment, such as an order ID, is dynamic. |
URLPattern |
new URLPattern({ pathname: '/projects/:id' }) |
You want structured URL-pattern matching. |
| Predicate | url => url.pathname === '/search' && url.searchParams.has('q') |
The condition depends on parsed query parameters or several URL fields. |
// Glob for a dynamic host or prefix
await page.getByRole('link', { name: 'Login' }).click();
await page.waitForURL('**/login');
// Regular expression for a numeric order ID
await page.waitForURL(//orders/d+$/);
// Predicate for a required query parameter
await page.waitForURL(url =>
url.pathname === '/search' && url.searchParams.has('q')
);
// URLPattern for a parameterized path
await page.waitForURL(new URLPattern({ pathname: '/projects/:id' }));
Prefer the narrowest matcher that remains stable. An overly broad glob can let an unintended page satisfy the wait; an exact URL can become fragile when a legitimate query parameter is added.
#1 Best Overall
Start waiting before the action that navigates
Navigation can complete very quickly. Registering the wait after the click can miss that event and leave the test waiting until its timeout. Start both operations together with Promise.all:
await Promise.all([
page.waitForURL('**/dashboard'),
page.getByRole('button', { name: 'Continue' }).click(),
]);
This pattern also works for form submissions, menu selections, and any scripted action that may trigger navigation. Keep the action in the same Promise.all so the listener is installed before the action runs.
Control when the URL wait resolves
The wait supports lifecycle options that determine how far navigation should progress after the matching URL is reached:
Rank #2
waitUntil value |
Meaning | Practical use |
|---|---|---|
commit |
Resolve when the response is committed. | Use when you only need the destination URL as soon as navigation starts. |
domcontentloaded |
Resolve after the document’s DOM is loaded. | A reasonable boundary when the document structure is needed. |
load |
Resolve after the page load event. | Use when resources associated with the load event matter. |
networkidle |
Wait for no network connections for at least 500 ms. | Playwright documentation discourages this for tests; prefer web assertions that describe readiness. |
await Promise.all([
page.waitForURL('**/reports', { waitUntil: 'domcontentloaded' }),
page.getByRole('link', { name: 'Reports' }).click(),
]);
Do not select networkidle merely because a page feels slow. Analytics, polling, advertisements, and long-lived connections can prevent that state. A locator assertion such as await expect(page.getByRole('heading', { name: 'Reports' })).toBeVisible() is a more meaningful readiness check.
Windows Errors? Fix Them Before They Spread
Repair common Windows errors and clear accumulated junk for a smoother, more stable PC - no reinstall needed.Free scan · no reinstallOutdated Drivers Are Slowing You Down
One free scan finds every outdated or missing driver and matches the right update for your exact hardware.Free scan · exact hardware matchWait for a URL inside an iframe
page.waitForURL() observes the main frame. For a child frame, obtain the frame and call its equivalent method:
const frame = page.frameLocator('#checkout').owner();
if (!frame) throw new Error('Checkout frame was not found');
await Promise.all([
frame.waitForURL('**/embedded/complete'),
page.getByRole('button', { name: 'Pay' }).click(),
]);
In real tests, make sure the frame reference is available before starting the wait. If the frame is created dynamically, first wait for the iframe element or use a frame event, then apply frame.waitForURL() to that child frame.
Use expect(page).toHaveURL() when the URL is an assertion
The Playwright test assertion toHaveURL accepts the same broad matcher styles: exact strings, regular expressions, URLPattern, and predicates. It communicates that the URL is part of the test’s expected outcome and integrates with Playwright’s assertion retrying.
await page.getByRole('link', { name: 'Dashboard' }).click();
await expect(page).toHaveURL(//dashboard$/);
await expect(page).toHaveURL(url =>
url.pathname === '/search' && url.searchParams.get('q') === 'playwright'
);
A useful division is: use waitForURL to synchronize a subsequent action, and use toHaveURL to verify the final state. You can use both when a test needs an explicit navigation barrier followed by a separately reported assertion, but avoid duplicating the same check without a reason.
What’s actually slowing this PC down?
Pick the symptom - the matching free tool is one click away.
Why not page.waitForNavigation()?
page.waitForNavigation() is deprecated and documented as inherently racy. Use page.waitForURL() when the destination URL identifies the navigation you need. URL-based waiting also makes the intended destination visible in the test, rather than waiting for any navigation that happens to occur.
Rank #4
If an interaction can trigger multiple navigations, the Playwright navigation guidance recommends explicitly waiting for the specific URL. A broad navigation wait can resolve on an intermediate redirect or an unexpected page.
Complete runnable examples
Playwright Test with TypeScript
import { test, expect } from '@playwright/test';
test('search redirects to a query URL', async ({ page }) => {
await page.goto('https://example.com/search-form');
await Promise.all([
page.waitForURL(url =>
url.pathname === '/search' && url.searchParams.has('q')
),
page.getByRole('button', { name: 'Search' }).click(),
]);
await expect(page).toHaveURL(//search?q=/);
await expect(page.getByRole('heading', { name: 'Search results' })).toBeVisible();
});
Node.js (JavaScript)
const { chromium } = require('playwright');
(async () => {
const browser = await chromium.launch();
const page = await browser.newPage();
await page.goto('https://example.com');
await Promise.all([
page.waitForURL('**/dashboard'),
page.getByRole('link', { name: 'Dashboard' }).click(),
]);
console.log(await page.url());
await browser.close();
})();
Python (sync API)
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 an explicit URL wait for URL-based synchronization:
page.wait_for_url('https://example.com/account')
browser.close()
For new Python tests whose purpose is specifically to wait for a URL, call page.wait_for_url() before the action rather than relying on the deprecated navigation-wait pattern shown only to identify the migration point:
page.goto('https://example.com')
with page.expect_event('framenavigated'):
page.get_by_role('link', name='Account').click()
page.wait_for_url('https://example.com/account')
Diagnose common timeout and mismatch failures
- The wait times out immediately after a click. The listener may have been installed too late. Put
waitForURLand the click inPromise.all. - The page reaches a URL that looks right but does not match. Log
await page.url()and compare the complete value, including trailing slash, query parameters, hash, scheme, and redirects. Switch to a glob, regex, URLPattern, or predicate only for the variable part. - A redirect satisfies the wait too early. Match the final route rather than the first host or a broad
**glob. Follow the URL wait with a locator assertion for the destination page. - The test waits forever on
networkidle. Replace it withdomcontentloaded,load, orcommit, then assert the specific UI state required by the test. - An iframe navigation is invisible to the page wait. Use the child frame’s
waitForURL, not the main page’s method. - No matching navigation occurs. A click may open a new tab, update content without changing the URL, or be blocked by validation. Verify the action, inspect pages and frames, and use a locator or response assertion when the URL is not the state transition.
- The test is flaky only in CI. Keep the wait registered before the action, avoid arbitrary sleeps, select a stable matcher, and assert readiness with a web assertion instead of guessing with a long delay.
Reliability and maintenance checklist
- Use accessible locators for the action so the test fails for a meaningful reason when the UI changes.
- Match only the URL components that define the behavior under test; do not discard a security- or tenant-specific host accidentally.
- Keep URL synchronization separate from page-readiness assertions. A successful URL wait is not proof that data loaded.
- Use the shortest suitable lifecycle event. Treat
networkidleas discouraged for tests, not as a universal “fully ready” signal. - When a route contains IDs or tracking parameters, use a regex or predicate and test the important parameter explicitly.
- When several navigations are possible, identify the final destination in the matcher and start the wait before the initiating action.
Or skip the browser setup
If your goal is a rendered page image rather than an end-to-end navigation assertion, ScreenshotNeo returns a screenshot or PDF from one HTTP request. Its cleanup step accepts cookie and consent banners and removes more than 60 known consent platforms, newsletter popups, and chat widgets before capture; each step can be disabled. Bot checks or CAPTCHAs, 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. An MCP server provides take_screenshot, get_page_info, and capture_pdf for Claude, Cursor, and other MCP clients.
The Tool Desk
Outbyte Driver Updater FREEFix the driver behind crashes, sound loss and screen glitchesFind Drivers →Outbyte PC Repair FREEClear out junk files and repair common Windows errorsFree Scan →See the ScreenshotNeo API documentation for parameters. cURL:
curl -G 'https://api.screenshotneo.com/v1/shot' -d access_key=YOUR_API_KEY --data-urlencode url=https://stripe.com -o shot.webp
Python:
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)
Node.js:
const q = new URLSearchParams({ access_key: 'YOUR_API_KEY', url: 'https://stripe.com' });
const res = await fetch(`https://api.screenshotneo.com/v1/shot?${q}`);
Every plan includes every feature. The Free plan includes 1,000 shots per month with no card; paid plans start at $5 for 3,000 shots, and yearly billing gives two months free. You can also use full-page capture, element selectors, device presets, custom waits, request blocking, authentication headers, cookies, PDF settings, caching, signed links, asynchronous jobs, webhooks, bulk capture, and the usage API when those are better suited to your workflow than driving a browser test.
Create a free ScreenshotNeo account with 1,000 screenshots a month and no card required.
Frequently Asked Questions
Does waitForURL wait for page content to finish rendering?
No. It synchronizes on a matching URL and lifecycle boundary. Add a locator or other web assertion for the content your test needs.
Can I wait for only a query-string value?
Yes. Pass a predicate that checks url.searchParams, or use a regular expression when a simpler pattern is sufficient.
Which method should I use for a child iframe?
Call frame.waitForURL() on the child frame. page.waitForURL() observes the main frame.
What should replace waitForNavigation?
Use page.waitForURL() when the destination URL is the synchronization target, and use expect(page).toHaveURL() when it is the assertion.
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.
Do these 3 things before closing this tab:
1Repair Windows errors before they cause bigger problems2Fix the driver behind crashes, sound loss and screen glitches3Clear out junk files and repair common Windows errors

