A test that passes by itself but fails only in the full suite is usually not “random.” It is often telling you that the test depends on state that was left behind by another test, another worker, or an earlier run. In browser automation, the usual suspects are cookies, localStorage, sessionStorage, IndexedDB, cached auth state, and backend data created by setup steps that were never fully isolated.

If you are trying to debug tests that fail only in a full suite, start with one assumption: the test is probably correct in isolation but wrong as part of a shared system. The fix is rarely a bigger wait or a looser assertion. It is usually state isolation, explicit setup, or a cleanup strategy that matches how the suite actually runs.

The short version

If a test only fails in the full suite, check these in order:

  1. Order dependency: does the failure appear only after a specific earlier test?
  2. Worker-level state: is the suite reusing a browser context, session, or backend record across tests?
  3. Client storage: are cookies, localStorage, sessionStorage, or IndexedDB carrying auth or feature flags between tests?
  4. Backend setup: does a previous test create data that changes the login flow, permissions, or selectors?
  5. Parallel execution: do multiple workers collide on the same account, same email inbox, same tenant, or same record ID?

A test that depends on “whatever happened earlier” is not isolated, even if it passes locally.

Why isolation is different from suite execution

A single test often runs with a clean browser, a clean profile, and a convenient backend state. The full suite changes that picture.

In Playwright, for example, browser contexts are designed to isolate cookies and storage from each other. That helps, but only if your tests actually create fresh contexts when they need them. If your code reuses context, page, account, or backend fixtures across tests, state can leak even though the browser still looks “new enough.”

The problem is not limited to the browser. A login that sets a cookie may also create a server-side session. A checkout test may mutate the same user record that a profile test expects. A setup step may plant data that makes later selectors change. The browser is only one layer of the dependency chain.

Fast triage: find the leak before you rewrite the suite

Start with a small set of reproducible checks.

1) Run the failing test after the suspected predecessor

If you suspect order dependence, run the passing test and the failing test back to back, then reverse the order. If the failure follows one specific predecessor, you have evidence of shared state, not just a brittle assertion.

For Playwright, you can focus on one file or test name:

bash npx playwright test tests/login.spec.ts -g “creates session” npx playwright test tests/profile.spec.ts -g “shows user menu”

Then flip the order by running the second test first. If the failing behavior changes, inspect what the earlier test writes to the browser or backend.

2) Force serial execution temporarily

If the suite normally runs in parallel, force a small subset to run serially. If the failure disappears, the issue may be worker collision rather than pure test logic.

bash npx playwright test tests/auth.spec.ts –workers=1

In Selenium-based suites, the same idea applies even if the framework does not manage workers for you. Run the narrow slice in a single process and compare behavior.

3) Print the storage you care about

When a failure is intermittent, inspect cookies and storage right before the failing action. Don’t guess.

const cookies = await context.cookies();
const localStorage = await page.evaluate(() => ({ ...localStorage }));
const sessionStorage = await page.evaluate(() => ({ ...sessionStorage }));
console.log({ cookies, localStorage, sessionStorage });

If the test is unexpectedly logged in, logged out, or seeing a feature flag, this usually shows up here.

The storage layers that cause suite-only failures

Cookies

Cookie contamination is the simplest failure mode. A previous test logs in, and the next test inherits the session cookie. That can make a login test pass locally and fail in CI, or it can make an anonymous flow start in an authenticated state.

What to check:

  • Is the same browser context reused across tests?
  • Does the app store auth entirely in cookies, or split it across cookie plus backend session?
  • Are there tenant, locale, or A/B test cookies that affect visible UI?

Fixes:

  • Create a fresh browser context per test when authentication or tenant state matters.
  • Clear cookies before each test if reuse is unavoidable.
  • Avoid reusing the same browser profile for unrelated tests.

localStorage

localStorage often holds auth hints, onboarding completion, feature flags, UI preferences, or cached app state. Because it persists per origin, it can silently survive across tests if the same context or profile is reused.

Typical symptoms:

  • A test opens on the wrong dashboard because a prior test stored a last-used workspace.
  • A wizard no longer appears in suite runs because the “seen tutorial” flag was already written.
  • A feature flag test behaves differently depending on earlier navigation.

Fixes:

  • Clear localStorage before each test.
  • Seed only the keys the test needs, not the whole browser profile.
  • Prefer an explicit setup helper that writes known storage values for each scenario.
await page.addInitScript(() => {
  localStorage.clear();
  sessionStorage.clear();
});

Use this carefully. It is useful for isolation, but it can also hide whether your app depends on stale client state.

sessionStorage

sessionStorage is scoped to a tab or top-level browsing context, but suite code can still accidentally carry it forward if the same page or tab is reused. It often stores temporary UI state, auth steps, or ephemeral redirect data.

If a test only fails when a previously opened tab is still around, inspect whether the test is relying on a fresh page but actually receiving an old one.

IndexedDB

IndexedDB is a frequent blind spot. App shells, PWAs, and offline-capable apps may store token caches, app metadata, or queued actions there. Browser context resets are not always enough if your test setup reuses the same persistent profile.

When IndexedDB is the problem, browser-level cleanup may be necessary, or the test fixture may need a dedicated isolated profile per run.

The hidden half of the problem, backend state

Browser state is only one side of the equation. A test can pass alone and still fail in the suite because earlier tests altered the backend.

Look for these patterns:

  • A shared test user account gets modified by multiple tests.
  • A setup API call creates a record with the same email, slug, or identifier every time.
  • One test revokes permissions that another test assumes still exist.
  • A login flow changes after the first authenticated session and the suite reuses that account.

If your test environment is stateful, two browser tests can conflict even when their UI steps are isolated.

Use test data that is unique per run

Prefer unique identifiers for records that survive beyond a single browser context. For example, generate a run-specific email or username instead of reusing a canonical fixture account.

const runId = `${Date.now()}-${Math.random().toString(16).slice(2)}`;
const email = `qa+${runId}@example.com`;

That is not a full solution, but it removes a major source of contamination.

Reset backend state with the same discipline as browser state

If your team has database reset scripts, seed jobs, or API cleanup hooks, treat them as part of the test contract. A browser reset without backend reset still leaves suite-only failure modes in place.

Playwright-specific places to look

Playwright’s isolation model is helpful, but only if you use it consistently.

Do not confuse a fresh page with a fresh context

A new page inside the same context shares cookies and storage with other pages in that context. If you need hard isolation, create a fresh context.

const context = await browser.newContext();
const page = await context.newPage();

If your test suite uses storageState to speed up login, verify that the stored state is not being reused across scenarios that should start anonymous.

Check whether workers share fixtures you assumed were per-test

A fixture with worker scope can be correct for expensive setup, but risky if it contains mutable user state. If one worker logs in with a shared account and another worker edits that account, you have a collision even though both tests pass alone.

A useful rule: shared fixtures should be read-only or disposable. If they mutate, they need a reset strategy.

Be careful with authenticated storage snapshots

Playwright supports saving and reusing auth state through storageState. That is useful for speed, but it can also freeze in stale cookies, expired tokens, and feature toggles that no longer match the current backend.

If a test starts failing only after the auth snapshot ages, regenerate the snapshot and confirm whether the failure was caused by stale auth rather than app behavior.

A practical debugging sequence

When I need to debug tests that fail only in a full suite, I would use this order:

  1. Identify the smallest failing subset. Don’t start with the whole suite.
  2. Toggle parallelism. If serial execution fixes it, suspect worker collision.
  3. Inspect browser state. Cookies, localStorage, sessionStorage, and possibly IndexedDB.
  4. Inspect backend state. Shared accounts, reused records, deleted permissions, stale tokens.
  5. Make one test truly isolated. Fresh context, fresh account, fresh backend data.
  6. Rerun the same subset repeatedly. If the failure only appears after a previous run, hidden persistence still exists.

What to change in the suite

Use per-test browser isolation for auth-sensitive flows

If a test checks login, logout, tenant switching, or permissions, give it a fresh context unless you have a strong reason not to.

Make setup explicit

Avoid “mystery login” helpers that do too much. A helper should declare whether it creates a signed-in context, seeds storage, or provisions backend records.

Separate anonymous and authenticated scenarios

A lot of suite-only failures come from mixing anonymous tests with logged-in tests in the same shared profile. Keep those paths distinct.

Clean up what you create

If a test creates a record, delete it or use a disposable namespace. If it writes client storage, clear it. If it logs in with a shared account, treat that account as mutable shared infrastructure and protect it accordingly.

Not the same problem as a flaky selector

It is tempting to treat every suite-only failure as a wait problem. That is a mistake.

A flaky selector fails because the DOM changed, the element was delayed, or the page transitioned too early. A suite-only state leak fails because the test entered the wrong starting condition. Retrying the assertion may hide the problem without fixing it.

If a test always sees the wrong user, wrong workspace, wrong language, or wrong onboarding state only in suite mode, your debugging target is shared state, not timing.

A simple decision table

Symptom Most likely cause First fix to try
Passes alone, fails after another login test Shared cookie or auth state Fresh browser context per test
Passes alone, fails in parallel Worker collision on shared account or data Unique test data, single-worker rerun
Login screen missing in suite only localStorage or cookie carries auth/onboarding state Clear storage before test
Wrong dashboard or tenant shown Shared backend account or tenant cookie Isolate account and backend seed
Failure appears only after rerun Persistent browser profile or stale auth snapshot Regenerate auth state and profile

When to stop debugging and redesign

If a test repeatedly depends on cleanup scripts, order, or a global account, the test is telling you something structural. At that point, the right move is not another workaround. It is to redesign the fixture model so the test owns its state.

That may mean slower setup, but it usually buys back more time than it costs by reducing suite triage and CI noise.

Final takeaway

When a test fails only in the full suite, treat that as a state isolation bug until proven otherwise. Start with browser storage, then worker reuse, then backend data, then auth snapshots. The browser may be the visible failure point, but the root cause is often a shared state boundary that the suite has been crossing without noticing.

FAQ

Why does a test pass alone but fail in a suite?

Usually because a previous test changed browser state, backend data, or execution order. The test is starting from a different condition in the full suite.

Run the failing test after a login-heavy test, then inspect context.cookies() before the failing step. If the session is already present, the test is inheriting state.

Does clearing localStorage fix all state leaks?

No. It helps with client-side persistence, but cookies, sessionStorage, IndexedDB, and backend records can still leak between tests.

Why do Playwright tests sometimes fail only when workers run in parallel?

Parallel workers can share accounts, emails, tenants, or other mutable backend data. The browser may be isolated, but the test data is not.

Is storageState a bad idea?

No, but it is easy to misuse. It is useful for speed, yet stale auth snapshots and reused state can create suite-only failures if you apply them too broadly.

Should I always create a fresh browser context for every test?

For auth-sensitive or stateful scenarios, yes, that is often the cleanest option. For pure read-only flows, you may choose a different tradeoff, but the boundary should be deliberate.