Skip to content
TargetQA
Playwright · TypeScriptWindow & Tab ManagementIntermediate4 min read

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.

By TargetQA Engineering Team•Updated 2026-03-10
Interactive Cloud Sandbox

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.

Test Live in Compiler
The Problem & Error Symptoms

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.

Under the Hood

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.

Field-Tested Patterns

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)

Recommended

Set up the event listener concurrently with the trigger action to eliminate race conditions.

1. Concurrent Promise.all Pattern (The Canonical Solution) (typescript)
// 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/);
How it works: `Promise.all` ensures that the `page` event listener is active before the click action triggers the browser to open the tab. This completely eliminates race conditions.

2. page.waitForEvent('popup') for Explicit Popups

If the popup is triggered by an interaction on a specific page, use `page.waitForEvent('popup')`.

2. page.waitForEvent('popup') for Explicit Popups (typescript)
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());
How it works: `page.waitForEvent('popup')` scopes the listener to popups initiated by that specific parent page.

3. Inspecting All Open Tabs in Context

Access the list of all active pages in the current browser context.

3. Inspecting All Open Tabs in Context (typescript)
const allPages = context.pages();
console.log(`Open tabs: ${allPages.length}`);

// Interact with any tab by index:
const initialTab = allPages[0];
const secondTab = allPages[1];
How it works: `context.pages()` returns an array of all currently open `Page` instances in that context.
Full Executable Cloud Test
Open in compiler

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.

test-recipe.spec.ts
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!');
});
Target framework: Playwright · Language: TypeScript
Preload & Run in Compiler
Production Checklist

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`.
Avoid Test Flakiness

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.

Hands-On Practice

Practice This Skill in TargetQA Challenges

Explore More Automation Guides