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();
});
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.