# 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](assertions-and-waiting.md), [core/fixtures-and-hooks.md](fixtures-and-hooks.md) ## Quick Reference ```typescript // 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** ```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** ```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](authentication.md). **TypeScript** ```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("testuser@gmail.com") 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** ```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** ```typescript import { test, expect } from "@playwright/test" test("PayPal popup checkout flow", async ({ page }) => { await page.goto("/checkout") await page.getByLabel("Email").fill("buyer@example.com") 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("buyer@paypal-test.com") 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** ```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("buyer@paypal-test.com") 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** ```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** ```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** ```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** ```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** ```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("newuser@example.com") await userPage.getByLabel("Password").fill("password123") await userPage.getByRole("button", { name: "Register" }).click() // Admin should see the new user appear await expect(adminPage.getByText("newuser@example.com")).toBeVisible({ timeout: 10000 }) await adminContext.close() await userContext.close() }) ``` **JavaScript** ```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("newuser@example.com") await userPage.getByLabel("Password").fill("password123") await userPage.getByRole("button", { name: "Register" }).click() await expect(adminPage.getByText("newuser@example.com")).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 - [core/authentication.md](authentication.md) -- bypassing OAuth popups with stored auth state - [core/multi-user-and-collaboration.md](multi-user-and-collaboration.md) -- multi-user real-time collaboration testing - [core/file-operations.md](file-operations.md) -- file download handling without popups - [core/third-party-integrations.md](third-party-integrations.md) -- mocking OAuth providers to avoid real popups - [core/fixtures-and-hooks.md](fixtures-and-hooks.md) -- fixtures for multi-context setups