PRACTICAL GUIDE / playwright multiple user roles storageState

Multi-Role Authentication in Playwright with Separate storageState Files

Set up multi-role Playwright authentication with separate storageState files, isolated projects, safe credentials, and reliable permission checks.

By The Testing AcademyUpdated July 11, 20269 min read
All field guides
In this guide10 sections
  1. Define the Role Contract First
  2. Generate One State File Per Role
  3. Bind Role State to Named Projects
  4. Assert Positive and Negative Authorization
  5. Use Multiple Contexts for Cross-Role Scenarios
  6. Manage State Lifetime and Account Ownership
  7. Diagnose Multi-Role Authentication Failures
  8. Choose Projects, Fixtures, or Per-File State
  9. Operational Checklist
  10. Action Plan

What you will learn

  • Define the Role Contract First
  • Generate One State File Per Role
  • Bind Role State to Named Projects
  • Assert Positive and Negative Authorization

Multi-role automation fails when "authenticated" is treated as one universal state. An administrator, support agent, editor, and ordinary member carry different cookies, claims, tenant membership, and server permissions. Give each role a separate Playwright storageState file, then make the selected role visible in the project or fixture that consumes it.

This arrangement does more than avoid repeated login screens. It creates a reviewable authorization model: a setup step produces role identity, a project binds identity to a test lane, and assertions prove both what the role may do and what it must not do.

Define the Role Contract First

List each role's allowed actions, denied actions, tenant scope, and test account ownership. "Admin" is too vague if one administrator manages billing while another manages content. Name roles after the permission boundary the product actually enforces, and keep account fixtures aligned with that boundary.

The Playwright authentication guide documents multiple signed-in roles by authenticating each role and saving separate state files. It also warns that stored browser state can contain sensitive cookies and headers. Put the auth directory in .gitignore, never attach raw states to public reports, and prefer dedicated accounts with the least privilege needed for the scenario.

Animated field map

Separate Authentication State for Each Role

Credentials enter isolated setup paths, produce distinct browser states, and feed role-specific contexts before permission checks run.

  1. 01 / role credentials

    Role credentials

    Load protected credentials for explicit admin, member, or support accounts.

  2. 02 / auth setup projects

    Auth setup projects

    Complete login and wait for the final authenticated application state.

  3. 03 / separate state files

    Separate state files

    Persist one ignored storageState artifact for each permission identity.

  4. 04 / role specific contexts

    Role-specific contexts

    Start projects or browser contexts with exactly one selected role state.

  5. 05 / permission assertions

    Permission assertions

    Verify allowed behavior and server-enforced denial for lower privilege roles.

Generate One State File Per Role

Create the files in a setup test so login has normal assertions, traces, and reporting. Wait for a post-login URL or a stable authenticated control before saving state; cookies may be set across redirects, and capturing too early produces a file that looks valid but opens as logged out.

TypeScript
import { expect, Page, test as setup } from "@playwright/test";

const adminState = "playwright/.auth/admin.json";
const memberState = "playwright/.auth/member.json";

function required(name: string): string {
  const value = process.env[name]?.trim();
  if (!value) throw new Error(`Missing required credential: ${name}`);
  return value;
}

async function signIn(page: Page, email: string, password: string) {
  await page.goto("/login");
  await page.getByLabel("Email").fill(email);
  await page.getByLabel("Password").fill(password);
  await page.getByRole("button", { name: "Sign in" }).click();
  await page.waitForURL("**/workspace");
  await expect(page.getByTestId("account-menu")).toBeVisible();
}

setup("authenticate administrator", async ({ page }) => {
  await signIn(
    page,
    required("E2E_ADMIN_EMAIL"),
    required("E2E_ADMIN_PASSWORD"),
  );
  await page.context().storageState({ path: adminState });
});

setup("authenticate member", async ({ page }) => {
  await signIn(
    page,
    required("E2E_MEMBER_EMAIL"),
    required("E2E_MEMBER_PASSWORD"),
  );
  await page.context().storageState({ path: memberState });
});

Keep state file names constant and role-specific. Do not derive paths from email addresses, because that leaks identity into logs and artifacts. If parallel workers mutate account state, allocate a distinct account per worker rather than generating several files from the same credentials.

Bind Role State to Named Projects

Projects make the role visible in CLI output and reports. An auth setup project can generate both files, while downstream projects select role-specific test files and load only their own state.

TypeScript
import { defineConfig, devices } from "@playwright/test";

export default defineConfig({
  testDir: "./tests",
  use: {
    baseURL: "https://staging.example.test",
    trace: "on-first-retry",
  },
  projects: [
    {
      name: "auth-setup",
      testMatch: /auth\.setup\.ts/,
    },
    {
      name: "admin-chromium",
      testMatch: /.*\.admin\.spec\.ts/,
      dependencies: ["auth-setup"],
      use: {
        ...devices["Desktop Chrome"],
        storageState: "playwright/.auth/admin.json",
      },
    },
    {
      name: "member-chromium",
      testMatch: /.*\.member\.spec\.ts/,
      dependencies: ["auth-setup"],
      use: {
        ...devices["Desktop Chrome"],
        storageState: "playwright/.auth/member.json",
      },
    },
  ],
});

This model is strongest when most tests use one role. If every file switches roles repeatedly, custom fixtures may be more concise. Regardless of mechanism, the test title or project name should reveal whose permissions are under examination.

Assert Positive and Negative Authorization

An admin test should prove a privileged operation completes, not merely that an "Admin" link is visible. A member test should prove the same operation is rejected. UI absence is useful for product behavior, but it is not evidence that the API enforces authorization.

TypeScript
import { expect, test } from "@playwright/test";

test("member cannot delete another user's workspace", async ({ page }) => {
  await page.goto("/workspaces/ws-42/settings");

  await expect(
    page.getByRole("button", { name: "Delete workspace" }),
  ).toHaveCount(0);

  const response = await page.request.delete("/api/workspaces/ws-42");
  expect(response.status()).toBe(403);

  await page.goto("/workspaces/ws-42");
  await expect(page.getByRole("heading", { name: "QA Workspace" })).toBeVisible();
});

Use a resource owned by another dedicated test account and reset it independently. If a failed authorization test can actually delete shared data, the fixture design has already created a serious hazard. Negative tests need disposable targets and cleanup that does not depend on the denied operation succeeding.

Use Multiple Contexts for Cross-Role Scenarios

Approval and moderation workflows require two identities in one test. Create one browser context per role because contexts have independent cookies and browser storage. Two pages in the same context remain the same signed-in user.

TypeScript
import { expect, test } from "@playwright/test";

test("administrator approves a member submission", async ({ browser }) => {
  const memberContext = await browser.newContext({
    storageState: "playwright/.auth/member.json",
  });
  const adminContext = await browser.newContext({
    storageState: "playwright/.auth/admin.json",
  });

  try {
    const memberPage = await memberContext.newPage();
    const adminPage = await adminContext.newPage();

    await memberPage.goto("/submissions/new");
    await memberPage.getByLabel("Title").fill("Release evidence");
    await memberPage.getByRole("button", { name: "Submit" }).click();
    await expect(memberPage.getByText("Awaiting approval")).toBeVisible();

    await adminPage.goto("/admin/submissions");
    await adminPage.getByRole("row", { name: /Release evidence/ })
      .getByRole("button", { name: "Approve" })
      .click();

    await memberPage.reload();
    await expect(memberPage.getByText("Approved")).toBeVisible();
  } finally {
    await memberContext.close();
    await adminContext.close();
  }
});

Coordinate through observable application state, not fixed delays. A database status, API response, notification, or eventually visible UI element is a useful handoff point. Keep the scenario focused; a long chain of role switches makes the first failure obscure later authorization evidence.

Manage State Lifetime and Account Ownership

Storage state is a snapshot, not an immortal login. Server sessions can expire or be revoked even while the JSON file remains unchanged. Generate states close to the test run, and avoid sharing cached files across environments. Cookie domain, secure flags, tenant hostnames, and identity-provider policy can make a state valid on one base URL and useless on another.

Account ownership matters just as much. Read-only roles may safely reuse an account across parallel tests if the application does not track mutable per-user state. Roles that approve items, change preferences, or consume one-time notifications need a worker account pool or unique data namespace. A separate state file does not isolate backend records by itself.

Diagnose Multi-Role Authentication Failures

If every role opens as the same user, inspect file paths first. Two setup tests may be overwriting one path, or both projects may point to admin.json. Attach only the role name and cookie metadata needed for diagnosis, never full cookie values.

If login succeeds in setup but downstream tests redirect to login, confirm that setup waited for the final authenticated state. Then check base URL, cookie domain, secure scheme, expiration, and whether authentication lives in IndexedDB rather than cookies or local storage. If failures appear only in parallel, look for shared-account mutation and server-side session replacement when the same account logs in twice.

A 403 in the admin lane may be a stale claim rather than a locator issue. Verify the account's assigned role through a safe identity endpoint and recreate state after permission changes. Never "fix" the test by weakening the assertion until the represented identity is known.

Choose Projects, Fixtures, or Per-File State

Role projects provide excellent report visibility and work well when test files naturally belong to one role. They can multiply browser projects if every role must run on every engine. Per-file test.use({ storageState }) is lightweight for a few exceptions but makes the role less visible at the suite level. Typed role fixtures offer reuse for complex flows, at the cost of more lifecycle code.

Use the simplest model that keeps identity obvious. A common pattern is projects for the two dominant roles, plus a fixture that creates secondary contexts for cross-role scenarios. Avoid a universal helper that accepts any arbitrary state path; an allowlisted role type prevents accidental privilege selection.

Operational Checklist

  • Define permissions and tenant scope for every automated role.
  • Store each role in a unique, ignored storageState path.
  • Load credentials from protected runtime secrets.
  • Wait for a stable post-login signal before saving browser state.
  • Bind role state to clearly named projects or typed fixtures.
  • Test allowed actions and server-side denial of forbidden actions.
  • Use separate browser contexts when two roles interact in one scenario.
  • Allocate unique accounts when tests mutate per-user state.
  • Close manually created contexts in a finally block or fixture teardown.
  • Regenerate state after expiration, revocation, host, or permission changes.

Action Plan

Choose two roles with a meaningful permission difference and create one dedicated account for each. Add an auth setup test that writes separate ignored files, then bind those files to named projects. Implement one positive admin operation and one member denial at both UI and API boundaries. After those lanes are stable, add a two-context workflow only where role interaction is essential. This sequence makes identity, authorization, and state ownership explicit before the suite expands.

// LIVE COURSE / THE TESTING ACADEMY

Playwright Automation Mastery

Go beyond Selenium. Master Playwright with JS/TS in 90 days.

From the instructor behind this guide.

Playwright jobs are growing 8x faster than Selenium. 90 days / 75+ live hrs / Tue-Thu-Sat 7 AM IST.

Code PROMODE / 10% offJoin the batch

The Testing Academy editorial desk

Practical QA guidance built around test evidence, production tradeoffs, and interview-ready explanations.

Published July 11, 2026 / Reviewed July 11, 2026

PRIMARY REFERENCES

Verify the details at the source

QABattle guides are practical explanations. Product behavior, standards, and APIs can change, so use these primary references for the canonical details.

  1. 01
    Playwright documentation

    Microsoft

    Canonical API, locator, fixture, browser, and test-runner behavior.

  2. 02
    Playwright best practices

    Microsoft

    Official guidance for resilient tests, isolation, and user-facing locators.

  3. 03
    Web Security Testing Guide

    OWASP Foundation

    Primary testing scenarios for identity, authorization, input validation, and web security controls.

  4. 04
    OWASP Top 10

    OWASP Foundation

    Current high-level web application security risk taxonomy.

FAQ / QUICK ANSWERS

Questions testers ask

Should admin and member tests use the same Playwright storageState file?

No. Each role should have its own state file created from its own account. Sharing one file hides authorization mistakes and makes the role represented by a project or test ambiguous.

Where should Playwright role storage files be stored?

Use a dedicated ignored directory such as playwright/.auth. Treat every state file as sensitive because it can contain cookies or browser storage that impersonates the account until the state expires or is revoked.

Can one Playwright test use two authenticated roles at the same time?

Yes. Create separate browser contexts with the corresponding storageState files, then open one page in each context. This is appropriate for approval, moderation, support, and collaboration workflows.

How often should multi-role authentication state be regenerated?

Generate it in an authentication setup project for the current run or according to a short, controlled cache policy. Regenerate immediately when credentials, cookie domains, scopes, or server-side sessions change.

What should a role-based Playwright test assert besides visible UI?

Assert both allowed and denied capabilities. Check that permitted controls work, forbidden controls are absent or disabled, and protected endpoints reject a lower-privilege session so a cosmetic UI restriction cannot mask a server authorization defect.