Chapter 9 · Auth State Reuse with storage_state
Chapter goals: Understand Playwright's auth reuse model, learn to save and load
storage_state, manage multiple login states by role, and make all tests "boot already logged in" via pytest fixtures.
9.1 Why Authentication Slows Down E2E
Most web app tests cannot avoid login. If every test walks through the login form from scratch: 2–5 extra seconds per case, login page UI changes break all tests, and CAPTCHA/2FA become automation nightmares.
Playwright's solution is built on its isolation model: auth state (cookies, localStorage, IndexedDB) lives on the BrowserContext, which can export that state as a JSON file and inject it into any new context — this is storage_state:
# ① Log in once, save state to disk
page.goto("https://github.com/login")
page.get_by_label("Username or email address").fill("username")
page.get_by_label("Password").fill("password")
page.get_by_role("button", name="Sign in").click()
page.wait_for_load_state()
storage = context.storage_state(path="state.json") # Export to file
# ② Any context created after can "spawn already logged in"
authed = browser.new_context(storage_state="state.json")
page2 = authed.new_page()
page2.goto("https://github.com") # No login flow neededSecurity warning
The official docs strongly advise: state files contain sensitive Cookies and tokens that can impersonate your account. Add the storage directory (e.g. playwright/.auth/) to .gitignore — never commit to the repo.
9.2 Managing Multiple Roles
Systems with complex permissions need "admin view" and "regular user view" tested separately. The approach: save one state file per role:
import os
os.makedirs("playwright/.auth", exist_ok=True)
def login_and_save(browser, creds, out_path):
"""Generic login routine: export storage_state after successful login"""
context = browser.new_context()
page = context.new_page()
page.goto("https://myapp.example/login")
page.get_by_label("Username").fill(creds["user"])
page.get_by_label("Password").fill(creds["pass"])
page.get_by_role("button", name="Log in").click()
expect(page.get_by_text("Dashboard")).to_be_visible() # Confirm login succeeded
context.storage_state(path=out_path) # Persist by role
context.close()
login_and_save(browser, ADMIN_CREDS, "playwright/.auth/admin.json")
login_and_save(browser, USER_CREDS, "playwright/.auth/user.json")
# Each test suite loads the role-specific state
admin_ctx = browser.new_context(storage_state="playwright/.auth/admin.json")
user_ctx = browser.new_context(storage_state="playwright/.auth/user.json")9.3 Loading Auth State via pytest Fixtures
Combining with the browser fixture from pytest-playwright, write an auth_page fixture so dependent tests auto-receive a logged-in page:
# conftest.py
import pytest
from playwright.sync_api import expect, Page
@pytest.fixture(scope="session") # Session-scoped: log in once per test run
def auth_context(browser, base_url):
context = browser.new_context(
storage_state="playwright/.auth/user.json",
base_url=base_url,
)
yield context
context.close()
@pytest.fixture
def auth_page(auth_context) -> Page:
page = auth_context.new_page()
yield page
@pytest.fixture(scope="session", autouse=False)
def ensure_auth_state(browser, base_url):
"""Generate state file on the fly if it doesn't exist (first run / CI cold-start fallback)"""
import os
path = "playwright/.auth/user.json"
if not os.path.exists(path):
ctx = browser.new_context(base_url=base_url)
page = ctx.new_page()
page.goto("/login")
page.get_by_label("Username").fill(os.environ["E2E_USER"])
page.get_by_label("Password").fill(os.environ["E2E_PASS"])
page.get_by_role("button", name="Log in").click()
expect(page.get_by_text("Dashboard")).to_be_visible()
ctx.storage_state(path=path)
ctx.close()Scope matters: auth_context uses scope="session" to reuse one context; if some tests break session state (logout, profile change), downgrade to function scope or provide a separate clean fixture variant.
9.4 Token Expiration Strategies
storage_state is just a snapshot — when tokens expire, all tests turn red simultaneously. Common strategies:
- Short validity + CI pre-refresh: have CI run a "login and export state" setup step before each build, ensuring tokens stay fresh for the entire test run;
- Detect expiry and auto-rebuild: in the fixture, check after visiting the home page whether you were redirected back to login; if so, re-run the login routine and overwrite the state file:
def is_logged_in(page) -> bool:
"""Visit a protected page; return False if redirected back to login"""
page.goto("/dashboard")
page.wait_for_load_state()
return "login" not in page.url
# Health-check inside the state-loading fixture; rebuild if expired
if not is_logged_in(auth_page):
login_and_save(browser, USER_CREDS, "playwright/.auth/user.json")- Extend token validity in the test environment: the simplest and most reliable engineering trade-off — set E2E-environment tokens to hour-level expiry.
9.5 Chapter Summary
storage_stateexports/imports Cookie + localStorage + IndexedDB as JSON;- Save one state file per user role; never commit state files to git;
- Session-scoped
auth_contextfixture makes all tests boot logged in; - Token expiry: pre-refresh in CI, auto-rebuild in fixtures, or extend env token lifetime.
🧪 随堂测验
点击你认为正确的选项。答错时会展示正确答案与原因解析。
1. Where does Playwright store auth state (Cookies, localStorage, etc.)?
2. Why should storage_state JSON files be added to .gitignore?
3. What is the benefit of scope="session" for an auth_context fixture?
4. When tokens expire mid-test-suite, the most reliable fix is?
🛠️ Hands-On Exercises
- Build a local Express server with a
/loginroute; write a fixture that logs in once at session scope and servesauth_pageto all tests. - Simulate token expiry by setting a 1ms Cookie expiration; implement auto-rebuild logic in the fixture.
- Save two state files (admin + user); write tests that load each and assert role-specific UI elements appear.
Proceed to Chapter 10: Forms and Complex Component Interaction.