skip to content

In Playwright, why might admin.json and viewer.json end up holding the same signed-in session?

level: seniorimportance: should knowfreq 32%

answer

  1. Both captures started already signed in
  2. Login page redirected past the form
  3. State written from an inherited context
  4. Open the capture context with no session
  5. Assert a role-specific landmark first

basics

~20 s

Because each capture ran in a context that was already signed in. With a state file supplied under use, the login page redirects straight to the console, the form steps do nothing, and the capture saves the inherited session under a new name.

solid answer

~40 s

The usual cause is an inherited session. A capture that runs inside a project whose `use` already sets `storageState` opens an authenticated context, so navigating to `/login` bounces to the dashboard, the fill and click steps either fail silently or match nothing, and `storageState({ path })` faithfully writes the cookies that were already there. Both role files then describe the same account. Capture each role from a clean context instead — `browser.newContext({ storageState: undefined })` — or run the captures in a step that does not inherit a signed-in `use`. Then prove it before saving: assert a landmark that only that role sees, so a capture that silently reused another identity fails at the capture rather than months later inside an unrelated spec.

code

typescript · 22 lines
typescript
import { test as setup, expect } from '@playwright/test';

const roles = [
  { name: 'admin', email: '[email protected]', landmark: 'Audit log' },
  { name: 'viewer', email: '[email protected]', landmark: 'My reports' },
];

for (const role of roles) {
  setup(`capture ${role.name} session`, async ({ browser }) => {
    const context = await browser.newContext({ storageState: undefined });
    const page = await context.newPage();

    await page.goto('/login');
    await page.getByLabel('Email').fill(role.email);
    await page.getByLabel('Password').fill(process.env.STAFF_PASSWORD!);
    await page.getByRole('button', { name: 'Sign in' }).click();
    await expect(page.getByRole('link', { name: role.landmark })).toBeVisible();

    await context.storageState({ path: `playwright/.auth/${role.name}.json` });
    await context.close();
  });
}

go deeper

for a junior

Know that a saved session file records whatever cookies the context happened to hold, so a capture that started signed in saves the wrong account under the right name.

for a middle

Explain the silent chain: the login route redirects, the form steps miss, and the write succeeds anyway with the inherited cookies.

for a senior

Diagnose from the symptom — role specs passing for the wrong reason — and fix it with a clean context per capture plus a role-specific assertion before the file is written.

for a principal

Insist that credential setup fails loudly: a capture that cannot prove which identity it holds is a silent correctness hole under every permission test in the suite.

## The symptom Two role files exist, they have different names and plausible timestamps, and every spec passes — including specs that assert a viewer *cannot* do something, which now pass for the wrong reason or fail unpredictably depending on which file was written last. Opening the two files side by side shows the same session cookie value in both. ## Why an inherited session hides itself Storage state supplied through `use` is applied when the context is created, before any test code runs. A capture step written as an ordinary test therefore starts signed in if the project it belongs to sets a state file. From there the failure is quiet at every stage: - `page.goto('/login')` is answered with a redirect to the console, because the app sees a valid session. - The email and password steps target fields that no longer exist, and the sign-in button lookup fails or resolves to something else entirely. - `page.context().storageState({ path: 'playwright/.auth/viewer.json' })` writes whatever cookies the context holds. It has no idea which account they belong to. Nothing in that chain is an error Playwright can raise on your behalf; the API did exactly what it was told. ## Capturing each role clean 1. Open the context explicitly with no session: `browser.newContext({ storageState: undefined })`, rather than reusing a `page` that inherited one. 2. Perform the sign-in for that role's own account. 3. Assert a landmark visible only after that role is authenticated — an admin-only navigation link, a viewer-only banner. 4. Write the file with a path derived from the role name, one path per role. 5. Close the context so nothing leaks into the next capture in the same file. Running each capture in its own step keeps step 5 cheap and makes a failure name the role that broke. ## Proving the capture before you save it The assertion in step 3 is the whole defence. Without it, a capture cannot distinguish "signed in as the viewer" from "was already signed in as the admin", and the defect propagates into every spec that loads the file. Pick a landmark that differs *between* roles rather than one that merely proves someone is signed in — a dashboard heading is visible to both roles and proves nothing about which one you captured. ## Other ways two roles collapse into one | Cause | What you see | The check that catches it | |---|---|---| | Capture inherited an existing session | identical cookies in both files | clean context plus a role-specific assertion | | Both captures used one account | identical cookies, correct file names | credentials sourced per role, not from one variable | | Two captures shared one context | the second file is the first role | a fresh context per role | | Both captures wrote one path | only one file, oddly named | the path derived from the role name | ## What this is not A role file that suddenly stops working across the whole suite is a different failure with a different shape — that is one session ending, not two roles merging. The signature here is specific: separate files, separate names, one identity inside them, and permission assertions that pass or fail depending on nothing the test changed. ## Guardrails worth keeping - Derive both the file path and the credentials from the same role record, so they cannot drift apart. - Keep the capture steps out of any project that supplies a signed-in `storageState` by default. - Give each role a distinct landmark assertion; reusing one landmark for every role removes the check. - Treat a capture failure as a run-stopping error rather than something to retry into a passing state.

  • How would you catch this class of defect automatically rather than by reading the files?
    Assert a role-specific landmark inside the capture, and add one negative spec per role that fails if the identity is wrong — a viewer spec expecting an admin-only control to be hidden. Both files being the admin session then breaks the capture or that spec immediately, not weeks later.
  • Is passing storageState: undefined the same as passing an empty state object?
    Both start the context without a session. `undefined` means the option is simply not applied, while `{ cookies: [], origins: [] }` supplies an explicitly empty state — useful as a value in `test.use`, where omitting the key would let an outer configuration's file win instead.

saying these in an interview costs you the question

  • Capturing a role inside an already signed-in context
  • Saving the state file without asserting the sign-in
  • Reusing one context for several role captures
  • Trusting different file names to mean different accounts
  • Using one shared landmark assertion for every role