Persistent client-side state is useful until it makes a broken app look healthy. A test suite that always starts from a clean slate can miss bugs in rehydration, logout, multi-tab behavior, and stale cache handling. A suite that reuses state too aggressively can create false passes because the app never exercises the code paths real users hit after a refresh, a sign-in, or a session reset.

The goal is not to delete every browser storage mechanism before every test. The goal is to isolate the right state at the right layer, so your automation can verify what the app actually promises. That means distinguishing between ephemeral session state, durable Web Storage, and structured client databases, then choosing reset strategies that do not erase the very bug you want to catch.

The short version

If you need to test IndexedDB and local storage in browser automation, start with this rule:

  • Use a fresh browser context or profile for test isolation whenever possible.
  • Clear only the storage that the scenario actually depends on.
  • Keep at least one test path that proves the app behaves correctly with pre-existing state.

A blank browser is a good starting point, but it is not a substitute for realistic state transitions.

This matters because Web Storage and IndexedDB live at different layers. localStorage and sessionStorage are simple key-value stores tied to an origin and, for session storage, to a tab or browsing context. IndexedDB is a transactional browser database with a schema, versioning, and data that often survives longer than the current tab. Treating them as the same thing leads to cleanup code that is either too weak or too destructive.

What belongs where

Before writing cleanup code, decide what each storage layer is for.

sessionStorage

Use it for state that should disappear when the browsing context ends, such as transient wizard progress, one-tab flow markers, or short-lived redirect data.

In tests, sessionStorage is the easiest place to get false confidence. If a test opens a new page in the same context, session data may still exist. If your reset logic only clears cookies, the next assertion can pass for the wrong reason.

localStorage

Use it for small, durable origin-scoped data, such as UI preferences, feature-flag snapshots, or cached tokens in apps that still rely on them.

Because it is simple and synchronous, localStorage is easy to inspect and easy to overuse. It also creates a classic testing trap, an app can appear logged in because a prior test left a token behind.

IndexedDB

Use it for structured client data, offline caches, queueing, drafts, and larger app state.

IndexedDB is the one most teams under-handle in browser automation. It is not enough to clear cookies, and it is not enough to call localStorage.clear(). If the app rehydrates from IndexedDB on load, leftover records can hide bugs in onboarding, migration, and data expiration logic.

A practical isolation strategy

The cleanest approach is usually layered:

  1. Start each test in a new browser context or equivalent isolated profile.
  2. Use a unique origin or test tenant if the app supports it.
  3. Reset storage explicitly only when a test depends on an existing signed-in or cached state.
  4. Add a small set of stateful regression tests that intentionally reuse browser storage.

This is the central tradeoff:

  • Full reset gives reproducibility, but can hide bugs in state recovery.
  • Reused state gives realism, but can hide inter-test contamination.

For most suites, the right answer is both, with different tests serving different purposes.

Browser context isolation first, manual cleanup second

Modern automation tools usually provide a fresh browser context or profile abstraction. That is the safest default because it resets cookies, local storage, session storage, and other origin-scoped data at the context boundary. It does not automatically solve IndexedDB in every setup, but it is still the first line of defense.

In Playwright, a new context is the unit of isolation you should prefer for most end-to-end tests. The example below creates a fresh context, visits the app, and then clears storage inside the page when you want to verify reset behavior inside the same session.

import { test, expect } from '@playwright/test';
test('resets state without reusing old storage', async ({ browser }) => {
  const context = await browser.newContext();
  const page = await context.newPage();

  await page.goto('https://app.example.test');
  await page.evaluate(async () => {
    localStorage.clear();
    sessionStorage.clear();

    const dbs = await indexedDB.databases?.();
    for (const db of dbs ?? []) {
      if (db.name) indexedDB.deleteDatabase(db.name);
    }
  });

  await expect(page.getByText('Signed out')).toBeVisible();
  await context.close();
});

There are two important caveats here.

First, indexedDB.databases() is not universally supported in all browser environments. If your runner or browser version does not support it, you need a known database list or an app-level reset endpoint.

Second, clearing all databases is a blunt instrument. It is useful for setup and teardown, but it can mask migration bugs if every test begins by destroying the data the migration code should read.

How to reset without hiding real bugs

A reset strategy should match the bug class you want to catch.

Use app-level reset hooks for data-heavy flows

If the application stores complex offline state, consider exposing a test-only reset route or an authenticated API that deletes test tenant data server-side. That keeps the browser cleanup small and makes the test fixture easier to reason about.

This works especially well when IndexedDB mirrors backend records, because the test can start from a known server state and then verify the browser cache after navigation, refresh, or reconnection.

Keep one test that starts dirty

At least one test should preload state before the page loads. That test should verify behavior such as:

  • rehydrating a draft from IndexedDB,
  • reading a remembered preference from localStorage,
  • clearing stale session data on logout,
  • handling a schema upgrade in IndexedDB.

If every test begins with clear() calls, you never prove that the app can recover from realistic residue.

Reset per scenario, not per assertion

Do not clear browser storage between every assertion in the same user journey. If a flow depends on state surviving a redirect, a refresh, or a second tab, aggressive cleanup will destroy the thing you are trying to validate.

A better pattern is one reset at setup, then explicit verification of state transition points:

  • before login,
  • after login,
  • after refresh,
  • after sign out,
  • after reopening the app in a new context.

How to test session resets correctly

Session resets are where many false passes start. The app may clear the visible UI while leaving behind storage that restores the previous user on the next load.

A useful session reset test should verify three separate things:

  1. The current UI reflects the logged-out or reset state.
  2. sessionStorage is cleared for the active tab or context.
  3. Any durable state that should be removed, such as tokens in localStorage or user records in IndexedDB, is either cleared or invalidated.

With Cypress, storage access is often checked through browser APIs from the application window or from test setup code. The same principle applies, isolate first, then inspect the storage layer that the scenario depends on.

cy.visit('/');
cy.window().then((win) => {
  win.sessionStorage.clear();
  win.localStorage.removeItem('auth_token');
});

That snippet is intentionally small. The real work is in deciding what should be cleared, and what should remain to prove the app handles partial state correctly.

Failure modes worth testing on purpose

A good storage test suite does more than prove happy paths. It should target failure modes that are easy to miss when state is too clean.

1. Old auth data survives logout

After sign out, refresh the page and assert that the app does not silently restore the previous user from localStorage or IndexedDB.

2. IndexedDB schema changes break rehydration

Load a pre-upgrade fixture, then open the app against a newer build. The test should prove that migration logic runs or that the app falls back safely when it cannot read old records.

3. A stale cache hides an API regression

If the UI keeps reading a cached object from IndexedDB, a backend bug may never surface in tests unless you create a context with no prior records.

4. One test contaminates the next

Run the suite twice in the same browser profile, or at least run a subset back-to-back. If the second run behaves differently, your cleanup is incomplete.

If the test only passes after a manual reset in the runner UI, the suite is telling you that cleanup logic is part of the product under test.

A decision framework for browser state reset testing

Use this to choose a strategy.

Situation Best starting point Why
Pure UI flow, no persistent state dependency Fresh browser context Lowest contamination risk
Login, logout, refresh, or reopen behavior Fresh context plus explicit state verification Catches hidden persistence
Offline cache or draft recovery Seed IndexedDB, then verify rehydration Exercises the real storage path
Migration from older client data Preload old fixtures and upgrade in place Tests schema/version handling
Debugging a flaky suite Log storage before and after each test Makes leaks visible

My default recommendation is simple: use context-level isolation for most tests, then add a small number of deliberate stateful tests that prove persistence behaves the way your product claims.

Debugging checklist when a test passes for the wrong reason

If a test is suspiciously green, inspect storage before changing the assertions.

  • Open a fresh context and compare the result.
  • Dump localStorage, sessionStorage, and IndexedDB contents before the action under test.
  • Check whether the app reads from cache before it calls the network.
  • Verify that logout removes both UI state and stored credentials.
  • Confirm that your teardown runs even when a test fails early.

A simple diagnostic helper can save time:

async function dumpStorage(page) {
  return await page.evaluate(async () => ({
    localStorage: { ...localStorage },
    sessionStorage: { ...sessionStorage },
    indexedDbAvailable: !!indexedDB,
  }));
}

That does not inspect every IndexedDB record, but it is enough to show whether the app is starting from an unexpectedly populated state.

Not the best fit if

This approach is not ideal when your suite needs to verify the exact behavior of a long-lived authenticated browser profile across many runs. In that case, a dedicated reusable profile fixture may be better, but only if you also keep a separate isolated suite for regression and leak detection.

It is also not enough when the app’s critical state is mostly server-side. If the browser only holds a thin session pointer, focus your test effort on server reset APIs, token invalidation, and cache headers instead of over-engineering browser cleanup.

Final judgment

For most teams, the right answer to browser state testing is not “clear everything” and not “reuse everything.” It is a split strategy, fresh context by default, explicit cleanup where needed, and at least one test that proves your app survives real persistent state.

If you are trying to test IndexedDB and local storage in browser automation without hiding bugs, optimize for isolation first, then add intentional persistence coverage. That keeps the suite honest, keeps failures interpretable, and reduces the chance that a stale record makes a broken feature look stable.

FAQ

Should I clear localStorage before every test?

Only if the scenario does not depend on persisted UI state. If the flow includes refresh, sign-in persistence, or logout, use a fresh browser context and verify the state transition explicitly.

Is IndexedDB harder to test than localStorage?

Yes, because it is asynchronous, structured, and often used for richer app state. It is also easier to under-test, which is why leaks in IndexedDB can hide for a long time.

Does sessionStorage get cleared automatically?

It is scoped to the browsing context, so it disappears when that context ends. But if your test stays in the same tab or context, it can still leak between steps.

What is the safest cleanup method for end-to-end suites?

A new browser context or profile per test is the safest default. Manual storage cleanup is better used as a targeted tool, not as the only isolation mechanism.

How do I know my reset logic is too aggressive?

If logout, refresh, or offline-recovery tests stop exercising the real storage path, your cleanup is probably hiding the bug class you want to catch.