A browser context is an isolated browser session with its own cookies, local storage, and permissions. One browser can hold several contexts at once, and nothing leaks between them, so contexts are how you test more than one user at a time and how you keep a logged-in session separate from an anonymous one. This guide covers how contexts work in Playwright, how to persist an authenticated session with storageState, and how to turn a logged-in journey into a scheduled monitor.
Understanding Browser Contexts#
A context carries the state that makes a session a session: cookies, local and session storage, granted permissions, geolocation, and the user agent. Pages opened in the same context share that state; pages in different contexts do not. That isolation is what makes contexts useful for testing:
- Run an admin session and a standard-user session in the same browser
- Keep each test on a clean slate instead of leaking cookies between runs
- Reuse a saved login so you do not sign in on every test
Creating and Managing Contexts#
Create a context from a launched browser and configure it at creation time:
import { chromium } from "playwright";const browser = await chromium.launch();// Two independent sessions in one browserconst adminContext = await browser.newContext({storageState: "admin-storage.json",});const userContext = await browser.newContext({permissions: ["notifications"],});const adminPage = await adminContext.newPage();const userPage = await userContext.newPage();// Contexts are cheap to create and should be closed when doneawait adminContext.close();await userContext.close();await browser.close();
import { chromium } from "playwright";const browser = await chromium.launch();// Two independent sessions in one browserconst adminContext = await browser.newContext({storageState: "admin-storage.json",});const userContext = await browser.newContext({permissions: ["notifications"],});const adminPage = await adminContext.newPage();const userPage = await userContext.newPage();// Contexts are cheap to create and should be closed when doneawait adminContext.close();await userContext.close();await browser.close();
Contexts accept the emulation options you would otherwise set per test: viewport, timezone, color scheme, geolocation, and permissions.
const context = await browser.newContext({viewport: { width: 1920, height: 1080 },timezoneId: "America/Chicago",colorScheme: "dark",permissions: ["geolocation"],});
const context = await browser.newContext({viewport: { width: 1920, height: 1080 },timezoneId: "America/Chicago",colorScheme: "dark",permissions: ["geolocation"],});
Cookies and Storage State#
The most valuable thing a context holds is an authenticated session. Rather than log in at the start of every test, log in once, capture the resulting state, and restore it:
// Capture the session after a successful loginawait context.storageState({ path: "user-storage.json" });// Restore it in a later run, already logged inconst context = await browser.newContext({storageState: "user-storage.json",});
// Capture the session after a successful loginawait context.storageState({ path: "user-storage.json" });// Restore it in a later run, already logged inconst context = await browser.newContext({storageState: "user-storage.json",});
You can also set cookies directly when you need a specific value:
await context.addCookies([{name: "session_token",value: "test_token_123",domain: "example.com",path: "/",expires: Date.now() / 1000 + 3600,},]);
await context.addCookies([{name: "session_token",value: "test_token_123",domain: "example.com",path: "/",expires: Date.now() / 1000 + 3600,},]);
A saved storageState file contains live session cookies. Treat it as a
secret: keep it out of version control and rotate it when it expires.
Running Authenticated Flows as Monitors#
Saving a storageState file works on your own machine, but it does not fit continuous monitoring. A stored session expires, and a monitor that logs in fresh on each run is what tells you the login itself still works. That means the check needs credentials at run time, not a stale session file.
ObserveOne's Autopilot handles the credential side. You add login values as named variables, for example LOGIN_USER and LOGIN_PASSWORD. The values are stored as write-only secrets and injected into the run as environment variables, so the generated test references them by name and never holds a literal password:
await page.goto("https://app.example.com/login");await page.getByLabel("Email").fill(process.env.LOGIN_USER!);await page.getByLabel("Password").fill(process.env.LOGIN_PASSWORD!);await page.getByRole("button", { name: "Sign in" }).click();await expect(page.getByRole("heading", { name: "Dashboard" })).toBeVisible();
await page.goto("https://app.example.com/login");await page.getByLabel("Email").fill(process.env.LOGIN_USER!);await page.getByLabel("Password").fill(process.env.LOGIN_PASSWORD!);await page.getByRole("button", { name: "Sign in" }).click();await expect(page.getByRole("heading", { name: "Dashboard" })).toBeVisible();
Autopilot generates the suite from your URL, fills and submits the auth form with the supplied credentials, asserts the post-login result, and runs the whole flow on a schedule. When the login markup shifts, the selector healer repoints the steps so the monitor survives the redesign instead of failing on it.
The trade-offs are worth stating plainly. This is credential-based login: the suite fills and submits the real auth form, it does not import an existing storageState file. Secret values are write-only, so you can update a credential but not read it back. And the run gives you logs and terminal output, not a video replay.
Permissions and Security Contexts#
Grant only the permissions a test needs, and keep download and CSP behavior explicit:
const context = await browser.newContext({permissions: ["geolocation"],acceptDownloads: false,bypassCSP: false,});
const context = await browser.newContext({permissions: ["geolocation"],acceptDownloads: false,bypassCSP: false,});
Follow least privilege in test contexts. Broad permission grants hide bugs that real users, with narrower grants, would hit.
Troubleshooting Browser Contexts#
Best Practices#
- Close contexts as soon as their work finishes
- Persist login once with
storageStateand reuse it across local tests - Treat saved session files and credentials as secrets
- Grant the narrowest permission set each test needs
- For anything that must stay green over time, run the authenticated flow on a schedule so a broken login surfaces as an alert, not a support ticket