Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Latest commit

 

History

History
526 lines (400 loc) · 20.6 KB

File metadata and controls

526 lines (400 loc) · 20.6 KB

Multi-Context, Popups, and New Windows

When to use: Handling popup windows, new tabs, OAuth authorization flows, payment gateway redirects, multi-tab coordination, and any scenario where your application opens additional browser windows or tabs. Prerequisites: core/assertions-and-waiting.md, core/fixtures-and-hooks.md

Quick Reference

// Catch a popup triggered by a click
const popupPromise = page.waitForEvent('popup');
await page.getByRole('button', { name: 'Open preview' }).click();
const popup = await popupPromise;
await popup.waitForLoadState();
await expect(popup.getByRole('heading')).toContainText('Preview');

// List all open pages in a context
const allPages = context.pages();
console.log(`Open tabs: ${allPages.length}`);

Key concept: In Playwright, a "popup" is any new page opened by window.open(), target="_blank" links, or JavaScript-triggered new windows. They all fire the popup event on the originating page.

Patterns

Handling Basic Popups

Use when: A user action opens a new tab or window and you need to interact with it. Avoid when: The "popup" is actually a modal dialog within the same page -- use getByRole('dialog') instead.

TypeScript

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

test('handle popup opened by target="_blank" link', async ({ page }) => {
  await page.goto('/help');

  // Set up the popup listener BEFORE the action that triggers it
  const popupPromise = page.waitForEvent('popup');
  await page.getByRole('link', { name: 'Documentation' }).click();
  const popup = await popupPromise;

  // Wait for the popup to load
  await popup.waitForLoadState();

  // Interact with the popup
  await expect(popup.getByRole('heading', { level: 1 })).toContainText('Documentation');
  expect(popup.url()).toContain('/docs');

  // Close the popup when done
  await popup.close();
});

test('handle popup opened by window.open()', async ({ page }) => {
  await page.goto('/reports');

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

  await popup.waitForLoadState();
  await expect(popup.getByTestId('print-preview')).toBeVisible();

  // The original page is still accessible
  await expect(page.getByRole('heading', { name: 'Reports' })).toBeVisible();
});

JavaScript

const { test, expect } = require('@playwright/test');

test('handle popup opened by target="_blank" link', async ({ page }) => {
  await page.goto('/help');

  const popupPromise = page.waitForEvent('popup');
  await page.getByRole('link', { name: 'Documentation' }).click();
  const popup = await popupPromise;

  await popup.waitForLoadState();
  await expect(popup.getByRole('heading', { level: 1 })).toContainText('Documentation');

  await popup.close();
});

OAuth Popup Flows

Use when: Your app opens a third-party OAuth window (Google, GitHub, Microsoft, etc.) for authentication. Avoid when: You can bypass OAuth entirely by injecting auth tokens directly -- see core/authentication.md.

TypeScript

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

test('Google OAuth popup flow', async ({ page }) => {
  await page.goto('/login');

  // Listen for the popup before clicking the OAuth button
  const popupPromise = page.waitForEvent('popup');
  await page.getByRole('button', { name: 'Sign in with Google' }).click();
  const oauthPopup = await popupPromise;

  // Wait for the OAuth provider page to load
  await oauthPopup.waitForLoadState();
  expect(oauthPopup.url()).toContain('accounts.google.com');

  // Fill in credentials on the OAuth provider page
  await oauthPopup.getByLabel('Email or phone').fill('[email protected]');
  await oauthPopup.getByRole('button', { name: 'Next' }).click();
  await oauthPopup.getByLabel('Enter your password').fill('test-password');
  await oauthPopup.getByRole('button', { name: 'Next' }).click();

  // The popup closes automatically after authorization
  // Wait for the original page to receive the auth callback
  await page.waitForURL('/dashboard');
  await expect(page.getByRole('heading', { name: 'Dashboard' })).toBeVisible();
});

test('GitHub OAuth popup flow', async ({ page }) => {
  await page.goto('/login');

  const popupPromise = page.waitForEvent('popup');
  await page.getByRole('button', { name: 'Sign in with GitHub' }).click();
  const popup = await popupPromise;

  await popup.waitForLoadState();
  expect(popup.url()).toContain('github.com');

  await popup.getByLabel('Username or email address').fill('testuser');
  await popup.getByLabel('Password').fill('test-password');
  await popup.getByRole('button', { name: 'Sign in' }).click();

  // Authorize the app if prompted
  const authorizeButton = popup.getByRole('button', { name: 'Authorize' });
  if (await authorizeButton.isVisible({ timeout: 3000 }).catch(() => false)) {
    await authorizeButton.click();
  }

  // Popup closes, original page redirects
  await page.waitForURL('/dashboard');
  await expect(page.getByText(/Welcome/)).toBeVisible();
});

JavaScript

const { test, expect } = require('@playwright/test');

test('GitHub OAuth popup flow', async ({ page }) => {
  await page.goto('/login');

  const popupPromise = page.waitForEvent('popup');
  await page.getByRole('button', { name: 'Sign in with GitHub' }).click();
  const popup = await popupPromise;

  await popup.waitForLoadState();
  await popup.getByLabel('Username or email address').fill('testuser');
  await popup.getByLabel('Password').fill('test-password');
  await popup.getByRole('button', { name: 'Sign in' }).click();

  await page.waitForURL('/dashboard');
  await expect(page.getByText(/Welcome/)).toBeVisible();
});

Payment Gateway Popups

Use when: Checkout flows open a payment provider in a new window (PayPal, 3D Secure verification). Avoid when: The payment gateway loads in an iframe on the same page -- use frameLocator instead.

TypeScript

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

test('PayPal popup checkout flow', async ({ page }) => {
  await page.goto('/checkout');

  await page.getByLabel('Email').fill('[email protected]');
  await page.getByRole('button', { name: 'Proceed to payment' }).click();

  // PayPal opens in a popup
  const popupPromise = page.waitForEvent('popup');
  await page.getByRole('button', { name: 'Pay with PayPal' }).click();
  const paypalPopup = await popupPromise;

  await paypalPopup.waitForLoadState();
  expect(paypalPopup.url()).toContain('paypal.com');

  // Complete PayPal flow
  await paypalPopup.getByLabel('Email').fill('[email protected]');
  await paypalPopup.getByRole('button', { name: 'Next' }).click();
  await paypalPopup.getByLabel('Password').fill('test-password');
  await paypalPopup.getByRole('button', { name: 'Log In' }).click();
  await paypalPopup.getByRole('button', { name: 'Complete Purchase' }).click();

  // Popup closes, return to original page
  await page.waitForURL('/order/confirmation');
  await expect(page.getByText('Payment successful')).toBeVisible();
});

test('3D Secure verification popup', async ({ page }) => {
  await page.goto('/checkout');
  await page.getByLabel('Card number').fill('4000000000003220'); // 3DS test card
  await page.getByLabel('Expiry').fill('12/28');
  await page.getByLabel('CVC').fill('123');
  await page.getByRole('button', { name: 'Pay' }).click();

  // 3DS challenge opens in popup or iframe -- handle both
  const popupPromise = page.waitForEvent('popup', { timeout: 5000 }).catch(() => null);
  const popup = await popupPromise;

  if (popup) {
    await popup.waitForLoadState();
    await popup.getByRole('button', { name: 'Complete authentication' }).click();
  } else {
    // Fallback: 3DS in iframe
    const frame = page.frameLocator('iframe[name*="challenge"]');
    await frame.getByRole('button', { name: 'Complete authentication' }).click();
  }

  await expect(page.getByText('Payment successful')).toBeVisible();
});

JavaScript

const { test, expect } = require('@playwright/test');

test('PayPal popup checkout flow', async ({ page }) => {
  await page.goto('/checkout');
  await page.getByRole('button', { name: 'Proceed to payment' }).click();

  const popupPromise = page.waitForEvent('popup');
  await page.getByRole('button', { name: 'Pay with PayPal' }).click();
  const paypalPopup = await popupPromise;

  await paypalPopup.waitForLoadState();
  await paypalPopup.getByLabel('Email').fill('[email protected]');
  await paypalPopup.getByRole('button', { name: 'Next' }).click();
  await paypalPopup.getByLabel('Password').fill('test-password');
  await paypalPopup.getByRole('button', { name: 'Log In' }).click();
  await paypalPopup.getByRole('button', { name: 'Complete Purchase' }).click();

  await page.waitForURL('/order/confirmation');
  await expect(page.getByText('Payment successful')).toBeVisible();
});

Multi-Tab Coordination

Use when: Testing scenarios where multiple tabs share state -- real-time collaboration, shopping cart sync, or session management across tabs. Avoid when: Each tab is independent and can be tested in separate test cases.

TypeScript

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

test('cart updates reflect across tabs', async ({ context }) => {
  // Open two tabs in the same context (shared cookies/storage)
  const page1 = await context.newPage();
  const page2 = await context.newPage();

  await page1.goto('/products');
  await page2.goto('/cart');

  // Add item in tab 1
  await page1.getByRole('button', { name: 'Add to cart' }).first().click();

  // Tab 2 should reflect the update (via WebSocket, polling, or storage event)
  await expect(page2.getByTestId('cart-count')).toHaveText('1', { timeout: 5000 });
});

test('logout in one tab logs out all tabs', async ({ context }) => {
  const page1 = await context.newPage();
  const page2 = await context.newPage();

  // Both tabs are on authenticated pages
  await page1.goto('/dashboard');
  await page2.goto('/settings');

  // Log out from tab 1
  await page1.getByRole('button', { name: 'Log out' }).click();
  await page1.waitForURL('/login');

  // Tab 2 should redirect to login on next action or automatically
  await page2.reload();
  expect(page2.url()).toContain('/login');
});

test('manage multiple tabs with context.pages()', async ({ context, page }) => {
  await page.goto('/dashboard');

  // Open several new tabs
  const popupPromise1 = page.waitForEvent('popup');
  await page.getByRole('link', { name: 'Report A' }).click();
  const tab1 = await popupPromise1;

  const popupPromise2 = page.waitForEvent('popup');
  await page.getByRole('link', { name: 'Report B' }).click();
  const tab2 = await popupPromise2;

  // List all pages in this context
  const allPages = context.pages();
  expect(allPages).toHaveLength(3); // original + 2 popups

  // Interact with specific tabs
  await tab1.waitForLoadState();
  await expect(tab1.getByRole('heading')).toContainText('Report A');

  await tab2.waitForLoadState();
  await expect(tab2.getByRole('heading')).toContainText('Report B');

  // Close tabs when done
  await tab2.close();
  await tab1.close();
  expect(context.pages()).toHaveLength(1);
});

JavaScript

const { test, expect } = require('@playwright/test');

test('cart updates reflect across tabs', async ({ context }) => {
  const page1 = await context.newPage();
  const page2 = await context.newPage();

  await page1.goto('/products');
  await page2.goto('/cart');

  await page1.getByRole('button', { name: 'Add to cart' }).first().click();
  await expect(page2.getByTestId('cart-count')).toHaveText('1', { timeout: 5000 });
});

test('manage tabs with context.pages()', async ({ context, page }) => {
  await page.goto('/dashboard');

  const popupPromise = page.waitForEvent('popup');
  await page.getByRole('link', { name: 'Report A' }).click();
  const newTab = await popupPromise;

  expect(context.pages()).toHaveLength(2);
  await newTab.close();
  expect(context.pages()).toHaveLength(1);
});

Download Triggers in New Tabs

Use when: A download is triggered by opening a new tab (e.g., PDF generation that opens in a new window before downloading). Avoid when: The download starts directly without opening a new tab -- use page.waitForEvent('download') directly.

TypeScript

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

test('PDF download from new tab', async ({ page }) => {
  await page.goto('/invoices');

  // Some apps open the PDF in a new tab, which then triggers a download
  const popupPromise = page.waitForEvent('popup');
  await page.getByRole('link', { name: 'Download Invoice #123' }).click();
  const popup = await popupPromise;

  // The popup may directly trigger a download
  const downloadPromise = popup.waitForEvent('download');
  const download = await downloadPromise;

  expect(download.suggestedFilename()).toContain('invoice');
  const path = await download.path();
  expect(path).toBeTruthy();

  await popup.close();
});

test('export opens in new tab then auto-downloads', async ({ page }) => {
  await page.goto('/reports');

  // Handle both: popup that downloads AND popup that shows content
  const popupPromise = page.waitForEvent('popup');
  await page.getByRole('button', { name: 'Export CSV' }).click();
  const popup = await popupPromise;

  // Try to catch a download; if no download, the popup has the content
  try {
    const download = await popup.waitForEvent('download', { timeout: 5000 });
    const filename = download.suggestedFilename();
    expect(filename).toMatch(/\.csv$/);
    await download.saveAs(`./downloads/${filename}`);
  } catch {
    // No download event — content displayed in the popup
    const content = await popup.textContent('body');
    expect(content).toContain('Report Data');
  }

  await popup.close();
});

JavaScript

const { test, expect } = require('@playwright/test');

test('PDF download from new tab', async ({ page }) => {
  await page.goto('/invoices');

  const popupPromise = page.waitForEvent('popup');
  await page.getByRole('link', { name: 'Download Invoice #123' }).click();
  const popup = await popupPromise;

  const downloadPromise = popup.waitForEvent('download');
  const download = await downloadPromise;

  expect(download.suggestedFilename()).toContain('invoice');
  await popup.close();
});

Multi-Context for Isolated Sessions

Use when: Testing interactions between different users (e.g., admin and regular user, two chat participants). Avoid when: You only need a single user perspective -- a single context is sufficient.

TypeScript

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

test('admin sees user changes in real-time', async ({ browser }) => {
  // Create separate contexts for two users (separate sessions)
  const adminContext = await browser.newContext();
  const userContext = await browser.newContext();

  const adminPage = await adminContext.newPage();
  const userPage = await userContext.newPage();

  // Admin logs in and watches the user list
  await adminPage.goto('/admin/users');

  // User signs up
  await userPage.goto('/register');
  await userPage.getByLabel('Name').fill('New User');
  await userPage.getByLabel('Email').fill('[email protected]');
  await userPage.getByLabel('Password').fill('password123');
  await userPage.getByRole('button', { name: 'Register' }).click();

  // Admin should see the new user appear
  await expect(adminPage.getByText('[email protected]')).toBeVisible({ timeout: 10000 });

  await adminContext.close();
  await userContext.close();
});

JavaScript

const { test, expect } = require('@playwright/test');

test('admin sees user changes in real-time', async ({ browser }) => {
  const adminContext = await browser.newContext();
  const userContext = await browser.newContext();

  const adminPage = await adminContext.newPage();
  const userPage = await userContext.newPage();

  await adminPage.goto('/admin/users');

  await userPage.goto('/register');
  await userPage.getByLabel('Name').fill('New User');
  await userPage.getByLabel('Email').fill('[email protected]');
  await userPage.getByLabel('Password').fill('password123');
  await userPage.getByRole('button', { name: 'Register' }).click();

  await expect(adminPage.getByText('[email protected]')).toBeVisible({ timeout: 10000 });

  await adminContext.close();
  await userContext.close();
});

Decision Guide

Scenario Approach Why
target="_blank" link page.waitForEvent('popup') Playwright fires popup for all new windows/tabs
window.open() call page.waitForEvent('popup') Same mechanism regardless of how the window opens
OAuth login popup waitForEvent('popup') + interact + wait for close OAuth providers redirect back and close the popup
Payment popup (PayPal) waitForEvent('popup') + complete flow Same as OAuth but with payment-specific UI
Download in new tab popup.waitForEvent('download') Catch the download event on the popup page
Multiple tabs, same user Open pages in the same context Shared cookies, localStorage, session
Multiple users (separate sessions) Create separate browser.newContext() per user Isolated cookies, storage, auth state
Tab sync testing Multiple context.newPage() + assert shared state Tests real-time state synchronization
Popup that may or may not appear waitForEvent('popup', { timeout }) with try/catch Graceful handling of conditional popups

Anti-Patterns

Don't Do This Problem Do This Instead
Clicking then calling waitForEvent('popup') Popup may open before listener is registered (race condition) Set up waitForEvent BEFORE the click
Using context.pages()[1] to get a popup Index order is not guaranteed; brittle Use waitForEvent('popup') which returns the exact page
Forgetting popup.waitForLoadState() Popup page may not be loaded when you interact with it Always call waitForLoadState() after receiving the popup
Not closing popups after test Leaked pages consume memory and may affect subsequent tests Close popups with popup.close() in the test or use fixtures
Using separate browser.newContext() when tabs should share state Separate contexts have separate cookies/sessions Use context.newPage() for tabs in the same session
Using context.newPage() for isolated users Pages in the same context share state Use browser.newContext() for separate user sessions
page.waitForTimeout() after popup trigger Popup timing is unpredictable Use page.waitForEvent('popup') which resolves when the popup opens
Catching popup on the wrong page Popup event fires on the page that triggered it Listen for popup on the page where the click/action happens

Troubleshooting

Symptom Likely Cause Fix
waitForEvent('popup') times out The action did not open a new window; it navigated in the same tab Check if the link has target="_blank" or if window.open is called
Popup opens but is blank Popup is still loading; you interacted too early Add await popup.waitForLoadState() before interacting
Popup URL is about:blank Popup is created empty, then navigated by JavaScript Wait for popup.waitForURL() with the expected URL pattern
OAuth popup is blocked by browser Pop-up blocker is active Playwright disables pop-up blocking by default; check if a browser arg re-enables it
Two popups open but only one is caught waitForEvent resolves for the first event only Use page.on('popup', callback) to catch all popups, or chain two waitForEvent calls
context.pages() returns unexpected count Previous test left pages open Close all extra pages in afterEach or use per-test contexts
Popup closes before interaction completes The app or OAuth provider auto-closes after timeout Increase test speed or remove slowMo; interact with the popup immediately
Cross-origin popup interaction fails Popup navigated to a different origin Playwright handles cross-origin popups; ensure you are not setting --disable-web-security

Related