Playwright Fixture Scopes Break Scenario Isolation
Playwright's fixture system is one of its genuine strengths — composable, typed, and far less ceremonial than Selenium's @Before/@After tangle. But when you layer a BDD runner on top of it (playwright-bdd, Cucumber-JVM 7 with a Playwright driver, or Behave with async page objects), fixture scope becomes a silent threat. A fixture declared at the worker scope does not reset between scenarios. Most teams don't notice until a test that passes in isolation starts failing in suite order — and the failure points to the wrong scenario entirely.
The problem compounds in BDD specifically because Gherkin encourages shared setup through Background steps. Those steps run before every scenario, but if the fixture they call is worker-scoped, the "reset" is an illusion — you're calling setup code against state that was never torn down. The same class of bug shows up when Background steps silently corrupt scenario isolation through shared object references rather than fixture scope.
By the end of this article you'll be able to audit your Playwright fixture graph for scope leaks, understand exactly where BDD runners diverge from raw Playwright test isolation, and apply targeted fixes without rewriting your suite.
Securely manage keys for 60+ AI providers in one encrypted vault instead of juggling them across apps.
Playwright Fixture Scopes in a BDD Context: What the Runtime Actually Does
Playwright has three fixture scopes: test (torn down after each test), worker (shared across all tests on a worker process), and project (shared across the entire run). In vanilla Playwright Test, "test" maps cleanly to one test() block. In a BDD runner, "test" maps to one scenario — but only if the runner creates a new fixture context per scenario. Several popular integrations don't do this by default.
playwright-bdd (the most widely adopted Playwright-native BDD adapter) wraps each scenario in a test() call, so test-scoped fixtures do reset correctly. The trap is fixtures you promote to worker scope for performance — authenticated browser contexts, database seeders, Kafka consumers. Those objects persist across scenarios on the same worker, and any mutation (navigation, session cookie change, consumed message) bleeds forward. This is architecturally identical to the leakage pattern documented for browser context pooling breaking scenario isolation, except here the leak is invisible in the fixture declaration itself.
Auditing and Fixing Scope Leaks in a Playwright BDD Suite
Start by printing your fixture dependency graph. playwright-bdd exposes a --list flag; pair it with a grep for scope: 'worker' in your fixture files to surface every worker-scoped object your scenarios touch.
# list all fixtures and their resolved scopes
npx bddgen --config=playwright.config.ts --list 2>&1 | grep -E "(fixture|scope)"
For each worker-scoped fixture that carries mutable state — a logged-in page, a seeded database connection, a Pulsar consumer — ask whether the performance gain justifies the isolation risk. Authentication is the most common offender. Teams cache a signed-in browser context at worker scope to avoid re-running OAuth flows for every scenario. The correct fix is storageState, not scope promotion.
// fixtures/auth.ts (playwright-bdd style)
import { test as base } from 'playwright-bdd';
export const test = base.extend<{ authedPage: Page }>({
authedPage: async ({ browser }, use) => {
// test-scoped: new context per scenario, but storageState
// means no live OAuth round-trip
const ctx = await browser.newContext({
storageState: './auth-state.json',
});
const page = await ctx.newPage();
await use(page);
await ctx.close(); // explicit teardown — don't rely on implicit
},
});
This keeps the fixture at test scope while paying the storage-state cost (~80 ms) rather than the full OAuth cost (~1.8 s). On a 200-scenario suite that previously ran auth at worker scope, one team reduced intermittent cross-scenario failures from ~12 per run to zero, with a wall-clock increase of under 30 seconds — an acceptable trade.
Playwright BDD vs Selenium BDD: Scope Handling Compared
A common search question is python selenium bdd vs playwright typescript — specifically whether Behave + Selenium handles this better. It doesn't; it just fails differently. Behave's before_scenario hook gives you a teardown hook you control explicitly, but there's no type-safe fixture graph, so scope leaks become invisible global state on the context object. Playwright's fixture system at least makes scope declarable — the bug is that engineers declare worker scope without understanding the BDD lifecycle consequence.
For teams evaluating a playwright bdd framework vs staying on Selenium 4 + Cucumber-JVM 7: Playwright wins on async-native browser control, auto-waiting, and network interception. Selenium 4 wins when you need multi-browser coverage beyond Chromium/Firefox/WebKit, or when your org has deep Java investment and Cucumber-JVM's step-binding ecosystem. The fixture scope problem is Playwright-specific, but Selenium's equivalent — shared WebDriver instances across scenarios — is just as damaging and harder to detect because there's no graph to inspect.
YAML Pipeline: Shard Safely Without Promoting Scope
# .github/workflows/e2e.yml
jobs:
e2e:
strategy:
matrix:
shard: [1, 2, 3, 4]
steps:
- run: npx playwright test --shard=${{ matrix.shard }}/4
env:
# Each shard gets its own storageState — no shared auth context
STORAGE_STATE: ./auth-state-shard-${{ matrix.shard }}.json
Sharding amplifies scope leaks because workers are distributed across machines — a worker-scoped fixture on shard 2 can't even see shard 1's state, so failures become non-deterministic across runs. AI test agents misrouting fixture calls across shard boundaries is the same class of problem at a higher level of abstraction. Keep fixtures test-scoped; let storageState and seed scripts handle the performance problem.
Where Senior Engineers Still Get Burned by Fixture Scope
The most common mistake is inheriting a fixture without checking its declared scope. You extend a base fixture file someone else wrote, your new fixture depends on authedPage, and you assume it resets per scenario because it's a "page" object. If the upstream declaration is worker-scoped, your fixture inherits that scope regardless of what you declare. Playwright resolves the scope of a fixture to the broadest scope in its dependency chain. Read the source; don't assume.
The second mistake is using worker-scoped fixtures for database seeders because seeding is slow. The correct architecture is a seed-once script run before the suite (a global setup file in playwright.config.ts) that writes deterministic, non-overlapping data partitions per worker. Each scenario then reads from its partition without mutating shared rows. This also connects to hook execution order determining suite-wide fixture reliability — global setup runs once, worker setup runs per worker, test setup runs per scenario; conflating these layers is where isolation breaks.
Myths About BDD Isolation That Persist in Mature Teams
Myth 1: "Our scenarios are independent because each one has its own Background." Background steps run per scenario, but they call fixtures. If those fixtures are worker-scoped, the Background is re-executing setup logic against already-mutated state. The step passes, the fixture is dirty. This is why Background-driven isolation is a convention, not a guarantee — the fixture graph determines actual isolation, not Gherkin structure.
Myth 2: "Playwright handles teardown automatically, so explicit close() calls are redundant." Playwright does clean up contexts when a worker exits, but not between test-scoped fixture uses if you forget await ctx.close() inside the fixture's use() wrapper. Leaked contexts accumulate open network connections, which causes subtle timing failures on slower CI runners — not crashes, just flakiness that's hard to attribute. Explicit teardown inside the fixture body is not redundant; it's the contract. Myth 3: "Selenium BDD is more predictable here." Selenium's WebDriver lifecycle is more explicit, but that explicitness shifts the burden to every hook author. Playwright's fixture graph is stricter and more auditable once you understand scope inheritance.
Fixture scope bugs in BDD suites are infrastructure bugs, not test bugs — they survive refactors and fool coverage metrics. The immediate next step: run npx bddgen --list against your suite and flag every worker-scoped fixture that holds mutable browser or database state. Replace scope promotion with storageState or partitioned seed data. Once isolation is solid, the next metric worth tracking is mean-time-to-detect on flaky scenarios — because clean isolation makes flakiness attributable rather than random.
Note: This article is for informational purposes only and is not a substitute for professional advice. If you need guidance on specific situations described in this article, consider consulting a qualified professional.