DEV Community

kadir
kadir

Posted on

Auth Once with storageState (Playwright + TypeScript, Ch.15)

Welcome to Part 4 — Integration, the part that separates a toy suite from a real
one: making the API and UI layers work together. We start with the highest-leverage
example — authentication.

Logging in through the UI form on every test is slow (page load + type + submit +
redirect) and repetitive. Playwright's answer is storageState: capture the
browser session — cookies and localStorage — once, save it to disk, and load it
into any test so it opens already authenticated.

Code for this chapter is tagged ch-15 in the repo:
https://github.com/aktibaba/playwright-qa-course — see src/setup/auth.setup.ts,
playwright.config.ts, and src/tests/ui/authenticated.spec.ts.

A setup project that authenticates once

Playwright runs setup as a normal test file in its own project, which other
projects depend on. Here's the integration twist: instead of driving the login form,
we log in through the API (one fast request), then write the session into
localStorage exactly how Inkwell expects it, and save the storage state:

// src/setup/auth.setup.ts
import { test as setup, expect } from "@playwright/test";
import { env } from "@utils/env";
import { SEED_USERS } from "../fixtures/data.fixture";

const authFile = ".auth/playwright.json";

setup("authenticate", async ({ page, request }) => {
  const { email, password } = SEED_USERS.playwright;

  // 1. Log in via the API (no form interaction) and grab the token.
  const res = await request.post(`${env.apiURL}/users/login`, {
    data: { user: { email, password } },
  });
  expect(res.ok()).toBeTruthy();
  const { user } = await res.json();

  // 2. Write the exact session shape Inkwell restores from on load.
  const session = {
    headers: { Authorization: `Token ${user.token}` },
    isAuth: true,
    loggedUser: user,
  };
  await page.goto("/");
  await page.evaluate((v) => localStorage.setItem("loggedUser", JSON.stringify(v)), session);

  // 3. Persist cookies + localStorage to disk.
  await page.context().storageState({ path: authFile });
});
Enter fullscreen mode Exit fullscreen mode

Why this works: Inkwell's AuthContext initializes from
localStorage.getItem("loggedUser"), so a page that loads with that key populated
is logged in from the first render. We discovered that exact shape by reading the
app — the kind of small SUT detail integration tests depend on.

Wire it up with project dependencies

// playwright.config.ts
projects: [
  { name: "api", testDir: "./src/tests/api", use: { baseURL: env.apiURL } },
  {
    name: "setup",
    testDir: "./src/setup",
    testMatch: /auth\.setup\.ts/,
    use: { baseURL: env.webURL },
  },
  {
    name: "ui",
    testDir: "./src/tests/ui",
    dependencies: ["api", "setup"], // setup runs first → the auth file exists
    use: { baseURL: env.webURL, ...devices["Desktop Chrome"] },
  },
],
Enter fullscreen mode Exit fullscreen mode

Opt a test into the session

Crucially, you choose per file whether to start authenticated. Our anonymous tests
(home, locators, login) stay logged out; only this file loads the saved session:

// src/tests/ui/authenticated.spec.ts
import { test, expect } from "@playwright/test";

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

test("starts already logged in", async ({ page }) => {
  await page.goto("/");
  await expect(page.getByRole("link", { name: "New Article" })).toBeVisible();
  await expect(page.getByRole("navigation").getByText("playwright")).toBeVisible();
  await expect(page.getByRole("link", { name: "Sign up" })).toBeHidden();
});
Enter fullscreen mode Exit fullscreen mode

No LoginPage, no form, no redirect — the test opens the app and the user is already
there. Multiply that saving across a hundred authenticated tests.

The .auth/ folder is git-ignored — it holds a live token and is regenerated by
the setup project on every run.

When to use which login

  • storageState (this chapter): the default for most authenticated tests — fast, shared, set up once.
  • Logging in through the UI (LoginPage): keep it for the handful of tests whose subject is the login flow — you still want to prove the form itself works (Chapter 4's test stays exactly as it was).

Next up

We've used the API to set up auth. Next we generalize that to all test data.
Chapter 16 — Seed via API, verify in UI: create an article through the API in
milliseconds, then assert it renders in the browser — the integration pattern that
makes UI suites fast and reliable. Tag: ch-16.

Following along? Star the repo
and tell me how many seconds storageState shaved off your suite.

Top comments (0)