skip to content

In Playwright, how does the BrowserContext 'page' event differ from the Page 'popup' event?

level: middleimportance: should knowfreq 54%

answer

  1. One event is wide, one is narrow
  2. The context sees every new page
  3. Popups reach both listeners
  4. Unknown trigger means context level
  5. opener() links a popup to its parent

basics

~20 s

The context's page event fires for every page created in that context, including ones your script opens. The page's popup event fires only for popups that particular page opened, and it fires in addition to the context event.

solid answer

~40 s

`context.on('page')` is the wide net: it fires for every `Page` created in that `BrowserContext` — `context.newPage()` calls, `window.open` windows, `target="_blank"` links and modifier-clicked tabs alike. `page.on('popup')` is the narrow one: it fires only for popups belonging to that page, and it is emitted *in addition to* the context event, never instead of it. Use `page.waitForEvent('popup')` when you know the exact element you are clicking, and `context.waitForEvent('page')` when the trigger is unknown or lives outside the page you hold. Both hand you the page early — it has navigated to its initial URL but may still be loading. `page.opener()` completes the picture: it resolves to the page that opened this one, or `null` for a page created by `context.newPage()`.

code

typescript · 13 lines
typescript
import { test, expect } from '@playwright/test';

test('a surprise tab is caught by the context event', async ({ context, page }) => {
  await page.goto('https://hotels.example.com/guests/me');

  // Wide net: any new page in the session, whoever opened it.
  const pagePromise = context.waitForEvent('page');
  await page.getByRole('button', { name: 'Download invoice' }).click();
  const invoiceTab = await pagePromise;

  await invoiceTab.waitForLoadState();
  expect(await invoiceTab.opener()).toBe(page);
});

go deeper

for a junior

Remember which is which: the context event covers every new page in the session, and the page popup event covers only popups that page opened.

for a middle

Explain that a popup fires both events, that the context event also covers context.newPage(), and that neither waits for the new tab to finish loading.

for a senior

Show when to reach for the wide net. Frames, modifier-clicks and third-party widgets can all produce tabs the page-level event misses, so a hardened suite listens on the context.

for a principal

Decide the convention for the suite. A single context-level handler that logs or fails on unexpected tabs turns surprise windows into a diagnosable signal instead of a scattered per-test wait.

## Two events for one new tab Playwright reports a newly created tab twice, on two different objects, and the difference is scope. - `context.on('page', handler)` fires for **every** page created in that `BrowserContext`. That includes pages your own script opens with `context.newPage()`, tabs the site opens with `window.open`, `target="_blank"` links, and tabs the user opens by Ctrl- or Shift-clicking a link. - `page.on('popup', handler)` fires **only** for popups belonging to that particular page. It is emitted in addition to the context event, never instead of it. So the context event is the wide net and the page event is the narrow one. A popup opened from the hotel search results reaches both listeners; a tab your script creates with `context.newPage()` reaches only the context listener. ## Side by side | | `context.on('page')` | `page.on('popup')` | |---|---|---| | Scope | every page in the context | popups of one page | | Fires for `context.newPage()` | yes | no | | Fires for a `target="_blank"` click | yes | yes | | Awaited form | `context.waitForEvent('page')` | `page.waitForEvent('popup')` | | Typical use | you do not know which page will trigger it | you know exactly which link you clicked | ## Choosing between them 1. If you are about to click a known element on a page you hold, use `page.waitForEvent('popup')`. It is precise: another tab opening elsewhere in the session cannot resolve your wait by accident. 2. If the trigger is unknown, indirect, or lives outside the page you hold — a widget, a redirect chain, a background script on the guest-profile screen — use `context.waitForEvent('page')`. 3. If you want a standing rule for the whole session rather than a one-shot wait, register `context.on('page', ...)` once and handle each tab as it arrives. That is how you catch tabs opened by something you did not anticipate. ```js context.on('page', async newPage => { await newPage.waitForLoadState(); console.log('new tab:', newPage.url()); }); ``` ## Both hand you the page early Neither event waits for the tab to finish loading. The earliest moment a page is available is when it has navigated to its initial URL and that response has started arriving, so the object you receive is live but the document may be incomplete. Two consequences follow. First, wait for the state you need before acting on the tab. Second, if you want to observe or intercept that very first request, the page-level hooks are already too late — the context-level ones are where it is still visible. ## The opener link `page.opener()` is an async method returning the `Page` that opened this one, or `null`. It returns `null` for a page you created with `context.newPage()`, `null` for the page that did the opening, and `null` once the opener has been closed. For a genuine popup it gives you the page that spawned it, which is how a handler registered on the context can work out which flow a surprise tab came from. Two details worth carrying: - With `rel="noopener"` on the link, Playwright still reports the popup on both events. What changes is the popup's own `window.opener` inside the browser, which the platform leaves `null`. - A tab opened by Ctrl- or Shift-clicking a link is browser-dependent and often has no opener at all, so `page.opener()` may be `null` even though a real tab appeared. The context `page` event still catches it, which is the practical argument for preferring the wide net when you are hardening a suite rather than writing one assertion. ## The failure mode to recognise A test that waits on `page.waitForEvent('popup')` and times out, in a run where a new tab clearly opened, usually means the tab was not a popup of that page — it was opened by a frame, by a keyboard-modified click, or by a different page in the session. Switching that single wait to `context.waitForEvent('page')` is the fix, and it is a better default in a suite where you do not control the markup of every link.

  • Does the context 'page' event fire for pages your own script creates with context.newPage()?
    Yes. It fires for every page created in the context, including ones you opened yourself, which is why a `context.on('page')` handler registered up front sees your own tabs too. The page `popup` event does not — it is limited to popups a page opened.
  • What does page.opener() return, and when is it null?
    It resolves to the `Page` that opened this one. It is `null` for a page created by `context.newPage()`, `null` for the opener itself, `null` once the opener has been closed, and often `null` for a tab opened by a Ctrl- or Shift-click, which is browser-dependent.

saying these in an interview costs you the question

  • Thinking the popup event replaces the context event
  • Expecting page popup to fire for context.newPage()
  • Assuming the delivered page has finished loading
  • Believing rel=noopener suppresses the Playwright events
  • Using the page event for a tab opened elsewhere