Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Organize contexts around the browser state that must be shared. In Playwright, keep related tabs and popups for one user in a single BrowserContext; create a separate context for each independent test, user identity, or isolation boundary. A context is the session, while a Page is a tab inside that session. WebDriver BiDi uses different terms, so apply its user-context model rather than assuming that a BiDi browsing context is equivalent to Playwright’s BrowserContext.

The three levels of browser state

Playwright has a simple hierarchy:

  • Browser: the launched or connected browser process.
  • BrowserContext: an isolated browser session with its own cookies, local storage, session storage, permissions and other session-level state.
  • Page: a tab or document inside a context. Several pages can share one context.

A popup opened by a page remains in that page’s context, so it sees the same session state. Non-persistent contexts keep data in memory and do not write a browsing profile to disk.

The practical rule is: use a new context when state should be isolated; use another page when state should be shared.

Choose the boundary before writing the test

Question Same context Separate context
Do the pages represent one signed-in user? Yes, including related tabs and popups No, create one per identity
Should cookies and storage carry over? Yes No
Is this an independent test? Only when intentional shared state is part of the scenario Normally, one clean context per test
Are two actors interacting, such as buyer and seller? Only pages belonging to the same actor One context for each actor
Must a profile survive browser restarts? Use a deliberately managed persistent profile Use separate user-data directories for separate profiles

Playwright’s test runner creates an isolated context for each test by default. When using the library directly, your code owns context creation and shutdown, so make that lifecycle visible in fixtures or helper functions.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Organize a single-user workflow

Keep pages together when they are parts of one user journey. For example, an account page may open a billing popup and a documentation tab. All three should be pages in one context so the login cookie and storage are available everywhere.

import { chromium } from 'playwright';

const browser = await chromium.launch();
const context = await browser.newContext();
try {
  const account = await context.newPage();
  await account.goto('https://example.test/account');

  const popupPromise = context.waitForEvent('page');
  await account.getByRole('button', { name: 'Open billing' }).click();
  const billing = await popupPromise;
  await billing.waitForLoadState();

  const docs = await context.newPage();
  await docs.goto('https://example.test/docs');
} finally {
  await context.close();
  await browser.close();
}

The popup is captured from the context because it belongs to that session. Closing the context closes its pages and gives Playwright a chance to flush artifacts before the browser closes.

Give every independent test a clean context

Tests should not depend on execution order. Reusing a context can leak cookies, local storage, permissions, service-worker state or visited-link state. Some browser state is difficult to remove reliably, so starting clean is safer than attempting a partial reset.

import { test, expect } from '@playwright/test';

test('customer sees an empty cart', async ({ page }) => {
  await page.goto('https://shop.example.test/cart');
  await expect(page.getByText('Your cart is empty')).toBeVisible();
});

test('customer can add a product', async ({ page }) => {
  await page.goto('https://shop.example.test/products/widget');
  await page.getByRole('button', { name: 'Add to cart' }).click();
  await expect(page.getByTestId('cart-count')).toHaveText('1');
});

The runner supplies a page in a fresh context for each test. If you create contexts manually, use a fixture or try/finally block so every path closes it.

What’s actually slowing this PC down?

Pick the symptom - the matching free tool is one click away.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Model multiple users with multiple contexts

Multi-user tests are the main reason to create several contexts in one test. Put all pages for the administrator in one context and all pages for the customer in another. This prevents one actor’s cookies or storage from silently authenticating the other.

import { test, expect } from '@playwright/test';

test('admin approves a customer request', async ({ browser }) => {
  const adminContext = await browser.newContext({ storageState: 'playwright/.auth/admin.json' });
  const customerContext = await browser.newContext({ storageState: 'playwright/.auth/customer.json' });
  try {
    const admin = await adminContext.newPage();
    const customer = await customerContext.newPage();

    await customer.goto('https://app.example.test/request');
    await customer.getByRole('button', { name: 'Submit' }).click();

    await admin.goto('https://app.example.test/admin/requests');
    await admin.getByRole('button', { name: 'Approve' }).click();

    await customer.reload();
    await expect(customer.getByText('Approved')).toBeVisible();
  } finally {
    await Promise.all([adminContext.close(), customerContext.close()]);
  }
});

Keep contexts separate even when they run in the same browser process. The browser is a process-level container; the context is the isolation boundary your test should reason about.

Make authentication state explicit

Saved storage state is useful when logging in for every test would be slow or would trigger rate limits. Treat each state file as an input tied to one identity, not as a general-purpose shortcut.

import { chromium } from 'playwright';

const browser = await chromium.launch();
const context = await browser.newContext({
  storageState: 'playwright/.auth/customer.json'
});
try {
  const page = await context.newPage();
  await page.goto('https://app.example.test/dashboard');
} finally {
  await context.close();
  await browser.close();
}
  • Generate an admin state and a customer state separately.
  • Store authentication files outside published artifacts and source control when they contain live credentials.
  • Do not load an admin state into a customer scenario merely to avoid a login step.
  • When a test needs two identities, initialize two contexts with their respective state files.

Persistent profiles need their own directories

A persistent context writes browser data to a user-data directory and is appropriate when an extension, profile setting or long-lived login must survive runs. Give automation a dedicated directory. Pointing Playwright at the everyday Chrome profile can prevent pages from loading or cause the browser to exit, and it risks corrupting or exposing personal browsing data.

Free tools Windows power users keep installed

One-click scans. No signup required.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
import { chromium } from 'playwright';

const context = await chromium.launchPersistentContext('./automation-profile', {
  headless: true
});
try {
  const page = await context.newPage();
  await page.goto('https://example.test');
} finally {
  await context.close();
}

Do not launch two processes against the same profile directory at once. Use a different directory for each concurrent persistent worker and remove or archive directories according to your test-retention policy.

WebDriver BiDi: do not mix the terminology

WebDriver BiDi describes a browsing context as a navigable, such as a tab, iframe or popup. It also defines user contexts. Tabs in one user context share browser storage; tabs in different user contexts are isolated. Selenium’s BiDi APIs expose operations for opening tabs or windows, navigating, and inspecting the context tree.

That means a BiDi browsing context is not automatically the same thing as Playwright’s BrowserContext. Before designing fixtures, identify which abstraction your client library uses: a tab-level browsing context, a storage-sharing user context, or a library-specific session object.

Lifecycle, cleanup and parallel execution

  • Create contexts as close as possible to the scenario that owns them.
  • Close explicitly created contexts before closing the browser.
  • Close pages only when you need to stop one tab early; closing the context is the complete cleanup operation.
  • Keep test data and saved authentication state aligned with the context’s identity.
  • When parallelizing or sharding, avoid shared mutable accounts unless the test deliberately models contention.

Playwright documents contexts as fast and cheap, but there is no universal safe concurrency number. CPU, memory, browser engine, page complexity, video or HAR recording, and the runner all change capacity. Measure your own workload and reduce workers when the host starts swapping, navigation time rises, or the application begins throttling requests.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Useful organization patterns

One test, one user

Use the runner’s supplied page, or create one context and several pages for a tabbed workflow. This is the default for most functional tests.

One test, several users

Create one context per actor, optionally loading a distinct storage state into each. Never simulate isolation by opening two pages in one context.

Independent API and UI setup

Use API calls or fixtures to prepare data, then create the UI context with only the authentication state required by the test. This keeps setup concerns separate from browser state.

Long-lived monitoring session

Use a persistent context only when persistence is the requirement. Otherwise prefer a fresh non-persistent context so stale state cannot hide regressions.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Troubleshooting context problems

A test is unexpectedly logged in

Cause: a context or persistent profile was reused. Fix: create a new context, remove unintended storageState, and verify the user-data directory is not shared.

A second tab cannot see the first tab’s session

Cause: the tabs were created in different contexts. Fix: create both with context.newPage(), or deliberately transfer the required authentication state into the new context.

Admin actions appear as the customer

Cause: both actors share one context or the wrong state file was loaded. Fix: use separate contexts and log the identity associated with each state file.

Runs become flaky after several tests

Cause: leaked contexts, pages or persistent profiles. Fix: close contexts in teardown, make cleanup run after failures, and give parallel persistent workers separate directories.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

The browser exits or pages fail to load with a profile

Cause: automation is using the regular Chrome user-data directory or a directory already locked by another process. Fix: create a dedicated directory and run one owner per directory.

Parallel runs overload the machine

Cause: the workload exceeds the host’s practical capacity; no universal context-per-worker limit exists. Fix: measure navigation time and memory under your real pages, then tune worker count and artifact recording.

Independent reader supportYour contribution helps us test, update, and keep practical guides available for everyone.Support on Ko-Fi

Or skip the browser setup

When the goal is a clean image or PDF rather than an interactive test, ScreenshotNeo provides a one-call website screenshot API and MCP server. It accepts consent banners before capture and removes more than 60 known consent platforms, newsletter popups and chat widgets; each cleanup step can be disabled. Bot checks or CAPTCHAs, blank pages, timeouts, failed loads and cache hits are not billed, and response headers identify the page verdict and billing result.

With a free API key, run:

curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://stripe.com -o shot.webp

See the complete parameter reference in the ScreenshotNeo documentation. The same endpoint supports PNG, JPEG or WebP, PDF output, full-page captures with lazy images loaded, CSS-selector element shots, dark mode, device presets, arbitrary viewports, retina scale, custom CSS and JavaScript, clicks before capture, hidden selectors, selector/delay/network-idle waits, request and resource blocking, custom headers/cookies/user agents, authorization, timezone and geolocation, transparent backgrounds, resizing, chosen cache TTLs, signed image links, asynchronous jobs with signed webhooks, bulk capture for up to 100 URLs per call, usage reporting and an OpenAPI specification. Parameter names used by other screenshot APIs also work, easing migrations.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Python:

import requests
r = requests.get("https://api.screenshotneo.com/v1/shot", params={"access_key": "YOUR_API_KEY", "url": "https://stripe.com"}, timeout=90)
open("shot.webp", "wb").write(r.content)

Node.js:

const q = new URLSearchParams({ access_key: 'YOUR_API_KEY', url: 'https://stripe.com' });
const res = await fetch(`https://api.screenshotneo.com/v1/shot?${q}`);

An MCP server exposes take_screenshot, get_page_info and capture_pdf to Claude, Cursor and other MCP clients, so an AI agent can perform captures without you maintaining browser fixtures.

Plan Included shots Price
Free 1,000/month $0, no card
Starter 3,000 $5
Growth 15,000 $15
Pro 60,000 $39
Scale 250,000 $99
Business 1,000,000 $249

Yearly billing provides two months free, and every feature is included on every plan. Create a free ScreenshotNeo account to get 1,000 screenshots a month without a card.

Decision checklist

  • Define whether cookies and storage should be shared.
  • Map each human or service identity to one context.
  • Put same-user tabs and popups in that context.
  • Use a fresh context for each independent test unless shared state is intentional.
  • Assign saved authentication state to the identity it represents.
  • Use dedicated directories for persistent profiles.
  • Close contexts before browsers and tune concurrency from measurements.
  • For BiDi, distinguish browsing contexts from user contexts before designing fixtures.

Frequently Asked Questions

Can several pages in one Playwright context use different accounts?

Not safely. Pages in one context share session storage and cookies, so use separate contexts for separate accounts.

Does closing a page clear its cookies?

No. Cookies belong to the context. Close the context when the session itself should end.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Should I use persistent contexts for every test?

No. Use non-persistent contexts for isolated tests; reserve persistent contexts for requirements such as extensions or state that must survive restarts.

Is a WebDriver BiDi browsing context the same as a Playwright BrowserContext?

No. BiDi’s browsing context is a navigable such as a tab, iframe or popup, while BiDi user contexts define storage sharing.

Product prices and availability are accurate as of the date/time indicated and are subject to change. Any price and availability information displayed on Amazon at the time of purchase will apply.