| name | Playwright Multi-Tab & Window Handling |
| description | Teaches the agent to handle popups, new tabs, and multiple browser windows in Playwright using waitForEvent('page'), context pages, and reliable tab switching for OAuth and target=_blank links. |
| version | 1.0.0 |
| author | thetestingacademy |
| license | MIT |
| tags | ["playwright","multi-tab","popup","new-window","oauth","browser-context","target-blank","automation"] |
| testingTypes | ["e2e","integration"] |
| frameworks | ["playwright"] |
| languages | ["typescript"] |
| domains | ["web"] |
| agents | ["claude-code","cursor","github-copilot","windsurf","codex","aider","continue","cline","zed","bolt","gemini-cli","amp"] |
Playwright Multi-Tab & Window Handling
This skill makes the agent write correct, race-free code for any flow that opens a second tab or popup: target="_blank" links, "Open in new window" buttons, OAuth/SSO consent screens, payment redirects, and PDF preview tabs. The central rule is that a new page is an event you must subscribe to before the click, never a thing you poll for afterward.
Use this skill whenever a test clicks something and a new tab/window appears, or whenever the agent sees page.waitForTimeout being used to "wait for the popup."
Core Principles
- Subscribe before you click. Register
context.waitForEvent('page') (or page.waitForEvent('popup')) before the action that triggers the popup, then await both together. Subscribing after the click is a race.
- A tab belongs to a
BrowserContext, not a Page. All tabs in one context share cookies/storage. context.pages() is the live list of open tabs.
- Always
await newPage.waitForLoadState() before asserting — the page object resolves the moment the tab exists, not when it has loaded.
- Switch by holding a reference, never by index. Index order is not guaranteed across browsers. Capture the returned
Page object.
- Close popups you opened. Leaked tabs slow the suite and can hold modal focus. Close them or rely on context teardown.
- Cross-origin popups are fine within a context. OAuth on a different domain still arrives as a
page event; you do not need a new context.
Workflow / Patterns
Pattern 1 — Capture a popup from a target="_blank" link
The canonical, race-free shape uses Promise.all: start listening, then click, in one expression.
import { test, expect } from '@playwright/test';
test('opens docs in a new tab', async ({ context, page }) => {
await page.goto('https://example.com/app');
[newPage] = .([
context.(),
page.(, { : }).(),
]);
newPage.();
(newPage).();
(newPage.(, { : })).();
newPage.();
(page.(, { : })).();
});