How to fix Playwright failures from leaked state between tests?

Playwright

Leaked state between tests happens when one test leaves side effects that affect subsequent tests in the same worker: cookies or localStorage that carry forward from a previous session, mocked routes that weren't cleaned up, or global variables set in page.evaluate(). Tests that pass in isolation but fail when run as part of the full suite are the signature of state leakage — and they are particularly insidious because they are order-dependent, meaning failures may not be reproducible by re-running a single test.

Common mistake

test('logs in as admin', async ({ page }) => {
  await page.goto('/login');
  await page.getByLabel('Email').fill('admin@example.com');
  await page.getByRole('button', { name: 'Sign in' }).click();
  await page.waitForURL('/admin');
  // Session cookies now in the context — bleed into next test
});

test('public homepage is accessible', async ({ page }) => {
  await page.goto('/');
  // Fails because admin session redirects '/' to '/admin'
  await expect(page.getByRole('heading', { name: 'Welcome' })).toBeVisible();
});

Also broken: route mocks registered in one test affecting subsequent tests because handlers were never unregistered.

The fix

Playwright's built-in fixtures create a fresh BrowserContext for every test by default — if you rely on this and don't share contexts manually, most state leakage is prevented automatically:

import { test, expect } from '@playwright/test';

// Each test gets a new context and page — cookies, storage, and routes start clean
test('admin views users list', async ({ page }) => {
  // Use storageState fixture or login within the test — don't rely on previous test's state
  await page.goto('/login');
  await page.getByLabel('Email').fill('admin@example.com');
  await page.getByLabel('Password').fill('adminpass');
  await page.getByRole('button', { name: 'Sign in' }).click();
  await page.waitForURL('/admin');

  await expect(page.getByRole('table', { name: 'Users' })).toBeVisible();
});

test('public homepage loads without auth', async ({ page }) => {
  // This page fixture is a completely separate context — no cookie bleed
  await page.goto('/');
  await expect(page.getByRole('heading', { name: 'Welcome' })).toBeVisible();
});

For tests within the same test.describe that legitimately share a context, clean up explicitly:

test.describe('authenticated session', () => {
  test.beforeEach(async ({ page }) => {
    // Always start from the same known URL
    await page.goto('/dashboard');
  });

  test.afterEach(async ({ page }) => {
    // Clear any routes registered in this test
    await page.unroute('**/*');

    // Clear localStorage/sessionStorage if tests write to them
    await page.evaluate(() => {
      localStorage.clear();
      sessionStorage.clear();
    });
  });
});

Why it works

Playwright's page and context fixtures have "test" scope by default — each test() call receives a new BrowserContext created from scratch. The new context has no cookies, no localStorage, and no registered route handlers, making tests fully independent. Problems arise when you manually share context objects across tests (via module-level variables or extending fixtures with scope: 'worker'), or when page.route() handlers are added without being cleaned up. page.unroute() removes all handlers for a pattern; await page.context().clearCookies() removes session state.

Tips

  • Run your test suite with --shuffle to randomize execution order — tests that fail only in certain orders have state leakage between them.
  • If a suite passes alone but fails in a full run, use --grep to run just the preceding tests plus the failing test to isolate the exact test that's leaving bad state.
  • Shared worker-scoped fixtures (user accounts, API tokens) are fine for read operations — make sure they never mutate shared state that another test reads.
  • The page.evaluate(() => localStorage.clear()) approach in afterEach is a safety net — properly isolated tests shouldn't need it, but it catches edge cases in suites that use localStorage for state.