TesterArmyTesterArmy
DemoDemo/
How it worksHow it works/
PricingPricing/
FAQFAQ/
BlogBlog/
DocsDocs/
ContactContact
Sign inGet started
HomeBlogHow to Handle Authentication in Playwright E2E Tests

How to Handle Authentication in Playwright E2E Tests

Authentication is usually the first thing that makes Playwright suites slow or flaky. Here's how to reuse login state, test real login once, and keep auth reliable in CI.

Oskar Kwasniewski
Oskar KwasniewskiCTO
June 6, 202611 min read
How to Handle Authentication in Playwright E2E Tests

Authentication is usually the first thing that makes a Playwright suite slow or flaky.

The pattern repeats in most codebases: a beforeEach fills in the login form, the first test passes, and everyone moves on. A few months later the suite takes nine minutes, half the failures are "element not found" on the login page, and nobody trusts a red build.

The fix is not a better selector or a longer timeout. The fix is deciding which tests actually need to log in, and getting every other test out of that business. Playwright has good primitives for this. What is usually missing is a model for how to use them.

Let's walk through a setup that stays fast locally, works in CI, and handles multiple user roles.

What a good auth setup looks like

Before touching any config, it helps to know what we want:

  1. Speed. Logging in through the UI is slow, and most tests have no reason to do it.
  2. Independence. A test should never fail because an earlier test left a user in a weird state.
  3. Honest failures. When auth breaks, the error should say so, not show up as a random assertion failure three pages deep.
  4. Real login coverage, somewhere. The goal is not to stop testing login. The goal is to stop testing it by accident in every dashboard, billing, and settings spec.

That last point is easy to overcorrect on. Once you discover storageState, it is tempting to skip the login UI forever. Don't. You still want one test that proves a user can actually sign in. Playwright's authentication guide covers the mechanics; the rest of this post is the setup around them.

Save the login once with storageState

storageState dumps the cookies and local storage from a signed-in browser context into a JSON file. Any later test can load that file and start already logged in. Sign in once, reuse it everywhere.

One caveat up front: if your app keeps its auth token in IndexedDB (Firebase Auth is the common case), cookies and local storage are not enough, and you need to capture IndexedDB too. More on that below.

Start with a setup test whose only job is to log in and write the state to disk.

tests/auth.setup.ts

For IndexedDB-backed auth, pass the flag when you save:

Timing matters here. Many apps finish setting cookies during a redirect, so calling storageState too early saves a half-authenticated context that fails in confusing ways later. Wait for something that only exists after login, like the final URL or a piece of authenticated-only UI, before you save.

Then tell Playwright to run the setup before everything else and point the main browser project at the saved file.

playwright.config.ts

With that in place, your actual tests stop mentioning login at all.

tests/dashboard.spec.ts

This one change removes most of the noise from a suite. The login page can still break, but now it breaks the setup step with a clear message, instead of failing every authenticated test for an unrelated reason.

Keep one real login test

Reusing saved state works until you notice you stopped testing login entirely. If sign-in breaks, that is your whole product locked out, and a suite full of pre-authenticated tests will stay green while it happens.

So keep exactly one test that signs in the way a user would: open the page, type the credentials, click the button.

tests/login.spec.ts

The empty storageState is easy to forget. Without it, this test inherits the project's saved session and passes even when the login form is completely broken, which is the opposite of what it should prove.

Now the responsibilities are clean. One test covers the login flow, and the rest of the suite never pays for it. When something fails, you can tell at a glance whether it was login or the feature under test.

Skip the UI when the UI is not the point

There is a second step here: the setup test does not have to use the UI either.

If you control the backend, you probably have a faster way in. A test-only login endpoint, an internal session helper, or an API route that creates a session for a known user. The setup test does not exist to prove the login form works. That is the job of the login test above. It exists to produce a valid authenticated context as quickly and reliably as possible.

tests/auth.setup.ts

This is not always possible. If a third-party identity provider owns your login, you often have to drive the UI. But when the backend is yours, API setup is faster, more stable, and easier to debug. A simple rule: UI login for the login test, API login for everything else.

Give every role its own state file

Real apps rarely have one kind of user. There is an admin, a regular member, a read-only viewer, a billing owner. Each of them sees a different product, and your tests need to be able to become any of them.

Do not share one auth file between roles. Give each role its own.

tests/auth.setup.ts

Then each test opts into the role it needs.

tests/billing.spec.ts

Keeping roles explicit catches a sneaky class of bug: the viewer test that only passes because it accidentally ran with an admin session. If permissions regress, that test should go red, not keep passing for the wrong reason.

Some flows need two users in the same test, like an admin inviting a member. For those, create separate contexts from separate state files.

tests/invites.spec.ts

Do not let parallel workers share an account

This failure mode usually shows up after you speed the suite up with more workers: every worker is signed in as the same account.

When two tests share a user, they share that user's data. One test changes a setting while another reads it mid-change, and the build goes red for reasons that have nothing to do with the code. It only happens under parallelism, so it passes locally and fails in CI.

There are three ways out:

  1. Give each worker its own account.
  2. Create a fresh account per test.
  3. Keep shared accounts strictly read-only.

For CI, the first option is usually the right trade-off. Pre-create a small pool of test users and hand one to each worker through a fixture.

tests/fixtures.ts

This writes one state file per worker into the project's output directory, so it never touches committed files and gets cleaned up with the rest of the test artifacts.

You do not need this on day one. But if the suite is green locally and flaky only in CI, shared users are the first thing to check.

Fail loudly when auth goes stale

Saved auth does not stay valid forever. Tokens rotate, session policies change, a provider shortens cookie lifetimes, and one morning every test fails because the saved state file is logged out.

The trap is letting that surface deep inside an unrelated test, where the symptom reads as "the dashboard didn't load" instead of "auth expired". So make every setup verify it actually worked before handing the state off.

tests/auth.setup.ts

If your app redirects logged-out users to /sign-in, do not just wait for navigation to settle. Assert the authenticated state you expect to see. "Did not get redirected" and "is actually signed in" are different checks, and only the second one catches a silently expired session.

In CI, treat auth files as disposable.

.gitignore

Generate the state fresh on every run and keep credentials in your CI secret store. The files are throwaway artifacts, so treat them that way.

Common mistakes

The same patterns come up again and again:

  • Logging in through the UI before every test. Slow, and it turns one broken login into a hundred broken tests.
  • Not testing login at all. The opposite extreme. If users cannot sign in, nothing else matters, so keep the one real login test.
  • Sharing a single account across parallel workers. Fine until tests start writing data, then mysteriously red in CI.
  • Running logged-out tests with a saved session. Public pages and sign-up flows need an empty state, or they pass for the wrong reason.
  • Hiding auth setup in a shell script before npx playwright test. A setup project lives inside the runner, so it is traced, reported, and debuggable like any other test. An invisible pre-step is none of those things.

Where TesterArmy fits

If you maintain your own Playwright suite, everything above is a solid baseline. It is roughly what we would set up ourselves.

We built TesterArmy because past a certain point, hand-maintaining auth fixtures, role pools, screenshots, videos, retries, and reporting for your most critical flows stops being a good use of anyone's time. TesterArmy runs those flows from plain-language steps and gives you the evidence when one breaks: screenshots, video, logs, and a readable summary of what went wrong.

It is most useful for the journeys you cannot afford to ship broken:

  • Sign in
  • Complete onboarding
  • Invite a teammate
  • Change billing settings
  • Submit a checkout
  • Verify a PR preview before it merges

None of this means dropping Playwright. Keep it for the low-level coverage it is great at, and let TesterArmy run the high-level journeys and report back.

That's a wrap

Test authentication directly, in one place, and stop re-testing it by accident in every other spec.

Use storageState for the bulk of your authenticated tests. Keep a single UI login test so you know sign-in still works. Split roles into separate state files, do not let parallel workers share one account, and make stale auth fail in setup rather than halfway through something unrelated.

The result is a faster suite, readable failures, and a CI you can trust.

If you want to take this up to the pull-request level next, read Run E2E Tests on Vercel Preview Deployments.

ON THIS PAGE

  • What a good auth setup looks like
  • Save the login once with storageState
  • Keep one real login test
  • Skip the UI when the UI is not the point
  • Give every role its own state file
  • Do not let parallel workers share an account
  • Fail loudly when auth goes stale
  • Common mistakes
  • Where TesterArmy fits
  • That's a wrap

SHARE THIS ARTICLE

  • X

Check other TesterArmy insights

June 6, 2026

How to Handle Authentication in Playwright E2E Tests

Stop logging in through the UI before every Playwright test. Use storageState, keep one real login test, isolate roles, and make stale auth fail loudly in CI.

Read article
June 5, 2026

Run E2E Tests on Vercel Preview Deployments

Vercel preview deployments are the perfect place to run end-to-end tests. Connect GitHub, connect Vercel, choose the project, and TesterArmy runs your saved tests on every PR preview.

Read article
May 27, 2026

Running Mobile Tests With Expo EAS

Expo EAS already knows how to build your app. Here's how to pass that build to TesterArmy and run mobile tests from the same workflow.

Read article
Contact us

Let's connect

Get a demo
XLinkedInDiscord
TesterArmyTesterArmy

AI-powered QA testing for modern teams. Ship faster with confidence.

© 2026 TesterArmy, Inc.

Solutions
  • AI app testingAI app testing
  • EcommerceEcommerce
  • MobileMobile
  • Production monitoringProduction monitoring
  • WebWeb
  • WordPress testingWordPress testing
Quick links
  • HomeHome
  • DemoDemo
  • StackStack
  • How it worksHow it works
  • FAQFAQ
  • PricingPricing
  • Get a demoGet a demo
  • Contact usContact us
Resources
  • DocumentationDocumentation
  • BlogBlog
  • API referenceAPI reference
  • Getting startedGetting started
Legal
  • Privacy policyPrivacy policy
  • Terms of serviceTerms of service
TesterArmyTesterArmy
DemoDemo/
How it worksHow it works/
PricingPricing/
FAQFAQ/
BlogBlog/
DocsDocs/
ContactContact
Sign inGet started