In Playwright, a new browser tab is a Page in the same BrowserContext. Start waiting for the tab before the click or other action that opens it, await the resulting page, wait for the navigation state you need, and read its URL with newPage.url() in JavaScript or new_page.url in Python.
const popupPromise = page.waitForEvent('popup');
await page.getByText('open new tab').click();
const popup = await popupPromise;
await popup.waitForLoadState('domcontentloaded');
console.log(popup.url());
The reliable sequence
- Create the event wait before triggering the new tab.
- Perform the click, keyboard action, or script that opens it.
- Await the
Pagereturned by the event. - Wait for the navigation milestone that makes the URL meaningful.
- Read the URL from that page object.
Waiting first is essential. If the tab opens between the action and the listener registration, the event can be missed and the test may hang or time out.
When the current page opens the tab: popup
Use the page-level popup event when an action on a particular page creates the new tab, such as a link with target="_blank" or a button that calls window.open().
JavaScript
import { test, expect } from '@playwright/test';
test('reads the URL of a popup', async ({ page }) => {
await page.goto('https://example.com');
const popupPromise = page.waitForEvent('popup');
await page.getByRole('link', { name: 'Open new tab' }).click();
const popup = await popupPromise;
await popup.waitForLoadState('domcontentloaded');
const url = popup.url();
console.log(url);
expect(url).toContain('example.com');
});
TypeScript with a typed page
const popupPromise: Promise<Page> = page.waitForEvent('popup');
await page.getByText('open new tab').click();
const popup = await popupPromise;
await popup.waitForLoadState('domcontentloaded');
const url: string = popup.url();
The popup event is available after the new page has navigated to its initial URL. If redirects or a later script navigation determine the URL you actually need, add a URL wait or another targeted load wait before reading it.
Free tools Windows power users keep installed
One-click scans. No signup required.
#1 Best Overall
Python async API
import asyncio
from playwright.async_api import async_playwright
async def main():
async with async_playwright() as p:
browser = await p.chromium.launch(headless=True)
context = await browser.new_context()
page = await context.new_page()
await page.goto('https://example.com')
async with page.expect_popup() as popup_info:
await page.get_by_role('link', name='Open new tab').click()
popup = await popup_info.value
await popup.wait_for_load_state('domcontentloaded')
url = popup.url
print(url)
await browser.close()
asyncio.run(main())
expect_popup() is the Python context-manager equivalent of registering waitForEvent('popup') before the action.
When any page in the context may open
A browser context can contain several pages and multiple pages can create tabs. Use the context-level page event when you want the next page created anywhere in that context rather than a popup tied to one opener.
const newPagePromise = context.waitForEvent('page');
await page.getByText('open new tab').click();
const newPage = await newPagePromise;
await newPage.waitForLoadState('domcontentloaded');
console.log(newPage.url());
The context event also fires for popup pages. Its broader scope is useful for downloads, authentication flows, or code that does not expose which existing page initiated the new tab. The trade-off is that unrelated page creation can satisfy the wait, so use the page-level event when you can identify the opener.
Getting the final URL after redirects
url() returns the page’s current URL at the moment you call it. A redirect chain may still be running after the popup event is delivered. Wait for the expected destination when the final address matters.
PC Slower Than It Used to Be?
A free scan shows the junk files, broken settings and background clutter dragging Windows down - then fixes them in one click.Free scan · Windows 10 & 11Outdated 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 matchRank #2
const popupPromise = page.waitForEvent('popup');
await page.getByRole('button', { name: 'Continue' }).click();
const popup = await popupPromise;
await popup.waitForURL('**/dashboard');
const finalUrl = popup.url();
You can use a string, glob, or predicate appropriate to your test. If the destination is known only by a query value, a predicate makes the assertion explicit:
await popup.waitForURL(url => url.origin === 'https://app.example.com'
&& url.pathname === '/dashboard'
&& url.searchParams.has('session'));
For a less specific readiness check, domcontentloaded is usually a useful boundary. Avoid using networkidle as a generic test condition; pages with analytics, sockets, or long polling may never become idle. Prefer a web assertion that proves the destination is ready.
Choosing the right event
| Situation | Wait for | URL read |
|---|---|---|
| An action on one page opens its popup | page.waitForEvent('popup') / page.expect_popup() |
popup.url() / popup.url |
| Any new tab in the browser context | context.waitForEvent('page') |
newPage.url() |
| The address is changed by redirects or later navigation | One of the waits above, then waitForURL() |
Read only after the final wait |
| Pages already existed before the action | Prefer an event; use context.pages() only for an intentional snapshot |
Find the specific page, then read its URL |
Why a new-tab URL is empty, wrong, or missed
The listener was registered too late
Symptom: the test waits until timeout. Fix: assign the promise or enter expect_popup() before clicking. Do not put the wait after the click.
You listened on the wrong object
Symptom: a context-level wait never resolves, or a page-level wait captures nothing. Fix: use popup for a popup initiated by the current page; use the context’s page event for any newly created page.
Rank #3
You read the initial URL instead of the destination
Symptom: the value is an intermediate login, tracking, or redirect address. Fix: call waitForURL() with the destination pattern or predicate, then read url().
The action did not create a page
Symptom: the wait times out because the site opened a same-tab navigation, blocked a popup, or required a user gesture. Fix: verify the behavior in headed mode, check popup-blocking conditions, and decide whether you should wait for page.waitForURL() on the existing page instead.
Several tabs open at once
Symptom: the first new page is not the one your assertion expects. Fix: listen on the known opener, serialize actions, or inspect the new page’s URL and content before selecting it. A broad context listener cannot infer your intent.
The page closes early
Symptom: URL access fails because the popup is immediately closed. Fix: read or assert the URL as soon as the required navigation completes, and investigate whether the site uses a short-lived intermediary window.
Do these 3 things before closing this tab:
1Scan for outdated or missing drivers - takes under a minute2Clear out junk files and repair common Windows errors3Fix the driver behind crashes, sound loss and screen glitchesPractical patterns for robust tests
Keep the page reference
Return the popup from a helper so callers cannot accidentally inspect the original tab:
async function openTab(page) {
const popupPromise = page.waitForEvent('popup');
await page.getByRole('link', { name: 'Open new tab' }).click();
const popup = await popupPromise;
await popup.waitForLoadState('domcontentloaded');
return popup;
}
const tab = await openTab(page);
await tab.waitForURL('**/receipt/*');
const receiptUrl = tab.url();
Use a bounded timeout
Set a timeout that reflects the application’s normal navigation time and keep the failure actionable. A timeout should identify whether the event, the load state, or the URL assertion failed; avoid masking the original problem with an unbounded wait.
Close pages you no longer need
In long-running suites, close the popup after extracting the URL when it is not needed for further assertions. This prevents accidental interactions with stale tabs and keeps the context manageable.
Performance and reliability considerations
- Event waits are synchronized with page creation and avoid polling the page list.
domcontentloadedgenerally completes earlier than a full load while still providing a navigated document; choose the state that matches your assertion.- URL checks alone do not prove that application data rendered. Combine the final URL wait with a locator assertion when page readiness matters.
- Use one browser context per isolated test scenario when unrelated tabs could otherwise satisfy a context-level event.
- Redirects, authentication, consent screens, and client-side routing can all change the address after the initial popup event; model those transitions explicitly.
Or skip the browser setup
If your goal is a clean image or PDF of a destination rather than browser automation, ScreenshotNeo provides a single 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 or CAPTCHAs, blank pages, timeouts, failed loads, and cache hits are not billed, and response headers identify the page verdict and billing result. Its MCP server exposes take_screenshot, get_page_info, and capture_pdf to Claude, Cursor, and other MCP clients.
Recommended Free Tools
cURL
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
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 the feature set: full-page and element captures, device and viewport controls, retina scale, PDF settings, custom CSS and JavaScript, clicks, waits, request blocking, headers, cookies, user agent, timezone, geolocation, transparent backgrounds, resizing, TTL caching, signed links, asynchronous jobs with signed webhooks, bulk capture for up to 100 URLs per call, usage data, and an OpenAPI specification. Existing parameter names used by other screenshot APIs also work.
The Free plan includes 1,000 screenshots per month without a card. Paid plans start at $5 for 3,000 shots; yearly billing provides two months free. Create your free ScreenshotNeo account.
FAQ
Is a new tab a separate browser context?
No. It is a new Page inside the same BrowserContext, sharing that context’s cookies and other browser state.
Should I use context.pages() instead of an event?
Only when you deliberately want a snapshot of all existing pages. An event wait associates one newly created page with the action that triggered it and is safer for tests.
Can I read the URL without waiting for a load state?
You can read the initial URL as soon as the popup event resolves, but wait for a targeted navigation or URL condition when redirects or client-side navigation determine the value you need.
Frequently Asked Questions
Does Playwright support headless mode for popup URL checks?
Yes. The popup and page events work the same way in headless and headed Chromium, Firefox, and WebKit runs; headless mode changes visibility, not the Page-based URL API.
What if the link opens a tab only with a modifier key?
Perform the exact gesture that creates the tab while the event wait is active, for example a mouse click with the required button or keyboard modifiers, then await the resulting Page.
What’s actually slowing this PC down?
Pick the symptom - the matching free tool is one click away.
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.

