Authentication and Storage State

By Syncfusion®, Inc. · Last updated

Why logging in before every test is slow, what storage state holds (cookies and local storage), saving and reusing a login with storageState, the setup project pattern, logging in through the API, several user roles, logged-out tests, and keeping test accounts and session files safe.

Authentication and Storage State

  • Most real apps hide their best pages behind a login. Orders, accounts and admin pages all need a signed-in user.
  • If every test fills in the login form first, the suite gets slow. It also breaks in many places when the login page changes.
  • In this lesson, you will log in once, save the session, and reuse it in every test.

Simple lesson: log in once, save the session to a file, and start every test already logged in.

A Real-World Example

  • Asha checks her account on the Practice Shop. This test logs in through the form, then checks the welcome heading.
  • It works. But imagine 200 tests that each start like this.
  • Look at the time next to the test. Most of it is the login, not the check.

What You Will Learn

  1. Why repeated logins hurt, and what a session is.
  2. What storage state holds, and how to save and load it.
  3. The setup project pattern with auth.setup.ts.
  4. Logging in through the API instead of the form.
  5. Several user roles, and tests that need no login.
  6. Keeping session files and passwords safe.

Why Log In Once

The Cost of Logging In Every Time

  • A form login takes about a second in a real browser. Many apps take longer.
  • 200 tests times 1 second is more than 3 minutes of typing the same password.
  • Logins are also a common source of flaky tests. One slow login server fails many tests at once.
  • The login form still needs tests. But only a few, in one file.

What a Session Is

  • When you log in, the app gives the browser a session. It is usually a cookie, or a token in local storage.
  • On the next page, the browser sends the cookie back. The app knows who you are without a new login.
  • If you copy that cookie and local storage into a fresh browser context, it is logged in too.

Simple lesson: a session is just stored data. Copy the data and you copy the login.

Storage State

What storageState Holds

  • context.storageState() returns everything a context has stored: cookies and local storage for each origin.
  • Playwright can save it to a JSON file, and load the file into a new context later.
  • Session storage is not included. Most apps do not keep logins there.

Animation: the setup context logs in, its cookies and local storage are saved to user.json, then three test contexts load the file and start logged in

Setup context 1. fill email 2. fill password 3. click Log in cookie sid=asha-…

user.json cookies + storage

Test 1: logged in ✓ Test 2: logged in ✓ Test 3: logged in ✓

One login fills the file. Every test context loads the file and skips the form.

Saving and Loading State

  • Save: await context.storageState({ path: 'playwright/.auth/user.json' }).
  • Load: browser.newContext({ storageState: 'playwright/.auth/user.json' }), or test.use({ storageState: ... }).
  • This lab saves the state, prints the cookie names, then opens a new context from the file.
  • The second page never saw the login form. The file made it logged in.

In the lab, files live only for one run. On your computer, the JSON file stays in the playwright/.auth folder until you delete it.

Simple lesson: storageState({ path }) saves. The storageState option loads.

The Setup Project Pattern

auth.setup.ts and Project Dependencies

  • This is the pattern the Playwright docs recommend for most apps.
  • A setup project runs a file named like auth.setup.ts. It logs in and saves the file.
  • Your normal project lists dependencies: ['setup'] and loads the file with storageState.
  • Playwright runs setup first. If setup fails, the other tests do not run, so you see one clear error.
  • The output shows [setup] once, then [chromium] for each test.
  • Neither test logs in. Both start logged in.

Waiting for the Login to Finish

  • Save the state only after the app has stored the session.
  • toHaveURL('/account') waits for the redirect, which happens after the cookie is set.
  • If you save too early, the file is empty and every test lands on the login page.

Never save storage state straight after clicking Log in. Wait for a sign that the login worked, such as the account URL or a "Welcome" heading.

Simple lesson: a setup project logs in once. dependencies makes the tests wait for it.

Logging In Through the API

Why the API Is Faster

  • Many apps have a login endpoint, like POST /api/login.
  • Calling it with the request fixture skips the page completely. No HTML, no typing.
  • request.storageState() saves the cookies the server sent back, the same way a page does.
  • API Testing covers request in full. Here you only need two calls.
  • Check the time of the [setup] line. It is much faster than the form login.

When the API Is Not Enough

  • Some apps keep part of the login in local storage, set by page JavaScript. An API call cannot fill that.
  • The Practice Shop's account page is one of them. It reads a session item from local storage.
  • In that case, use the form login in setup, or add the local storage item yourself after the API call.

Simple lesson: use the API login when the app only needs cookies. Use the form when the page sets more.

Several Users and Roles

One File per Role

  • Real apps have roles: customer, admin, support agent.
  • Save one file per role: admin.json, asha.json. Then pick the file each test needs.
  • test.use({ storageState: ... }) inside a describe sets it for that group.
  • The helper logIn keeps the setup file short. Page is the type of the page fixture. The JavaScript flavour drops it.

Two Roles in One Test

  • Some flows need two people at once. An admin changes something, and a customer sees it.
  • Create one context per role from its file, as in Browser Contexts, Pages, and Emulation.
test('admin and customer together', async ({ browser }) => {
  const adminContext = await browser.newContext({ storageState: 'playwright/.auth/admin.json' });
  const userContext = await browser.newContext({ storageState: 'playwright/.auth/asha.json' });
  const adminPage = await adminContext.newPage();
  const userPage = await userContext.newPage();
  // ... the admin acts, the customer checks
  await adminContext.close();
  await userContext.close();
});

Tests That Must Be Logged Out

  • The login page itself, and the "please log in" message, need a logged-out browser.
  • Reset the state with an empty object: { cookies: [], origins: [] }.

Simple lesson: one file per role. Pick it with test.use, or an empty state for logged-out tests.

Keeping Sessions Safe

Do Not Commit Session Files

  • A storage state file is a real login. Anyone with the file can act as that user until the session ends.
  • Add the folder to .gitignore:
# .gitignore
playwright/.auth

Passwords Come from the Environment

  • Never write real passwords in test files. Read them from environment variables, as in Playwright Configuration.
  • Locally, keep them in a .env file that is also in .gitignore. On CI, use the CI's secret store.
setup('log in', async ({ page }) => {
  await page.goto('/login');
  await page.getByLabel('Email').fill(process.env.SHOP_USER!);
  await page.getByLabel('Password').fill(process.env.SHOP_PASSWORD!);
  await page.getByRole('button', { name: 'Log in' }).click();
  await expect(page).toHaveURL('/account');
  await page.context().storageState({ path: 'playwright/.auth/user.json' });
});

Test Accounts and Expiry

  • Use test accounts made only for testing. Never use a real customer or your own admin account.
  • Sessions expire. The setup project runs again on every npx playwright test, so the file is always fresh.
  • If tests change account data, like the cart, give each worker its own account. The Playwright docs show a worker-scoped fixture for this, built like the fixtures in Fixtures.

Tests that change the same account at the same time can clash. If two tests empty one cart in parallel, one of them may fail. One account per worker fixes that.

Simple lesson: treat session files like passwords. Ignore them in git and use test accounts.

Try It in Your Project

  • These steps take the lesson from the lab to your own app.
  • Add a setup project that logs in once and saves playwright/.auth/user.json.
  • Add playwright/.auth to .gitignore and use a dedicated test account.
  • Make one file run logged out with an empty storage state.

Simple lesson: use one idea from this lesson on your own app this week.

TL;DR

  • Logging in through the form in every test is slow and fragile.
  • Storage state is cookies plus local storage. storageState({ path }) saves it.
  • A setup project logs in once. Other projects load the file with storageState and dependencies.
  • API login is faster when the app only needs cookies.
  • One file per role. { cookies: [], origins: [] } for logged-out tests.
  • Keep playwright/.auth and passwords out of git.

Common mistakes

MistakeWhat happensFix
Saving state right after clicking Log inThe file has no session, tests land on the login pageWait for the account URL or a welcome heading first
Forgetting dependencies: ['setup']Tests run before the file exists and fail to read itAdd the dependency to every project that loads the file
Committing playwright/.authAnyone with the repo can use the loginAdd the folder to .gitignore
Using an API login for an app that keeps the session in local storagePages still say "please log in"Log in through the form, or add the local storage item
Many parallel tests changing one accountTests break each other's dataOne test account per worker

Check Yourself

Exercises

What to Remember

  • Log in once, not in every test.
  • storageState({ path }) saves cookies and local storage. The storageState option loads them.
  • Setup project plus dependencies is the standard pattern.
  • API login is fastest, when cookies are enough.
  • One file per role. An empty state for logged-out tests.
  • Session files and passwords never go into git.

Next: Network Interception and Mocking