Tools: Playwright BrowserContext: What It Is, Why It Matters, and How to Configure It

Tools: Playwright BrowserContext: What It Is, Why It Matters, and How to Configure It

Source: Dev.to

First: What Is a BrowserContext? ## Why BrowserContexts Exist (And Why You Should Care) ## The Hidden Relationship: browser → context → page ## Configuring BrowserContexts in playwright.config.ts ## Basic Example ## Auth State: The #1 Real-World BrowserContext Feature ## Problem ## Solution: Auth Setup + storageState ## 1️⃣ Create a global setup ## 2️⃣ Reference it in config ## Sharing Setup Without Sharing State ## ❌ Anti-pattern ## ✅ Correct Pattern: Fixtures ## Multiple BrowserContexts in One Test (Yes, You Can) ## Multiple Setup Files Using Projects ## Use Case Examples ## Example: Multiple Projects ## Can BrowserContexts Be Shared? ## Mental Model to Remember ## Common BrowserContext Mistakes (and How to Avoid Them) ## 1. Sharing Pages or Contexts Across Tests ## 2. Logging In Inside Every Test ## 3. Overusing beforeAll ## 4. Confusing Config Sharing with State Sharing ## 5. Avoiding Multiple Contexts When You Actually Need Them ## Final Thoughts If you’ve been using Playwright for a while, you’ve definitely used browserContext—even if you didn’t fully realize it. It’s one of those core concepts that quietly shapes test isolation, speed, flakiness, auth state, parallelism, and even how sane your test suite feels over time. This article is a practical, real-world deep dive into: This is written for engineers who already know Playwright basics and want to level up their test architecture. A BrowserContext is an isolated browser profile. Think of it like this: Each context has its own: But they all share the same browser process, which is why contexts are fast and cheap. If you’ve ever opened two incognito windows side by side — that’s basically two browser contexts. Playwright is opinionated about test isolation. If you’ve ever fought flaky Selenium tests caused by leftover cookies — this is why Playwright feels so much better. You almost never create this manually, but it’s worth understanding: Playwright’s test runner does this for every test, unless you tell it otherwise. Pages never exist without a BrowserContext. Most context configuration happens via the use block. Everything inside use becomes default BrowserContext options. This means every test gets: …but still not the same state. Let’s talk about the killer feature: storageState. Logging in before every test is: This is isolation with convenience. This is where many teams mess up. Each test still gets: But setup logic is shared cleanly. Sometimes you need multiple users. Example: chat apps, admin/user flows, invitations. This is powerful — and still fast. This is where Playwright really shines. You can even target them: No — and that’s the point. Contexts are designed to be: What should never be shared: If you remember nothing else, remember this: One test = one browser context Unless you explicitly create more. That rule alone explains: This is the fastest way to introduce flaky, order-dependent tests. If you see patterns like: You’re fighting Playwright instead of using it. Fix: Let Playwright create a fresh context per test. Share setup logic via fixtures or storageState, not live objects. Fix: Use a dedicated auth setup and storageState. Test login flows separately. beforeAll feels convenient, but it breaks: Fix: Prefer per-test setup with fixtures. If something must run once, make sure it does not create shared browser state. Config options like use, projects, and fixtures are safe to share. Browser contexts, pages, and mutable globals are not. If a failure only happens when tests run together, this is usually the reason. Some teams try to force complex multi-user flows into a single page. Fix: Use multiple browser contexts intentionally when modeling real users. BrowserContext is one of Playwright’s most important architectural concepts. Once you understand how contexts work, your test suite naturally becomes: If your Playwright tests feel fragile or hard to maintain, there’s a strong chance the root cause is how browser contexts are being managed. Design around them correctly, and the rest of Playwright starts to feel effortless. Templates let you quickly answer FAQs or store snippets for re-use. Are you sure you want to hide this comment? It will become hidden in your post, but will still be visible via the comment's permalink. Hide child comments as well For further actions, you may consider blocking this person and/or reporting abuse COMMAND_BLOCK: test('example', async ({ page }) => { // This page lives in a fresh browser context }); Enter fullscreen mode Exit fullscreen mode COMMAND_BLOCK: test('example', async ({ page }) => { // This page lives in a fresh browser context }); COMMAND_BLOCK: test('example', async ({ page }) => { // This page lives in a fresh browser context }); CODE_BLOCK: const browser = await chromium.launch(); const context = await browser.newContext(); const page = await context.newPage(); Enter fullscreen mode Exit fullscreen mode CODE_BLOCK: const browser = await chromium.launch(); const context = await browser.newContext(); const page = await context.newPage(); CODE_BLOCK: const browser = await chromium.launch(); const context = await browser.newContext(); const page = await context.newPage(); CODE_BLOCK: import { defineConfig } from '@playwright/test'; export default defineConfig({ use: { baseURL: 'https://my-app.com', headless: true, viewport: { width: 1280, height: 720 }, locale: 'en-US', timezoneId: 'America/New_York', }, }); Enter fullscreen mode Exit fullscreen mode CODE_BLOCK: import { defineConfig } from '@playwright/test'; export default defineConfig({ use: { baseURL: 'https://my-app.com', headless: true, viewport: { width: 1280, height: 720 }, locale: 'en-US', timezoneId: 'America/New_York', }, }); CODE_BLOCK: import { defineConfig } from '@playwright/test'; export default defineConfig({ use: { baseURL: 'https://my-app.com', headless: true, viewport: { width: 1280, height: 720 }, locale: 'en-US', timezoneId: 'America/New_York', }, }); COMMAND_BLOCK: // global-setup.ts import { chromium } from '@playwright/test'; export default async () => { const browser = await chromium.launch(); const context = await browser.newContext(); const page = await context.newPage(); await page.goto('https://my-app.com/login'); await page.fill('#email', '[email protected]'); await page.fill('#password', 'password'); await page.click('button[type=submit]'); await context.storageState({ path: 'auth.json' }); await browser.close(); }; Enter fullscreen mode Exit fullscreen mode COMMAND_BLOCK: // global-setup.ts import { chromium } from '@playwright/test'; export default async () => { const browser = await chromium.launch(); const context = await browser.newContext(); const page = await context.newPage(); await page.goto('https://my-app.com/login'); await page.fill('#email', '[email protected]'); await page.fill('#password', 'password'); await page.click('button[type=submit]'); await context.storageState({ path: 'auth.json' }); await browser.close(); }; COMMAND_BLOCK: // global-setup.ts import { chromium } from '@playwright/test'; export default async () => { const browser = await chromium.launch(); const context = await browser.newContext(); const page = await context.newPage(); await page.goto('https://my-app.com/login'); await page.fill('#email', '[email protected]'); await page.fill('#password', 'password'); await page.click('button[type=submit]'); await context.storageState({ path: 'auth.json' }); await browser.close(); }; CODE_BLOCK: export default defineConfig({ globalSetup: './global-setup.ts', use: { storageState: 'auth.json', }, }); Enter fullscreen mode Exit fullscreen mode CODE_BLOCK: export default defineConfig({ globalSetup: './global-setup.ts', use: { storageState: 'auth.json', }, }); CODE_BLOCK: export default defineConfig({ globalSetup: './global-setup.ts', use: { storageState: 'auth.json', }, }); COMMAND_BLOCK: let sharedPage; beforeAll(async ({ browser }) => { const context = await browser.newContext(); sharedPage = await context.newPage(); }); Enter fullscreen mode Exit fullscreen mode COMMAND_BLOCK: let sharedPage; beforeAll(async ({ browser }) => { const context = await browser.newContext(); sharedPage = await context.newPage(); }); COMMAND_BLOCK: let sharedPage; beforeAll(async ({ browser }) => { const context = await browser.newContext(); sharedPage = await context.newPage(); }); COMMAND_BLOCK: import { test as base } from '@playwright/test'; export const test = base.extend({ authenticatedPage: async ({ page }, use) => { await page.goto('/dashboard'); await use(page); }, }); Enter fullscreen mode Exit fullscreen mode COMMAND_BLOCK: import { test as base } from '@playwright/test'; export const test = base.extend({ authenticatedPage: async ({ page }, use) => { await page.goto('/dashboard'); await use(page); }, }); COMMAND_BLOCK: import { test as base } from '@playwright/test'; export const test = base.extend({ authenticatedPage: async ({ page }, use) => { await page.goto('/dashboard'); await use(page); }, }); COMMAND_BLOCK: test('admin invites user', async ({ browser }) => { const adminContext = await browser.newContext({ storageState: 'admin.json' }); const userContext = await browser.newContext({ storageState: 'user.json' }); const adminPage = await adminContext.newPage(); const userPage = await userContext.newPage(); await adminPage.goto('/admin'); await userPage.goto('/dashboard'); }); Enter fullscreen mode Exit fullscreen mode COMMAND_BLOCK: test('admin invites user', async ({ browser }) => { const adminContext = await browser.newContext({ storageState: 'admin.json' }); const userContext = await browser.newContext({ storageState: 'user.json' }); const adminPage = await adminContext.newPage(); const userPage = await userContext.newPage(); await adminPage.goto('/admin'); await userPage.goto('/dashboard'); }); COMMAND_BLOCK: test('admin invites user', async ({ browser }) => { const adminContext = await browser.newContext({ storageState: 'admin.json' }); const userContext = await browser.newContext({ storageState: 'user.json' }); const adminPage = await adminContext.newPage(); const userPage = await userContext.newPage(); await adminPage.goto('/admin'); await userPage.goto('/dashboard'); }); CODE_BLOCK: export default defineConfig({ projects: [ { name: 'guest', use: { storageState: undefined, }, }, { name: 'user', use: { storageState: 'auth.json', }, }, { name: 'admin', use: { storageState: 'admin.json', }, }, ], }); Enter fullscreen mode Exit fullscreen mode CODE_BLOCK: export default defineConfig({ projects: [ { name: 'guest', use: { storageState: undefined, }, }, { name: 'user', use: { storageState: 'auth.json', }, }, { name: 'admin', use: { storageState: 'admin.json', }, }, ], }); CODE_BLOCK: export default defineConfig({ projects: [ { name: 'guest', use: { storageState: undefined, }, }, { name: 'user', use: { storageState: 'auth.json', }, }, { name: 'admin', use: { storageState: 'admin.json', }, }, ], }); CODE_BLOCK: npx playwright test --project=admin Enter fullscreen mode Exit fullscreen mode CODE_BLOCK: npx playwright test --project=admin CODE_BLOCK: npx playwright test --project=admin - What a BrowserContext actually is (and why it exists) - How Playwright creates and manages contexts for you - How to configure contexts globally in playwright.config.ts - How to share setup across tests without sharing state - When and how to use multiple setup files / projects - Common anti-patterns and real-world use cases - One Browser → the actual Chrome / Firefox / WebKit instance - Multiple BrowserContexts → separate incognito-like sessions - LocalStorage / SessionStorage - Permissions - Playwright launches a browser - Creates a new browser context - Creates a page inside that context - Destroys the context after the test - No state leakage between tests - Safe parallel execution - Predictable failures - The same viewport - Same locale - Same timezone - Starts logged in - Still runs in a fresh browser context - Breaks test isolation - Breaks parallelism - Causes order-dependent failures - Its own context - Its own page - Logged-in user vs logged-out user - Admin vs regular user - Mobile vs desktop - Different feature flags - Uses the same tests - Spins up different browser contexts - Runs in parallel if you want - Config (use) - Storage snapshots (storageState) - Fixtures and helpers - Live contexts - Mutable global state - Why Playwright scales - Why parallel runs work - Why tests don’t leak state - beforeAll creating a page - Globals holding page or context - Tests depending on previous navigation - Slow down your suite - Increase flakiness - Add zero test value - Parallel execution - Isolation guarantees - Debuggability - Unreadable tests - Fake mocks instead of real behavior - Missed bugs - More reliable - Easier to scale - Easier to reason about