Authentication State and Browser Projects
Signing in once and reusing that session across every test — and running the exact same suite against multiple real browser engines through Playwright projects.
What you'll learn
- Explain why signing in inside every single test is slow and how storageState avoids it
- Configure multiple browser projects and understand what each one actually verifies
- Build a locator-quality and fixture-composition exercise reflecting real Playwright authentication patterns
Prerequisites
Explanation
Signing in through the UI inside every single test — filling a login form, submitting, waiting for redirect — is slow and repetitive, and it means a bug in the login flow itself breaks every other test in the suite, not just the ones actually testing login. Playwright's recommended pattern: sign in once, in a dedicated setup step, save the resulting cookies and storage via await context.storageState({ path: "auth.json" }), and have every other test start from a browser context that loads that saved state (test.use({ storageState: "auth.json" })) — instantly "already signed in," with zero login-form interaction needed in the tests that don't actually care about the login flow itself.
A project in playwright.config.ts is a named configuration — a browser engine, a viewport, a device emulation, or a specific storageState — and the same test files run once per configured project. This is exactly the mechanism from this course's first lesson's guided local lab (chromium/firefox/webkit projects), extended: a project can also specify storageState to pre-authenticate, or ...devices["iPhone 13"] to run the identical suite against a mobile emulation, all without duplicating a single test file. A common, effective structure uses a dependency: a setup project that runs first and performs the real login, saving storageState, and other projects declared with dependencies: ["setup"] so Playwright's test runner automatically runs the login step before any test that needs it, exactly once per full run, not once per test.
The genuinely important limitation worth stating honestly: storageState only captures cookies and localStorage/sessionStorage — it does not capture server-side session state that might expire independently, nor does re-using a saved auth state prove the login flow itself still works (that's exactly why a small, separate, real test of the login flow itself remains worthwhile even once most other tests bypass it via storageState). Treating storageState as a total replacement for ever testing login again is a common, easy mistake — it's an optimization for tests that aren't about login, not a reason to stop testing login at all.
Example
Modeling the sign-in-once, reuse-everywhere pattern and multi-project configuration as data.
function buildProjectConfig(name, deviceOverrides, useStorageState) {
return {
name,
use: {
...deviceOverrides,
...(useStorageState ? { storageState: "auth.json" } : {}),
},
...(useStorageState ? {} : { dependencies: [] }),
};
}
const setupProject = { name: "setup", testMatch: /.*\.setup\.ts/ };
const chromiumAuthed = { ...buildProjectConfig("chromium", { browserName: "chromium" }, true), dependencies: ["setup"] };
const chromiumAnonymous = buildProjectConfig("chromium-anonymous", { browserName: "chromium" }, false);
console.log(chromiumAuthed.use.storageState); // "auth.json" -- starts pre-authenticated
console.log(chromiumAnonymous.use.storageState); // undefined -- starts with a clean, signed-out context, deliberatelyTry it yourself
Build a webkit project with device overrides for 'Desktop Safari' and confirm its use object merges correctly.
Code editor. Press Escape then Tab to leave the editor if keyboard focus becomes trapped. Press Control+Shift+M inside the editor to toggle Tab-key focus trapping.
Guided exercise
Guided exercise
Write needsLoginSetup(testTags) modeling which tests need the storageState-providing setup dependency: return true unless testTags includes 'anonymous' or 'login-flow' (tests that deliberately start signed-out, including the login test itself, must NOT depend on a pre-authenticated setup).
Checks: a normal feature test needs the login setup · the login flow test itself does not depend on pre-authentication · a deliberately anonymous test does not use the setup dependency
Code editor. Press Escape then Tab to leave the editor if keyboard focus becomes trapped. Press Control+Shift+M inside the editor to toggle Tab-key focus trapping.
Stuck? Get a hint.
Independent exercise
Independent exercise
Write buildBrowserProjectMatrix(browserNames, includeMobile) returning an array of project name strings: one per browser name in browserNames, plus (if includeMobile is true) one additional 'mobile-chromium' entry. This models composing a real playwright.config.ts projects array from a small set of choices.
Checks: includes a mobile project when requested · excludes the mobile project when not requested
Code editor. Press Escape then Tab to leave the editor if keyboard focus becomes trapped. Press Control+Shift+M inside the editor to toggle Tab-key focus trapping.
Stuck? Get a hint.
Guided local lab
Build Reliable Tests with Locators, Fixtures, and Authentication State
Runs on your computerExtend your Playwright project from Module 1 with a real sign-in-once setup, a shared fixture, and role-based locators — the combination that makes a real test suite both fast and reliable.
Required tools
- Node.js (20.x or 22.x LTS)
- @playwright/test (1.62.x)
- A terminal (any)
Setup
- Continue from the pw-learning-lab project created in this course's first guided local lab (or recreate it if starting fresh).
- Add a tests-setup/ folder for the authentication setup project.
- Pick any real, publicly-accessible site with a login form you're comfortable using for practice (or use a local test app if you have one) — this lab's file structure assumes a generic login form.
Project structure
pw-learning-lab/
playwright.config.ts
tests-setup/
auth.setup.ts
tests/
dashboard.spec.ts
playwright/.auth/
user.json (generated, not committed)Starter files
tests-setup/auth.setup.ts
import { test as setup, expect } from "@playwright/test";
const authFile = "playwright/.auth/user.json";
setup("authenticate once", async ({ page }) => {
// TODO: navigate to your chosen site's login page
// TODO: fill in credentials using environment variables (process.env.TEST_USER / process.env.TEST_PASS),
// never hard-coded real credentials
// TODO: submit and assert something confirming a successful sign-in
// TODO: await page.context().storageState({ path: authFile });
});
tests/dashboard.spec.ts
import { test, expect } from "@playwright/test";
test("an already-authenticated page shows signed-in content", async ({ page }) => {
// TODO: navigate directly to a page that requires authentication --
// this test should start ALREADY signed in, via the project's storageState
// TODO: assert something that only appears when signed in
});
Requirements
- auth.setup.ts reads credentials from environment variables, never hard-coded literals.
- auth.setup.ts saves storageState to playwright/.auth/user.json after a real, successful sign-in.
- playwright.config.ts defines a 'setup' project and at least one other project with `dependencies: ['setup']` and `use: { storageState: 'playwright/.auth/user.json' }`.
- dashboard.spec.ts uses only role-based locators (getByRole/getByLabel/getByText), no structural CSS/XPath selectors.
- playwright/.auth/ is added to .gitignore so the saved session is never committed.
Commands to run
Set credentials for this session only (not committed anywhere)
export TEST_USER=your-test-username TEST_PASS=your-test-passwordRun the full suite (setup runs automatically first, via dependencies)
npx playwright test
Expected behavior
Running `npx playwright test` runs the setup project first (a real sign-in, producing playwright/.auth/user.json), then runs dashboard.spec.ts already signed in, with no login-form interaction inside that test at all.
Verify it yourself
npx playwright testExpected: The setup project passes, then dashboard.spec.ts passes without ever visiting the login page
cat playwright/.auth/user.jsonExpected: A real JSON file containing cookies/storage state now exists locally
git statusExpected: playwright/.auth/ does NOT appear as untracked — confirms .gitignore is working
Troubleshooting
- dashboard.spec.ts still shows signed-out content — Confirm the project running dashboard.spec.ts actually declares `dependencies: ['setup']` and `use: { storageState: ... }` pointing at the same path auth.setup.ts wrote to.
- `Error: TEST_USER is not defined` — Export the environment variables in the same terminal session before running the tests — they are never hard-coded in the committed files.
- storageState file is empty or missing expected cookies — Confirm auth.setup.ts's assertion after submitting genuinely confirms a signed-in state BEFORE calling storageState() — capturing state before sign-in actually completes saves a signed-out session.
Stuck? Get a hint.
Extension challenge
Add a second, deliberately anonymous project (no storageState) running a dedicated login.spec.ts that tests the sign-in flow itself for real — confirming the pattern from this lesson: storageState is an optimization for tests that aren't about login, not a replacement for testing login at all.
When you've verified this locally, use the "Mark lesson complete" button below to record your progress.
Common mistakes
- Signing in through the UI inside every single test -- this is slow, repetitive, and means a bug in the login flow breaks tests that have nothing to do with login.
- Hard-coding real or realistic-looking credentials directly in a committed test file -- credentials belong in environment variables, read via process.env, never committed even for a 'throwaway' test account.
- Treating storageState as a total replacement for ever testing the login flow again -- it's an optimization for tests NOT about login; a dedicated, real login test should still exist and run.
Knowledge check
Takeaway
Sign in once via a setup project, save storageState, and have other projects depend on it and load that state — this makes most tests fast and decoupled from the login flow, but a real, dedicated login test must still exist, since storageState only proves a previously-saved session works, not that signing in still does.
Summary
storageState captures cookies/web storage from a real sign-in, reusable across tests via test.use({ storageState }). A setup project with dependencies wires this in automatically. Playwright projects also configure different browsers/devices, running the same test files against each. storageState is an optimization for non-login tests, not a replacement for testing login itself.
References
Your notes
Notes save automatically.
Finished this lesson?
Mark it complete to track your progress and schedule a future review.
AI tutor
The optional AI tutor isn't enabled in this deployment. All lessons, exercises, quizzes, and search work fully without it.