How to Handle New Tabs, Popups, and Windows in Playwright
Handling new tabs in Playwright is clean and deterministic when you understand the event-driven page model. Learn the exact pattern to avoid race conditions.
Test this solution live in real cloud browsers
Preload the working test script directly into TargetQA's Playwright compiler. Zero local dependencies or browser drivers needed.
Understanding the Breakdown
When a button or link with `target='_blank'` opens a new tab, or when JavaScript triggers `window.open()`, test scripts often fail with race conditions: the test checks for the new page before the browser has finished opening it, or tries to interact with the original page instead of the new tab.
Why Traditional Selectors Fail
In traditional Selenium, you have to poll `driver.window_handles` and switch by string ID. In Playwright, each tab is an independent `Page` object inside a `BrowserContext`. If you click the link before setting up an event listener, the popup event can fire before your listener attaches, resulting in a test timeout.
Production-Grade Solutions
Choose the strategy that matches your test architecture. Pattern #1 is recommended for most production test suites.
1. Concurrent Promise.all Pattern (The Canonical Solution)
RecommendedSet up the event listener concurrently with the trigger action to eliminate race conditions.
// Start waiting for the new page event BEFORE or SIMULTANEOUSLY with clicking:
const [newPage] = await Promise.all([
context.waitForEvent('page'),
page.getByRole('link', { name: 'Open New Window' }).click()
]);
// Wait for the new page DOM to load:
await newPage.waitForLoadState();
// Interact with the new tab independently:
await expect(newPage).toHaveTitle('New Window');
await newPage.getByRole('button', { name: 'Accept Terms' }).click();
// Close new tab and return cleanly to main page:
await newPage.close();
await expect(page).toHaveURL(/main/);2. page.waitForEvent('popup') for Explicit Popups
If the popup is triggered by an interaction on a specific page, use `page.waitForEvent('popup')`.
const popupPromise = page.waitForEvent('popup');
await page.getByRole('button', { name: 'Sign in with Google' }).click();
const popup = await popupPromise;
await popup.waitForLoadState();
console.log('Popup URL:', popup.url());3. Inspecting All Open Tabs in Context
Access the list of all active pages in the current browser context.
const allPages = context.pages();
console.log(`Open tabs: ${allPages.length}`);
// Interact with any tab by index:
const initialTab = allPages[0];
const secondTab = allPages[1];Complete Working Test Script
You can copy this exact test into your repository or run it immediately in our cloud sandbox against live demo targets.
import { test, expect } from '@playwright/test';
test('Handle new tab opening from target=_blank link', async ({ page, context }) => {
// Navigate to herokuapp windows test page
await page.goto('https://the-internet.herokuapp.com/windows');
// Verify initial page
await expect(page.locator('h3')).toHaveText('Opening a new window');
// Set up event listener and click simultaneously
const [newPage] = await Promise.all([
context.waitForEvent('page'),
page.getByRole('link', { name: 'Click Here' }).click(),
]);
// Wait for new page load
await newPage.waitForLoadState();
// Assert new page content
const newHeader = newPage.locator('h3');
await expect(newHeader).toHaveText('New Window');
console.log('New tab URL:', newPage.url());
// Close the popup tab
await newPage.close();
// Verify we are back on the original page
await expect(page.locator('h3')).toHaveText('Opening a new window');
console.log('Successfully navigated across multiple tabs!');
});Best Practices & Golden Rules
- Always wrap `waitForEvent('page')` and `.click()` in `Promise.all()` to prevent missing the event.
- Call `await newPage.waitForLoadState()` before asserting elements on the new tab.
- Explicitly call `await newPage.close()` when finished with secondary windows to free memory.
- Remember that each tab inherits cookies and localStorage from the parent `BrowserContext`.
Common Pitfalls to Avoid
- Awaiting `.click()` before `waitForEvent()`: if the new tab opens fast, the event fires before the listener is registered, causing an indefinite timeout.
- Forgetting to call `newPage.close()`: leaves orphan tabs that slow down cloud test runners.
Frequently Asked Questions
How do I switch focus back to the parent tab in Playwright?
In Playwright, you do not need to switch focus! Both `page` (parent) and `newPage` (child) remain active JavaScript objects. You can call actions on either object at any time without calling a 'switch' command.
What if the button opens a window after an asynchronous API call?
Using `context.waitForEvent('page', { timeout: 15000 })` inside `Promise.all` allows you to customize the timeout for delayed window launches.
Do multiple tabs share authentication state?
Yes. All pages within the same `BrowserContext` share the same cookie jar, session storage, and cache, mirroring real user behavior in a browser.