JS Sketchbooksee JavaScript think ✏️
← back to phase 11

11.11 — Auth, storage state & sessions

Two hundred tests, each logging in through the UI first: seventeen minutes of pure login per run, and one flaky login form failing two hundred innocent tests. The cure: log in ONCE, bottle the session as a file, and hand every fresh context the bottle. 7.7 taught you what a session IS — today that knowledge becomes seventeen minutes.

watch it happen
// setup step: log in ONCE, bottle the session
await page.goto("/login");
await page.getByLabel("Email")
  .fill(process.env.TEST_USER);
await page.getByLabel("Password")
  .fill(process.env.TEST_PASS);
await page.getByRole("button", { name: "Log in" })
  .click();
await expect(page.getByText("Welcome")).toBeVisible();
await page.context()
  .storageState({ path: ".auth/shopper.json" });

// every test after: born logged in
test.use({ storageState: ".auth/shopper.json" });

test("cart persists", async ({ page }) => {
  await page.goto("/cart");   // already logged in
});

Price the problem first: a UI login — goto, fill, fill, click, wait for welcome — costs ~5 seconds. Times 200 tests, that’s ≈17 minutes PER RUN of logging in, before any test does its actual work. 10.2’s abandoned-suite math, self-inflicted.

the login tax, per testUI login (~5s) 🐌goto·fill·fill·clicktest 1’s ACTUAL workUI login (~5s) 🐌goto·fill·fill·clicktest 2’s ACTUAL workUI login (~5s) 🐌goto·fill·fill·clicktest 3’s ACTUAL work× 200 tests ≈ 17 minutes of logging inthe login tax: every test logging in through the UI = ~5s × 200 tests ≈ 17MINUTES of pure login, every run
under the hood

The deeper story, with the real names for things — this part is what turns “I saw it” into “I can explain it.”

Multiple personas scale the same way: bottle shopper.json, admin.json, new-user.json in the setup step, and different spec files test.use different bottles. Combined with fixtures (11.7), suites expose them as injectable names: { adminPage }.

Even faster than one UI login: API login — POST the credentials with the request fixture (11.10), receive the session cookie, write the storage state yourself. The UI login flow then gets exactly ONE dedicated test of its own (it still needs testing — it’s a feature!) while every other test skips it entirely. That division — test the login once, reuse auth everywhere else — is the professional standard.

What storageState does NOT capture: sessionStorage (7.7’s tab-scoped store) and IndexedDB. Most auth lives in cookies/localStorage so the bottle usually suffices — but when an app stubbornly logs you out despite the bottle, remember this list.

Job note: “how do you handle auth in your suite?” is a standard interview probe. The layered answer you now own: sessions are data (cookies/localStorage), so we log in once in a setup project — ideally via API — bottle storageState per persona, inject it into fresh contexts, keep bottles out of git, and regenerate per CI run so expiry can’t bite. That answer is a hiring signal.

your turn

⌨️ bottle the session

Model the whole economy: an expensive UI login, a bottle on the model disk (9.5’s trick), and three tests — run the suite the naive way, then the bottled way, and let the login counter tell the story.

requirements:

  • Keep the starter’s uiLogin — it counts every call and returns a session object.
  • Naive suite: run 3 tests, each calling uiLogin() itself. Then print naive: 3 logins — from the counter, not typed.
  • Bottled suite: reset the counter; call uiLogin() ONCE and store its result on the model disk under ".auth/shopper.json" (dynamic brackets); run 3 tests that each READ the bottle and print test N: sess_abc ✓ (the cookie from the loaded state).
  • Finish with bottled: 1 login — the counter again.

when you press RUN, the console must show exactly:

naive: 3 logins
test 1: sess_abc ✓
test 2: sess_abc ✓
test 3: sess_abc ✓
bottled: 1 login

✏️ Quick check 1

“Logged in” is ultimately made of what two browser-side stores? (7.7 knows)

✏️ Quick check 2

Tests share the storageState file. Does test 3’s cart-filling leak into test 5’s world? Type yes or no.

✏️ Quick check 3

The whole suite suddenly fails with 401s on every test. Your first suspect?

teach it back

🗣️ Now teach it back

Explain the auth strategy to a friend: the login tax (both costs), what a session actually is, the bottle-and-reuse mechanics, why isolation survives, and the expiry failure mode with its cure.

Write it as if your friend is sitting next to you. Saved to your journal — future-you will use these notes to teach others.

a few sentences, minimum — you’ve got this
to remember
The login tax: ~5s × N tests + the login form as a universal flaky dependency. A session = DATA (cookies + localStorage — 7.7), so it can be bottled: storageState → .auth/*.json (4.13 on 9.5’s disk).
Setup step logs in ONCE (env credentials — 9.4); test.use({ storageState }) births every fresh context WITH the bottle. Isolation survives: shared bottle, never a live session.
The bottle IS credentials → .gitignore. Sessions expire → mass 401s = stale bottle (5-second diagnosis); CI regenerates per run by design. Pro move: API login + one dedicated UI-login test.