How to fix Playwright auth state issues when login passes but tests start logged out?

Playwright

This happens when the authentication setup step saves session state to a file, but the test project that runs your tests either doesn't load it or loads it from a different path. The login automation itself works — you can see it succeed in the setup trace — but every test starts unauthenticated because the browser context for those tests was created fresh without the saved cookies and localStorage. This is the most common issue when first configuring Playwright's storageState pattern for authenticated test suites.

Common mistake

// global-setup.ts — saves state to one path
import { chromium } from '@playwright/test';

async function globalSetup() {
  const browser = await chromium.launch();
  const page = await browser.newPage();

  await page.goto('/login');
  await page.getByLabel('Email').fill('tester@example.com');
  await page.getByLabel('Password').fill('secret');
  await page.getByRole('button', { name: 'Sign in' }).click();

  await page.context().storageState({ path: 'auth-state.json' });
  await browser.close();
}
// playwright.config.ts — loads from a different path or doesn't specify
export default {
  use: {
    storageState: 'playwright/.auth/user.json', // Different path from auth-state.json
  },
};

The fix

Use the recommended project-based setup pattern so the auth file path is consistent across setup and consumption:

// playwright.config.ts
import { defineConfig } from '@playwright/test';
import path from 'path';

export const AUTH_FILE = path.join(__dirname, 'playwright/.auth/user.json');

export default defineConfig({
  projects: [
    {
      name: 'setup',
      testMatch: /global\.setup\.ts/,
    },
    {
      name: 'authenticated tests',
      dependencies: ['setup'],
      use: {
        storageState: AUTH_FILE, // Same constant used in setup
      },
    },
  ],
});
// global.setup.ts
import { test as setup } from '@playwright/test';
import { AUTH_FILE } from './playwright.config';

setup('authenticate', async ({ page }) => {
  await page.goto('/login');
  await page.getByLabel('Email').fill(process.env.TEST_USER!);
  await page.getByLabel('Password').fill(process.env.TEST_PASS!);
  await page.getByRole('button', { name: 'Sign in' }).click();
  await page.waitForURL('**/dashboard');

  // Save storageState — includes cookies, localStorage, sessionStorage
  await page.context().storageState({ path: AUTH_FILE });
});
// tests/billing.spec.ts — test runs with pre-loaded auth
import { test, expect } from '@playwright/test';

test('billing page is accessible', async ({ page }) => {
  await page.goto('/billing');
  await expect(page.getByRole('heading', { name: 'Billing' })).toBeVisible();
});

Why it works

storageState serializes the browser context's cookies, localStorage, and sessionStorage to a JSON file. When a BrowserContext is created with storageState pointing to that file, Playwright pre-loads all three stores before the first page navigation — the session appears as if the user had logged in manually. The dependencies config key ensures the setup project always runs before any authenticated test project starts, preventing race conditions where tests begin before the auth file exists.

Tips

  • Add playwright/.auth/ to .gitignore — auth files contain real session tokens and should never be committed.
  • For test suites with multiple user roles, create one auth file per role (admin.json, viewer.json) and define separate projects for each role.
  • If auth state expires between test runs, regenerate it by deleting the auth file and letting the setup project recreate it, or add a session freshness check to the setup step.
  • The storageState approach works for cookie-based and localStorage-based auth. For OAuth flows that require a real browser interaction, use the setup project pattern with a logged-in page interaction as shown above.