Authenticate once, test everywhere

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

When your test suite grows, it's likely that your tests do the same things over and over again. Logging in is a very common example. Wouldn't it be great if we could log in 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 }) in a setup file.

A setup file is just a regular spec file that performs some actions and writes the resulting state to disk. The example below logs into the store and then persists the browser state.

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("Your name").fill("workshop");  await page.getByLabel("Your password").fill("12345678");  await page.getByRole("button", { name: "Login" }).click();  await expect(page.getByTestId("login-name")).toHaveText("workshop");  await page.context().storageState({ path: authFile });});

The state file should look like this:

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

That's pretty sweet! But now the question is: how and when should you run this spec and 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

You can read the storage state with a quick one-liner in your spec files.

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

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

export default defineConfig({
  projects: [
    {
      name: "setup", // 1.
      testMatch: "*.setup.ts",
    },
    {
      // run the `setup` tests before these
      name: "cart",
      use: { storageState: "auth.json" }, // 4.
      testMatch: "*.spec.ts",
      dependencies: ["setup"], // 3.
    },
  ],
});

This playwright.config does multiple things at once:

  1. it defines a setup project matching *.setup.ts files
  2. the setup project will run tests that create the storage state files (auth.json, admin.json, account.json, etc.)
  3. the cart project depends on setup so that setup always runs first
  4. the cart project reads auth.json and applies it to all its tests

Beautiful!

Warning

Storage state goes stale. Cookies expire, sessions get revoked, and fixture data resets. All of these can lead to a wall of red tests that fail on the first assertion after the login. 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. 🎉