Welcome to Part 4 — Integration: making the API and UI layers work together.
This is what separates a toy suite from a real one, and we start with the highest-value
example — logging in.
The problem
Logging in through the form on every UI test is slow (load the page, type the
email, type the password, click, wait for the redirect) and repetitive. Do it on a
hundred tests and you've wasted real minutes per run.
The idea: save the session
When you log in, the browser stores proof that you're logged in — in cookies and
in localStorage (a small key/value store the page keeps in the browser). Together
these are your session.
Playwright can save that session to a file and load it back into other tests.
A test that loads it opens the app already logged in — no form needed. That saved
file is called a storage state.
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 logs in once
Playwright lets you run a setup project — a special test file that runs before
your other tests and prepares things they need. Ours logs in and saves the session.
Here's the integration twist: instead of driving the slow login form, we log in
through the API (one fast request), then write the session into the browser's
localStorage and save it:
// 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 clicking) and get 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 reads on load, into localStorage.
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. Save cookies + localStorage to a file.
await page.context().storageState({ path: authFile });
});
A couple of new things:
-
page.evaluate(fn, arg)runsfninside the browser page (wherelocalStoragelives) and passesarginto it. That's how we set the value the app expects. - We discovered the exact
localStorageshape (loggedUser) by reading Inkwell's source — Inkwell restores the session fromlocalStorage.getItem("loggedUser")on load. Knowing one small detail of the app under test is the essence of integration testing.
Wire it up with project dependencies
We tell Playwright "run setup before ui" using 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
You choose per file whether to start logged in. Our anonymous tests (home,
locators, login) stay logged out; only this file loads the saved session, with
test.use:
// src/tests/ui/authenticated.spec.ts
import { test, expect } from "@playwright/test";
test.use({ storageState: ".auth/playwright.json" }); // load the saved session
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 tests.
The
.auth/folder is git-ignored — it holds a live token and is regenerated by the
setup project on every run, so it never goes into version control.
When to use which login
- storageState (this chapter): the default for most authenticated tests — fast, shared, set up once.
-
Logging in through the form (
LoginPage): keep it for the few tests whose subject is the login flow — you still want to prove the form itself works (the Chapter 4 test stays exactly as it was).
Next up
We used the API to set up auth. Next we generalise that to all test data.
Chapter 16 — Seed via API, verify in UI: create an article through the API in
milliseconds, then check it renders in the browser — the 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)