Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

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.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

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:

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.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Wait 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.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

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.

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 waitForURL and the click in Promise.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 with domcontentloaded, load, or commit, 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 networkidle as 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.
Independent reader supportYour contribution helps us test, update, and keep practical guides available for everyone.Support on Ko-Fi

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.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

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.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

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.

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.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.