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.

Some links on this page are affiliate links: if you buy through them we may earn a commission, at no extra cost to you.

Call page.url() on the Playwright Page object that represents the tab or popup you are operating. It returns the current URL as a string. In Playwright Test, that object is normally the built-in { page } fixture.

Read the current URL with page.url()

A Page is Playwright’s handle for one browser tab. Navigate it, then read its URL:

import { test, expect } from '@playwright/test';

test('read the current page URL', async ({ page }) => {
  await page.goto('https://example.com');

  const currentUrl = page.url();
  console.log(currentUrl);

  await expect(page).toHaveURL('https://example.com/');
});

The Page API defines page.url() as the method that returns the page’s current URL. It is synchronous because Playwright reads the URL already known by the page; it does not wait for navigation to finish. Wait for navigation first when the page is still changing.

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

Understand which “page” you are reading

There is no global current-page singleton. A browser can contain multiple contexts, and each BrowserContext can contain several pages. Keep the Page reference returned by Playwright and call url() on that reference.

Object or source What it represents Use it when
page fixture An isolated page supplied to a Playwright Test You are writing a test and need its default tab
context.newPage() A new tab in a specific browser context Your code creates a tab and can retain the returned reference
context.pages() All currently open pages in one context Several tabs exist and you must select one deliberately
Browser-level convenience page A page created for a simple, single-page snippet You do not need production-style context management

The BrowserContext API documents page ownership and the Browser API covers browser-level creation and cleanup. Do not assume that index zero in context.pages() is the tab the user means; identify it by a known URL, title, or another property.

Get the URL after navigation

Read the URL only after the navigation operation or an explicit URL wait has reached the state your test needs.

Navigation with goto

await page.goto('https://example.com/account');
const urlAfterLoad = page.url();

page.goto() returns a response (or null for special cases). That response is not the replacement for page.url(): redirects, client-side routing, and hash changes can make the final URL different from the initially requested address.

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

Wait for a specific destination

await page.getByRole('link', { name: 'Dashboard' }).click();
await page.waitForURL('**/dashboard');
console.log(page.url());

page.waitForURL() is the URL-specific synchronization method. It is useful when a click triggers navigation and you need to read the destination only after it matches a pattern or exact URL.

Assert the URL in a test

await expect(page).toHaveURL(//dashboard(?:?|$)/);

Playwright’s URL assertion waits for the expected URL, so it is generally preferable to manually polling page.url() in an assertion. Use waitForURL() when you need an explicit synchronization point and then want the string for logging or further logic.

Use the Playwright Test fixture correctly

Each test receives a default Page in its own browser context. The isolation guide explains that this keeps cookies, local storage, and session state from leaking between tests: Browser contexts and isolation. The fixtures guide documents the built-in fixture: Playwright Test fixtures.

import { test } from '@playwright/test';

test('log the fixture page URL', async ({ page }) => {
  await page.goto('/dashboard');
  console.log('test page:', page.url());
});

A relative URL such as /dashboard requires the baseURL configured in your Playwright project. The fixture’s page is the tab for that test; it is not a reference to another test’s tab or to every open tab in the browser.

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

Read the URL of a popup or newly opened tab

When a click opens a popup, start waiting for the event before triggering the click. Otherwise the popup can open and navigate before your code begins listening.

Popup owned by the current page

const popupPromise = page.waitForEvent('popup');
await page.getByRole('button', { name: 'Open report' }).click();

const popup = await popupPromise;
await popup.waitForURL('**/reports/**');
console.log('popup URL:', popup.url());

The popup event can arrive while the new page is still loading. Waiting for its expected URL, or for a suitable load state, prevents reading an intermediate address.

Any new page in the browser context

const newPagePromise = context.waitForEvent('page');
await page.getByText('Open new tab').click();

const newPage = await newPagePromise;
await newPage.waitForLoadState('domcontentloaded');
console.log('new tab URL:', newPage.url());

Use the context-level event when the new tab might not be a direct popup of the page that received the click. If the application can open more than one page, apply a known identity check before continuing.

Choose among several open pages

context.pages() returns the pages currently owned by that context. Select by an observable property rather than assuming ordering:

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
const pages = context.pages();
const reportPage = pages.find((candidate) =>
  candidate.url().includes('/reports/')
);

if (!reportPage) {
  throw new Error('The report page is not open');
}

console.log(reportPage.url());

For applications where the URL is not stable, inspect a title or wait for a page-specific element instead. Keep the reference returned by context.newPage() or by a popup/page event whenever possible; enumeration is a recovery or discovery technique, not a substitute for tracking ownership.

Do not confuse a frame with a page

An iframe is a document inside a Page, represented by a Frame. A frame does not become a second browser tab. Calling page.url() reports the top-level tab’s URL, not a frame’s document URL. If your test needs to interact with an embedded document, locate its frame and use frame APIs; if it needs the address shown in the browser tab, keep using the owning Page.

Common mistakes and fixes

  • Reading too early: Calling page.url() immediately after a click can capture the old address. Fix it with page.waitForURL(), expect(page).toHaveURL(), or an appropriate load-state wait.
  • Using a global variable: A variable such as currentPage does not automatically track whichever tab is active. Pass the intended Page reference through your helper functions.
  • Choosing context.pages()[0] blindly: Page order is not an identity guarantee. Find a page by URL, title, or a unique element, or retain the event result.
  • Listening for a popup after the click: The event may already have fired. Create the waitForEvent('popup') promise first.
  • Confusing a navigation response with the final URL: A redirect or client-side route can change the address. Read page.url() after the final navigation state.
  • Closing the context too soon: If you create a context directly, do not close it until URL reads and assertions finish. Explicitly close created contexts before closing the browser.
  • Expecting a frame URL from the page: A frame is not a tab. Use the frame object for embedded content and the page object for the top-level address.
Independent reader supportYour contribution helps us test, update, and keep practical guides available for everyone.Support on Ko-Fi

Make URL checks reliable and efficient

Prefer event-driven waits

Waiting for a URL pattern or an application-specific element avoids arbitrary sleeps and shortens tests when navigation completes quickly. Use a delay only when the application has a documented asynchronous behavior that cannot be observed another way.

Keep ownership explicit

For test code, use the fixture page. For custom automation, create a context, retain each page returned by newPage() or an event, and close the context during teardown. This keeps cookies and storage scoped to the intended run and prevents stale page references.

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

Log enough context to diagnose failures

When a URL assertion fails, log the page URL immediately before the assertion and identify which page produced it. In multi-tab tests, include the number of pages from context.pages() and each page’s URL while debugging; remove noisy diagnostics once the test is stable.

Or skip the browser setup

If your actual goal is to obtain a clean visual capture of a URL rather than inspect the URL string inside a running browser, ScreenshotNeo provides a single HTTP request. It accepts consent banners before capture and removes more than 60 known consent platforms, newsletter popups, and chat widgets; each cleanup step can be disabled. Bot checks and CAPTCHAs, blank pages, timeouts, failed loads, and cache hits are not billed, and each response identifies the result with X-Page-Verdict and X-Billed headers.

Use the API documentation beside the code: ScreenshotNeo API docs.

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 service returns PNG, JPEG, WebP, or PDF output and supports full-page captures, element selectors, device presets, custom viewports, retina scale, dark mode, lazy-image loading, PDF paper and page-range controls, custom CSS and JavaScript, clicks, waits, request blocking, headers, cookies, user agents, authorization, timezone, geolocation, transparent backgrounds, resizing, configurable cache TTLs, signed links, asynchronous webhooks, bulk capture of up to 100 URLs per call, a usage API, and an OpenAPI specification. An MCP server exposes take_screenshot, get_page_info, and capture_pdf to Claude, Cursor, and other MCP clients.

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

Free use includes 1,000 screenshots per month with no card. Paid plans start at $5 for 3,000 shots; every feature is available on every plan, and yearly billing gives two months free. Sign up for the free ScreenshotNeo plan.

Frequently Asked Questions

Does page.url() return a URL object?

No. It returns the current URL as a string, so use normal string operations or construct a URL object yourself when you need parsed components.

What should I use when a click can open several tabs?

Listen at the BrowserContext level with context.waitForEvent('page'), then identify the returned page by its destination or another observable property.

Why can the URL differ from the address passed to goto()?

Redirects, client-side routing, and hash changes can produce a different final address. The authoritative value for the tab is the page’s URL after the relevant navigation wait.

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

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.