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.
// 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 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.
⌨️ 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 printnaive: 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 printtest 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:
✏️ 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?
🗣️ 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.