IKivan-kozenko -aqa
Try yourself as a QA tester
All topics·Intermediate·17 / 27

Authentication

Login once, run all tests already authenticated. Playwright saves browser state to a file and reuses it — no login flow repeated for every test.

The problem: login in every test

Login once in setup, share the auth file across all tests

If you have 80 tests that all need to be logged in — and you log in at the start of each one — that's 80 login flows per run. Each login is a network round-trip, a page load, a form fill. On a real app this adds up fast.

The solution: log in once, save the browser state (cookies + localStorage) to a JSON file, and tell every test to start from that file. Playwright calls this storageState.

Recommended setup: auth setup project

The cleanest approach: create a separate setup project that runs before your tests. It logs in and saves the state to playwright/.auth/user.json. Then every test project uses that file as its starting storageState.

First, create the directory and add it to .gitignore — you don't want auth files in your repo:

bash
mkdir -p playwright/.auth
echo '\nplaywright/.auth' >> .gitignore
ts
// tests/auth.setup.ts
import { test as setup, expect } from '@playwright/test'
import path from 'path'

const authFile = path.join(__dirname, '../playwright/.auth/user.json')

setup('authenticate', async ({ page }) => {
  await page.goto('/login')
  await page.getByLabel('Email').fill('admin@example.com')
  await page.getByLabel('Password').fill(process.env.TEST_PASSWORD!)
  await page.getByRole('button', { name: 'Sign in' }).click()

  // Wait for redirect — cookies are set after this
  await page.waitForURL('/dashboard')
  await expect(page.getByRole('heading', { name: 'Dashboard' })).toBeVisible()

  // Save cookies + localStorage to file
  await page.context().storageState({ path: authFile })
})
ts
// playwright.config.ts
export default defineConfig({
  projects: [
    // This project runs first and produces playwright/.auth/user.json
    { name: 'setup', testMatch: /.*\.setup\.ts/ },

    {
      name: 'chromium',
      use: {
        ...devices['Desktop Chrome'],
        storageState: 'playwright/.auth/user.json', // start every test authenticated
      },
      dependencies: ['setup'], // always run setup first
    },
  ],
})

Multiple accounts for parallel tests

The shared auth file works great when tests only read data. If your tests modify server state (create orders, change settings), parallel tests will step on each other's changes when sharing one account. The fix: one account per parallel worker.

Each worker gets its own auth file named by its index (0.json, 1.json, ...). The worker logs in once, saves state, and all its tests reuse that state.

ts
// playwright/fixtures.ts
import { test as base, expect } from '@playwright/test'
import fs from 'fs'
import path from 'path'

export const test = base.extend({
  storageState: ({ workerStorageState }, use) => use(workerStorageState),

  workerStorageState: [async ({ browser }, use) => {
    const workerId = test.info().parallelIndex
    const authFile = path.resolve(
      test.info().project.outputDir,
      `.auth/${workerId}.json`
    )

    if (fs.existsSync(authFile)) {
      await use(authFile)
      return
    }

    // First run for this worker — log in and save
    const page = await browser.newPage({ storageState: undefined })
    await page.goto('/login')
    await page.getByLabel('Email').fill(`worker${workerId}@example.com`)
    await page.getByLabel('Password').fill(process.env.TEST_PASSWORD!)
    await page.getByRole('button', { name: 'Sign in' }).click()
    await page.waitForURL('/dashboard')
    await page.context().storageState({ path: authFile })
    await page.close()
    await use(authFile)
  }, { scope: 'worker' }],
})

Faster: authenticate via API

If your app has a login API endpoint, you can skip the browser form entirely and get a token or session cookie directly via an HTTP request. This is much faster than driving a login form through the UI.

ts
// tests/auth.setup.ts — API login approach
import { test as setup, request } from '@playwright/test'

const authFile = 'playwright/.auth/user.json'

setup('authenticate via API', async ({ }) => {
  const apiCtx = await request.newContext()

  const response = await apiCtx.post('/api/auth/login', {
    data: {
      email: 'admin@example.com',
      password: process.env.TEST_PASSWORD,
    }
  })

  // Save cookies from the API response to the auth file
  await apiCtx.storageState({ path: authFile })
  await apiCtx.dispose()
})

Multiple roles: admin and user

If your app has roles (admin, manager, viewer), create a separate auth file for each. Then in the test fixture, pick the right file based on what the test needs.

ts
// playwright.config.ts — два проєкти з різними ролями
projects: [
  { name: 'setup', testMatch: /.*\.setup\.ts/ },

  {
    name: 'admin tests',
    use: { storageState: 'playwright/.auth/admin.json' },
    dependencies: ['setup'],
    testMatch: '**/admin/**/*.spec.ts',
  },
  {
    name: 'user tests',
    use: { storageState: 'playwright/.auth/user.json' },
    dependencies: ['setup'],
    testMatch: '**/user/**/*.spec.ts',
  },
]