The Clean Boundary: Contexts, Fixtures, and State

Playwright can make a click precise and wait for the right UI condition, but that work is still misleading if the test begins inside someone else’s browser state. This chapter focuses on the boundary around a test: what a BrowserContext isolates, how pages and fixtures live inside it, and when authentication or setup should be reused. The practical goal is to decide what belongs to one test, one worker, or a shared setup—and to recognize the data that Playwright cannot isolate for you.

A browser context is the clean starting state

A browser is the process that runs an engine. A page is a tab-like surface inside that browser. Between them, Playwright provides a BrowserContext: an isolated, incognito-like profile with its own cookies, local storage, session storage, and related browser state. Playwright Test creates a context for each test by default and gives the test a page in that context. When the Library is used directly, the caller creates the context and page explicitly.

This boundary is valuable because cleanup is harder than creation. Reusing one profile requires a test suite to remember every piece of state it changed and to restore it correctly. Cookies can be deleted, but a visited-link state or a browser detail that affects rendering may be difficult or impossible to reset reliably. One forgotten value can make a later test pass for the wrong reason or fail only after another test has run. Starting a fresh context makes the initial condition explicit: the test receives a new browser profile instead of inheriting an evolving history.

The context is not only a container for cookies. It is the scope in which many conditions that affect a page are configured. Permissions, locale, timezone, color scheme, viewport, device parameters, routing, storage state, tracing, and API request behavior can be associated with the context. This gives a test one coherent environment: the page, the identity it sees, the network rules around it, and the evidence collected about it are not scattered across unrelated global switches.

A context can also represent a user boundary within one scenario. Imagine a chat application in which an administrator sends a message and a normal user receives it. Two pages in one browser could share too much state to model that cleanly. Two contexts can start with independent profiles, each with its own login state and pages. The browser process may be shared for efficiency, but the contexts provide separate browser identities. The test can then observe the relationship between two users without relying on accidental cookie separation or tab order.

stateDiagram-v2 [*] --> FreshContext FreshContext --> FixtureSetup: test requests a page FixtureSetup --> IsolatedPage: setup completes FreshContext --> SecondContext: multi-user scenario SecondContext --> IsolatedPage: independent user page IsolatedPage --> ClosedContext: test ends ClosedContext --> [*] FreshContext --> ExternalState: backend data account or file ExternalState --> IsolatedPage: must be controlled separately

The diagram shows two meanings of “fresh.” The browser-local profile can begin clean, while the backend record or account that the page reaches may already contain history. A new context does not create a new database, erase a shared account, or reserve an external service. It isolates what belongs to the browser profile; the application and test design must own the rest.

This distinction matters for parallel execution. If two tests each receive a new context but both use the same account to change the same server-side setting, the browser state is isolated while the business state is racing. The tests may interfere even though neither test shares a page or cookie jar. Context isolation removes one major source of carry-over; it does not turn shared resources into independent resources.

Context closure is the end of the browser-local lifecycle. Pages, routes, traces, downloads, and other context-owned resources are released when the context closes. In a managed test, the runner handles that lifecycle for its built-in fixtures. In a Library script, the caller remains responsible for closing the context and browser. The same conceptual boundary exists in both modes, but the owner of the transitions is different.

Fixtures decide what is created and reused

A context answers “where does this test run?” A fixture answers “what must be prepared before it runs, and how is that preparation cleaned up?” Playwright Test’s built-in page, context, browser, and request fixtures express these dependencies as arguments to a test. When a test asks for a page, the runner creates the relevant environment; it does not require every test to write its own browser launch and teardown sequence.

The scopes are deliberately different. The browser can be shared across tests to avoid starting a new process for every case, while the context and page remain isolated for the test that requests them. That arrangement separates expensive process ownership from the state that most often causes test contamination. Sharing a browser process does not mean sharing cookies or local storage. Conversely, asking for a shared fixture or worker-scoped resource is a decision to keep some state alive longer than one test, so the fixture must explain what may safely persist and what must be reset before another test uses it.

A useful way to make the choice is to follow the resource’s mutation. A page that records only browser-local navigation belongs naturally to a test-scoped context. A browser binary or a read-only helper can be shared more widely. A seeded account that is intentionally reserved for one worker may live for the worker’s lifetime, while a record that each test edits should be unique to the test. The scope should follow the possibility of interference, not the convenience of putting setup in a global hook. If the resource’s owner is unclear, the suite may be fast while its results remain difficult to reproduce.

This is more than a shorter setup syntax. Fixtures are isolated between tests, on-demand, reusable, and composable. A custom fixture can create a page object, prepare a dataset, or establish a feature-specific environment, then yield it to the test and perform cleanup afterward. Another fixture can depend on it. The relationship is visible in the test’s requested inputs rather than hidden in a module-level variable or a long chain of hooks.

Consider a todo page used by several tests. A conventional setup might navigate before each test and then remove every item afterward. That works only if the cleanup remains complete as the page evolves. A fixture can own the page object, add the starting items, provide the prepared page to the test, and remove the items after the test returns. The setup and teardown are adjacent because they describe one resource lifecycle. If a test does not request that fixture, its work is not performed merely because the helper exists.

The same scope decisions apply to authentication. For tests that do not modify shared server-side state, the documentation recommends authenticating once in a setup project, saving browser storage state, and reusing it in later tests. This avoids repeating a login flow while preserving a clean context for each test. The reused file supplies cookies and other state to a new context; it does not make the contexts identical in every respect, nor does it prove that the account is safe for concurrent mutation.

When tests do modify shared server-side state, the account strategy changes. A parallel worker can authenticate once with a distinct account and reuse that account for the tests assigned to the worker. The key is not the convenience of fewer logins. It is the relationship between the scope of the saved state and the scope of the data being changed. A single shared account is reasonable only when concurrent tests can act through it without affecting one another. If one test changes a setting while another checks that setting, the browser contexts may be separate but the account’s server-side state is not.

Project dependencies provide another kind of lifecycle control. A setup project can run before browser projects that depend on it, allowing authentication or other preparation to complete before the dependent tests begin. That establishes ordering; it does not automatically make the prepared resource safe for every concurrent consumer. Ordering can prevent a race between setup and use, but it cannot prevent two tests from later writing the same record.

It helps to distinguish preparation from participation. The setup project may perform the login interaction once and write a storage-state file. A later test does not continue using the setup page; it creates its own context and starts with the saved state as an input. That is why one login can support many clean browser sessions. It is also why an expired state file can make an entire group of tests fail at startup even though the individual pages and locators are correct. The setup lifecycle and the test lifecycle are related, but they are not the same session.

The choice changes again when a worker owns the state. A worker-scoped authentication fixture can create or select one account for that worker, use it for several tests, and clean up the account when the worker’s work ends. This is a compromise between repeated per-test login and unsafe sharing across all workers. It is appropriate only when the account’s data is partitioned well enough for those tests. If a test changes a global setting, a worker-specific login may still be insufficient; the server-side resource, not the browser session, determines the true isolation requirement.

Configuration scopes make the same idea visible at several levels. A setting can apply globally, to one project, or to one test. A project can select a browser and device profile while a test overrides a viewport or locale for a specific case. Context options inherited from configuration shape the browser environment that the test receives. The most maintainable arrangement is not the one with the fewest settings; it is the one in which a reader can tell why a state or option exists and how far it should reach.

Saved authentication state introduces a direct security boundary. A state file may contain cookies and headers that can be used to impersonate a test account. It should be kept out of source control and treated as sensitive output, even when the account is not a production account. A trace, screenshot, or report can also reveal the authenticated page after the state is loaded. Reproducibility does not reduce the sensitivity of the data used to achieve it.

The important contrast is between reuse and leakage. Reuse is deliberate when a setup project creates a known state, a fixture owns its lifetime, and a project or worker defines who may share it. Leakage is accidental when a test depends on a file, account, page, or module variable simply because a previous test happened to leave it behind. Playwright’s context and fixture model makes deliberate reuse possible while giving the suite a fresh browser boundary by default.

A complete test therefore has at least two state questions. What must be new for this test to make its browser behavior reproducible? What must be unique or coordinated outside the browser so that its application behavior is reproducible? Browser contexts and fixtures answer the first question. Account allocation, data identifiers, locks, service contracts, and cleanup answer the second. Confusing these scopes is how a suite can appear isolated while still producing order-dependent results.

The clean boundary is not a promise that every run starts from a blank universe. It is a precise ownership rule: Playwright can create a fresh browser profile, provide scoped setup, and reuse state by design; the team must decide whether the external data and credentials behind that profile are independent, safe, and appropriate to share. Once interaction and state are viewed together, a single scenario can be examined as a chain of controlled conditions rather than as a click sequence that happens to pass.

References