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-15in the repo:
https://github.com/aktibaba/playwright-qa-course — seesrc/setup/auth.setup.ts,
playwright.config.ts, andsrc/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 });
});
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"] },
},
],
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();
});
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)