level 03 / browser-contexts

Browser Contexts & Multi-Tab

The browser → context → page hierarchy, test isolation, multi-tab and multi-window flows — the model underneath every Playwright test.

The hierarchy

One Browser process can host many BrowserContexts. Each context is an isolated, incognito-like session: its own cookies, localStorage, cache, and permissions. Each context can open many Pages (tabs).

import { chromium } from '@playwright/test';

const browser = await chromium.launch();        // one process
const context = await browser.newContext();     // isolated session
const page = await context.newPage();           // a tab inside it

In @playwright/test, the page fixture comes from a fresh context per test — this is why tests never leak cookies or storage into each other.

Why contexts beat new browsers

Launching a browser process costs seconds. Creating a context costs milliseconds. Contexts give you isolation at near-zero cost, which is what makes running hundreds of isolated E2E tests practical.

// Two users in one test — two contexts, one browser
test('admin sees what user posted', async ({ browser }) => {
  const userContext = await browser.newContext();
  const adminContext = await browser.newContext();

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

  await userPage.goto('/posts/new');
  // user creates a post...

  await adminPage.goto('/admin/moderation');
  // admin sees it — different session, no shared cookies

  await userContext.close();
  await adminContext.close();
});
Key insight: Multi-user scenarios (chat, approvals, marketplace buyer/seller) are the killer use case for multiple contexts. One test, two isolated sessions, real interaction between them.

Context options

const context = await browser.newContext({
  viewport: { width: 1280, height: 720 },
  locale: 'en-GB',
  timezoneId: 'Europe/London',
  permissions: ['geolocation'],
  geolocation: { latitude: 51.5, longitude: -0.12 },
  colorScheme: 'dark',
  baseURL: 'https://staging.example.com',
  storageState: 'auth/admin.json',   // pre-loaded cookies + localStorage
});

In @playwright/test, set these via use in the config or with test.use() per file:

test.use({ locale: 'de-DE', timezoneId: 'Europe/Berlin' });

test('dates render in German format', async ({ page }) => {
  // page fixture now has German locale
});

Multi-tab: handling popups

A new tab triggered by the app (target=“_blank”, window.open) arrives as a popup event on the page, or a page event on the context:

test('terms link opens in new tab', async ({ page }) => {
  await page.goto('/signup');

  // Set up the listener BEFORE the click — same pattern as waitForResponse
  const [termsPage] = await Promise.all([
    page.waitForEvent('popup'),
    page.getByRole('link', { name: 'Terms of Service' }).click(),
  ]);

  await termsPage.waitForLoadState();
  await expect(termsPage).toHaveURL(/terms/);
  await expect(termsPage.getByRole('heading', { name: 'Terms' })).toBeVisible();

  // Original page is still alive and usable
  await page.getByLabel('I agree').check();
});

Multiple windows from the context

test('compare two product pages side by side', async ({ context }) => {
  const page1 = await context.newPage();
  const page2 = await context.newPage();

  await page1.goto('/product/100');
  await page2.goto('/product/200');

  const price1 = await page1.getByTestId('price').innerText();
  const price2 = await page2.getByTestId('price').innerText();
  expect(price1).not.toBe(price2);
});

Both pages share the context’s session — same login, same cookies. Use separate contexts when you need separate identities.

Reference

browser.newContext(options)
New isolated session
context.newPage()
New tab in this context
page.waitForEvent('popup')
Capture tab opened by the app
context.pages()
All open pages in the context
context.storageState({ path })
Save cookies + localStorage to file
context.close()
Close context and all its pages
test.use({ ...options })
Context options for a test file

Interview questions