Shared Auth State Pollutes E2E Tests
Short answer
One storageState.json logged in as admin makes every spec inherit admin powers—or stale cookies from Test A break Test B's viewer flow. Fix with per-spec auth seeds, role fixtures, and runId-scoped users—never a global login in globalSetup without namespacing.
Part of Common E2E testing gotchas.
Symptom
- Viewer spec passes alone, fails in suite: "Expected button Export to be hidden"
- Parallel CI: random 401s, wrong tenant, or session overwritten mid-run
- Logout test leaves next spec unauthenticated
- Admin actions appear in specs that should run as free tier
Root cause
Auth leaks across specs through shared mutable session state:
- Single
storageStatefile reused for every project globalSetuplogs in once asadmin@staging.comtest.use({ storageState: 'auth.json' })at config root- Cookie/localStorage not reset between workers
This is a special case of world-state mutation—sessions are world-state too.
Fix: role-scoped auth per spec
1. Seed user + token in Arrange (preferred)
const runId = `auth-${test.info().workerIndex}-${Date.now()}`;
test('viewer cannot export', async ({ page, request }) => {
const { cookieHeader } = await request.post('/api/test/seed-user', {
data: { runId, role: 'viewer' },
}).then(r => r.json());
await page.context().setExtraHTTPHeaders({ Cookie: cookieHeader });
await page.goto('/dashboard');
await expect(page.getByRole('button', { name: 'Export' })).toHaveCount(0);
});
Seed routes create the user and return session material keyed to runId—see seed routes and probe Assert.
2. Playwright projects per role (when UI login is required)
// playwright.config.ts — separate storageState per role, generated in setup
projects: [
{ name: 'viewer', use: { storageState: '.auth/viewer.json' } },
{ name: 'admin', use: { storageState: '.auth/admin.json' } },
],
Generate .auth/*.json in setup projects with unique emails per worker—not one shared admin file. See Playwright authentication.
3. Never mutate shared storageState in tests
// Bad — writes to file other workers read
await page.context().storageState({ path: 'auth.json' });
If you must persist state, use worker-scoped paths: .auth/worker-${workerIndex}-viewer.json.
Auth + parallel CI
Parallel workers amplify session races. Before raising timeouts:
- Reproduce with
npx playwright test --workers=4 - Audit
storageState,globalSetup, and shared test users - Add
runIdto every auth seed route
Full CI patterns: GitHub Actions parallel. RBAC depth: testing RBAC permissions.
Anti-patterns
| Anti-pattern | Why it fails | Better approach |
|---|---|---|
One admin storageState for suite | Viewer tests inherit admin | Role fixture per spec/project |
Login in beforeAll without isolation | Order-dependent; parallel races | Seed route returns session per runId |
| Logout in spec without Arrange | Next spec inherits logged-out state | Fresh context per test (default) |
| Reuse Firebase/Auth0 prod credentials | Quota, lockout, cross-tenant | Emulator or seed route per worker |
| Assert UI role badge only | Badge wrong; API still open | Probe protected endpoint with seeded token |
TestChimp workflow
/testchimp init scaffolds auth seed routes (POST /api/test/seed-user with role and runId) alongside cart and order fixtures. /testchimp test on PRs repairs specs that assumed a global admin session—agents read markdown scenarios for required roles.
Related
- World-state mutation
- E2E foundations
- Firebase auth E2E
- Playwright authentication
- Playwright test fixtures
Frequently asked questions
Is storageState bad?
No—it is bad when one file is shared across roles or workers. Generate per-role, per-worker state in setup projects, or prefer seed routes that return session headers per runId.
Should every test log in through the UI?
Only when the login flow itself is under test. For most specs, seed a session via API or set cookies from a test route—faster and more stable than typing credentials every time.
We use Auth0/Okta—how do we avoid shared sessions?
Use test tenants, programmatic user creation, or headed capture for smoke storageState with documented expiry. Never point all workers at one staging user—see Auth0/Okta guides in the auth section.
Logout test breaks the next spec—why?
Shared storageState or a mutated context. Each spec should Arrange its own session; do not rely on a previous spec leaving the browser in a known state.
Can parallel CI share one login if tests are read-only?
Read-only still races on session refresh, MFA, and rate limits. Unique users per worker cost little with seed routes.
How do probes help auth tests?
UI may show the wrong nav for a role while APIs still return 200. Probe protected endpoints with the seeded token—authoritative for RBAC.
Does /testchimp init add auth seed routes?
Yes—it scaffolds seed-user patterns with role and runId alongside other test-only routes so SmartTests stop depending on a single admin storageState file.
globalSetup vs per-test seed—which wins?
Per-test or per-worker seed wins for parallel CI. globalSetup is fine for one-time asset prep—not for mutable session state every spec consumes.
Apply these patterns in your repo
Run `/testchimp init` to connect TestChimp to your repo, then `/testchimp test` on PRs to turn these patterns into maintained SmartTests. Use `/testchimp evolve` when you want to expand coverage as your app grows.