Authenticate once, test everywhere

Learn how to implement a setup step and reuse browser state.

If you test suite grows it's likely that your tests do the same things over and over again. Login is a very common example. Wouldn't it be great if we could log in the user once and then reuse the browser state (cookies, localstorage) in other test cases?

You bet! This is where project configuration and the storageState method come in handy!

Write the current browser state to disk

Whenever you perform actions in your Playwright scripts you can write the current state to disk via page.context().storageState({ path }). A setup file is just a regular spec file with the purpose to drive some actions and dump the resulting state to disk.

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

const authFile = "./auth.json";

setup("authenticate", async ({ page }) => {
  await page.goto("/");
  await page.getByRole("link", { name: "Login" }).click();
  await page.getByLabel("Name").fill("workshop");
  await page.getByRole("button", { name: "Sign in" }).click();
  await expect(page.getByText("Welcome, workshop")).toBeVisible();

  await page.context().storageState({ path: authFile });
});

If you write the state to disk, it should look like this.

{
  "cookies": [
    {
      "name": "name",
      "value": "stefan",
      "domain": "www.playwright-workshop.online",
      "path": "/",
      "expires": -1,
      "httpOnly": false,
      "secure": false,
      "sameSite": "Lax"
    }
  ],
  "origins": []
}

That's pretty sweet! But now there's the questions, how and when should your run this spec or read the resulting JSON?

Note

Storage state files contain real session cookies. Add them to .gitignore (e.g. auth.json or a dedicated playwright/.auth/ directory) so you never commit a logged-in session to your repo.

Read the browser state in your test files

In a single spec file, you can read the storage state with a quick one-liner.

test.use({ storageState: "./.auth.json" });test.describe(/* ... */);

However, adding this line to every test is no fun. The recommended approach is to set up project dependencies and storage state in one go!

export default defineConfig({  // projects that leverage setup and storage state  // npx playwright test --project=storageState  projects: [    {      name: "setup",      testMatch: "*.setup.ts",    },    {      // run the `setup` tests before these      dependencies: ["setup"],      name: "cart",      use: { storageState: "auth.json" },      testMatch: "*.with-state.spec.ts",    },  ],});

This playwright.config does multiple things at once:

  1. it defines a setup project matching *.setup.ts files
  2. the setup project will then create the storage state file (auth.json)
  3. the cart project depends on setup so that setup is always run first
  4. the cart project reads auth.json and applies it to all running tests.

Beautiful!

Warning

Storage state goes stale. Cookies expire, sessions get revoked, and fixture data resets. All these scenarios can lead to a wall of red tests that all fail on the first assertion past the login wall. When that happens, delete the auth.json, re-run the setup project, and you're back in business.


Hands on

Reuse a logged-in browser state

Exercise 1 of 1

Move login into a setup project

  1. Create a *.setup.ts file that logs the user in and writes the browser state to disk via page.context().storageState({ path: authFile }).
  2. Add a setup project to playwright.config.ts that matches *.setup.ts.
  3. Add dependencies on setup, point use.storageState at the file you wrote, and authenticate all your tests.
  4. Make all your shop tests run in a logged in state from now on. 🎉