Skip to content

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:

python
# ① 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 needed

Security 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:

python
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:

python
# 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:
python
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_state exports/imports Cookie + localStorage + IndexedDB as JSON;
  • Save one state file per user role; never commit state files to git;
  • Session-scoped auth_context fixture 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 ​

  1. Build a local Express server with a /login route; write a fixture that logs in once at session scope and serves auth_page to all tests.
  2. Simulate token expiry by setting a 1ms Cookie expiration; implement auto-rebuild logic in the fixture.
  3. 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.