Browser state is one of the easiest ways to get false confidence from automation. If a login test only passes because yesterday’s session cookie is still present, you do not have a stable test, you have a hidden dependency.

The goal is not to wipe everything blindly. The goal is to test browser storage reset between runs in a way that proves two things at once:

  1. Each test starts from a clean, known state.
  2. Your cleanup does not erase real bugs in auth, cart, draft saving, or session recovery.

The useful question is not “did storage get cleared?”, it is “did the app still behave correctly when storage was absent, stale, or partially cleared?”

What counts as browser storage in a test run?

In this article, browser storage means the client-side state that can survive page reloads and, depending on how you launch tests, sometimes survive between runs.

The main buckets

  • Cookies: Often used for session identifiers, CSRF tokens, preferences, and analytics.
  • localStorage: Persistent key-value data for the origin. It survives browser restarts until cleared.
  • sessionStorage: Key-value data scoped to a tab or top-level browsing context. It usually disappears when the tab closes.
  • IndexedDB: Structured client-side database, frequently used for offline data, drafts, queues, and app caches.
  • Cache Storage / Service Worker state: Can keep fetched assets or offline responses around, depending on your app.

The browser APIs are defined in primary documentation such as the MDN Web Docs for localStorage, sessionStorage, Cookies, and IndexedDB.

What should be cleared, and what should not?

The right answer depends on the test’s purpose.

Clear these when the test needs a clean start

  • Session cookies for authenticated flows that must start logged out
  • localStorage for stateful UI settings, cached auth tokens, feature flags, and app preferences
  • sessionStorage when the test depends on a fresh tab-level state
  • IndexedDB if the app stores drafts, offline queues, or cached records there
  • Service worker data when offline behavior or stale cache is affecting determinism

Do not clear these if you want to catch real bugs

  • Auth persistence in a test that is supposed to verify “remember me” or session restore
  • Cart persistence in a shopping flow where a user expects items to survive reloads
  • Draft saving if the product promises recovery after accidental navigation or refresh
  • User preferences if the test is specifically checking whether the app honors saved settings across sessions

The important distinction is between test isolation and product behavior. Isolation removes accidental leftovers from prior tests. Product behavior is the feature you actually want to verify.

A simple rule for deciding what to reset

Classify each state item using three questions:

  1. Is the state part of the feature under test?
  2. Does the app promise that the state persists across refresh, tab close, or sign-out?
  3. Could leftover state make a pass or fail misleading?

If the answer to 1 is yes, be careful. If the answer to 2 is yes, do not delete it in every setup step. If the answer to 3 is yes, isolate it in a fixture, context, or profile dedicated to that test.

Why naive cleanup hides bugs

The most common mistake is to run a universal cleanup before every test and then assume the app is healthy when tests pass.

That can hide defects like these:

  • The app fails to clear an auth token on logout, but your suite deletes the token before the next test starts.
  • A cart is supposed to persist after reload, but your cleanup removes the cart before the follow-up assertion.
  • A draft editor leaks state between users, but your test only ever runs with an empty profile.
  • A stale IndexedDB record causes a broken upgrade path, but an overzealous reset removes the old database before the migration code runs.

If a bug only appears with leftover browser state, the correct response is not “clear harder.” It is “write one test that preserves the state and exposes the failure.”

Do not try to solve everything in one setup hook.

Use two kinds of tests

Isolation tests prove the app behaves correctly from a clean state.

  • Start from a new browser context or profile
  • Clear only the storage required for determinism
  • Verify the page loads without relying on prior session data

Persistence tests prove the app preserves or restores state correctly.

  • Preload one specific state item, such as an auth cookie or draft record
  • Reload, reopen, or revisit the app
  • Assert that the state remains or is intentionally removed

This separation matters because a universal reset can make persistence bugs invisible.

What browser automation frameworks can help with

Frameworks differ in how they isolate state, so it is worth choosing the mechanism that matches your test goal.

Playwright

Playwright documents browser contexts as isolated sessions, which is useful when you want a fresh storage container per test. Its browser contexts documentation explains how contexts separate cookies and storage, and its authentication guide describes storageState for saving and restoring login state.

A small Playwright pattern for a clean context looks like this:

import { test, expect } from '@playwright/test';
test('starts without leaked storage', async ({ browser }) => {
  const context = await browser.newContext();
  const page = await context.newPage();
  await page.goto('https://example.com');
  await expect(page).toHaveTitle(/Example/);
  await context.close();
});

Use this style when you care about session isolation more than speed.

Cypress

Cypress provides commands for clearing state during a test, including clearCookies, clearLocalStorage, and related storage helpers. That is convenient, but it is also easy to overuse them and accidentally suppress a bug that depends on prior state.

A good pattern is to clear only what your test truly depends on, then keep a separate spec that verifies persistence across reloads.

Selenium

Selenium itself does not define a single universal storage API, because the mechanics depend on the language binding and browser driver. The browser-facing behavior still maps back to the same primitives: cookies, origin storage, and IndexedDB. The WebDriver specification is the primary reference for session behavior and browser automation boundaries.

For Selenium suites, prefer explicit browser profile control or fresh driver sessions over ad hoc JavaScript cleanup when test isolation matters.

A practical cleanup strategy that avoids overreach

The safest default is to reset in layers.

Layer 1: new browser session or context

This is the cleanest boundary for most UI tests. A fresh context usually gives you new cookies and storage containers.

Layer 2: targeted application cleanup

If your app stores data in IndexedDB or service workers, a new context may not be enough in every browser or deployment configuration. In those cases, clear the app-specific stores explicitly or use a dedicated test account and domain.

Layer 3: verify cleanup itself

Do not assume the reset worked. Add a small assertion that storage is actually empty before the test continues.

For example, in a browser page context you can check localStorage and cookies after setup:

const cookies = await context.cookies();
expect(cookies).toHaveLength(0);
const keys = await page.evaluate(() => Object.keys(localStorage));
expect(keys).toHaveLength(0);

That does not prove IndexedDB or service worker state is gone, but it does prove the test did not inherit the obvious leftovers.

How to tell leaked state from a broken app flow

When a test fails, trace the failure backward.

Signs of leaked browser state

  • The failure disappears when you run the test in a fresh profile or incognito-style context
  • The failure only happens after another test that logs in, saves a draft, or seeds localStorage
  • The failure reproduces with UI navigation but not with a blank browser profile

Signs of a real app bug

  • The failure reproduces in a truly clean context
  • The same bug appears on first visit, not just after previous tests
  • The app’s documented behavior is violated, such as logout not clearing session cookies or draft recovery not restoring data

A useful debugging trick is to run the same scenario twice:

  1. Once in a freshly created browser context.
  2. Once after explicitly seeding the expected storage state.

If the first passes and the second fails, you may have uncovered a state-transition bug rather than a flaky test.

Edge cases worth testing explicitly

Some bugs only appear when storage is partially cleared.

This catches apps that read auth from more than one place. If logout clears only cookies, a token in localStorage may resurrect a session.

This catches apps that treat localStorage as disposable but still rely on a server session.

3. IndexedDB survives a version upgrade

This matters for apps with offline data or cached drafts. A stale schema can break migrations even when cookies and localStorage look clean.

4. Cross-tab behavior

sessionStorage is tab-scoped, but cookies and localStorage are shared by origin. If your app uses multiple tabs, test that the right state appears in the right place.

A reproducible checklist for CI

Use this checklist when adding a browser storage reset to your suite:

  • Create a new browser context or equivalent isolated session per test where possible
  • Define which storage types the test should start without
  • Seed state only when the scenario explicitly requires it
  • Assert the absence of leftover cookies and localStorage keys before the test starts
  • Keep separate tests for persistence, logout, draft restore, and cart retention
  • Re-run failures in a fresh context before blaming the application
  • If IndexedDB or service workers are involved, include one scenario that exercises stale state on purpose

A small decision table

Test goal Reset storage? Reset what What to keep
Login should start from scratch Yes Cookies, localStorage, sessionStorage Nothing from prior auth
Remember me should survive reload No, or seed only what matters Avoid blanket reset The selected session state
Cart should persist across sessions Partial Clear unrelated app data only Cart storage
Draft recovery should work after refresh Partial Clear unrelated test state Draft record or IndexedDB entry
Logout should fully invalidate user state No Preserve the state long enough to verify removal The pre-logout session

When a fresh profile is better than manual cleanup

A fresh browser profile or context is usually the better choice when:

  • Your app uses multiple storage types
  • You have flaky cross-test contamination
  • You are debugging a failure that disappears after localStorage.clear() but reappears later
  • You need confidence that no hidden state survives from a previous scenario

Manual cleanup still has a place when you are intentionally preserving one state item to test persistence or restoration.

Final takeaway

To test browser storage reset between runs without hiding real session bugs, do not treat cleanup as a blanket precondition. Treat it as a test design choice.

Clear the storage that would contaminate the scenario, keep the storage that is part of the feature, and add at least one persistence test that proves you are not deleting the very bug you wanted to catch.

FAQ

Should I clear all browser storage before every test?

No. Clear only what would make the test state-dependent in a misleading way. If the scenario is about persistence, logout, cart retention, or draft recovery, a full reset can hide the bug.

Is localStorage.clear() enough?

Usually not. It does not clear cookies, sessionStorage, IndexedDB, or service worker caches. It also may erase state you actually wanted to verify.

What is the safest way to isolate browser tests?

A fresh browser context or profile per test is the safest starting point. Then add targeted cleanup only if the app stores data outside the context boundary.

How do I know if a failure is caused by leaked state?

Re-run the scenario in a fresh context. If the failure disappears, leaked state is likely. If it remains, the app flow itself is more suspect.

Should logout tests start with a clean browser?

No. Logout tests need existing authenticated state so they can verify that the app removes it correctly.

Do I need to test IndexedDB separately?

Yes, if your app uses it for drafts, offline data, queues, or cached user state. It can produce failures that cookies and localStorage cleanup will never reveal.