Back to Sanity

Playwright Configuration

.agents/skills/playwright-best-practices/core/configuration.md

5.20.013.8 KB
Original Source

Playwright Configuration

Table of Contents

  1. CLI Quick Reference
  2. Decision Guide
  3. Production-Ready Config
  4. Patterns
  5. Anti-Patterns
  6. Troubleshooting
  7. Related

When to use: Setting up a new project, adjusting timeouts, adding browser targets, configuring CI behavior, or managing environment-specific settings.

CLI Quick Reference

bash
npx playwright init                           # scaffold config + first test
npx playwright test --config=custom.config.ts # use alternate config
npx playwright test --project=chromium        # run single project
npx playwright test --reporter=html           # override reporter
npx playwright test --grep @smoke             # run tests tagged @smoke
npx playwright test --grep-invert @slow       # exclude @slow tests
npx playwright show-report                    # open last HTML report
DEBUG=pw:api npx playwright test              # verbose logging

Decision Guide

Timeout Selection

SymptomSettingDefaultRecommended
Test takes too long overalltimeout30s30-60s (max 120s)
Assertion retries too long/shortexpect.timeout5s5-10s
page.goto() or waitForURL() times outnavigationTimeout30s10-30s
click(), fill() time outactionTimeout0 (unlimited)10-15s
Dev server slow to startwebServer.timeout60s60-180s

Server Management

ScenarioApproach
App in same repowebServer with reuseExistingServer: !process.env.CI
Separate reposManual start or Docker Compose
Testing deployed environmentNo webServer; set baseURL via env
Multiple servicesArray of webServer entries

Single vs Multi-Project

ScenarioApproach
Early developmentSingle project (chromium only)
Pre-release validationMulti-project: chromium + firefox + webkit
Mobile-responsive appAdd mobile projects alongside desktop
Auth + non-auth testsSetup project with dependencies
Tight CI budgetChromium on PRs; all browsers on main

globalSetup vs Setup Projects vs Fixtures

NeedUse
One-time DB seedglobalSetup
Shared browser authSetup project with dependencies
Per-test isolated stateCustom fixture via test.extend()
Cleanup after all testsglobalTeardown

Production-Ready Config

ts
// playwright.config.ts
import {defineConfig, devices} from '@playwright/test'
import dotenv from 'dotenv'
import path from 'path'

dotenv.config({path: path.resolve(__dirname, '.env')})

export default defineConfig({
  testDir: './e2e',
  testMatch: '**/*.spec.ts',

  fullyParallel: true,
  forbidOnly: !!process.env.CI,
  retries: process.env.CI ? 2 : 0,
  workers: process.env.CI ? '50%' : undefined,

  reporter: process.env.CI
    ? [['html', {open: 'never'}], ['github']]
    : [['html', {open: 'on-failure'}]],

  timeout: 30_000,
  expect: {timeout: 5_000},

  use: {
    baseURL: process.env.BASE_URL || 'http://localhost:4000',
    actionTimeout: 10_000,
    navigationTimeout: 15_000,
    trace: 'on-first-retry',
    screenshot: 'only-on-failure',
    video: 'retain-on-failure',
    locale: 'en-US',
    timezoneId: 'America/Los_Angeles',
  },

  projects: [
    {name: 'chromium', use: {...devices['Desktop Chrome']}},
    {name: 'firefox', use: {...devices['Desktop Firefox']}},
    {name: 'webkit', use: {...devices['Desktop Safari']}},
    {name: 'mobile-chrome', use: {...devices['Pixel 7']}},
    {name: 'mobile-safari', use: {...devices['iPhone 14']}},
  ],

  webServer: {
    command: 'npm run start',
    url: 'http://localhost:4000',
    reuseExistingServer: !process.env.CI,
    timeout: 120_000,
    stdout: 'pipe',
    stderr: 'pipe',
  },
})

Patterns

Environment-Specific Configuration

Use when: Tests run against dev, staging, and production environments.

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

const ENV = process.env.TEST_ENV || 'local'
dotenv.config({path: path.resolve(__dirname, `.env.${ENV}`)})

const envConfig: Record<string, {baseURL: string; retries: number}> = {
  local: {baseURL: 'http://localhost:4000', retries: 0},
  staging: {baseURL: 'https://staging.myapp.com', retries: 2},
  prod: {baseURL: 'https://myapp.com', retries: 2},
}

export default defineConfig({
  testDir: './e2e',
  retries: envConfig[ENV].retries,
  use: {baseURL: envConfig[ENV].baseURL},
})
bash
TEST_ENV=staging npx playwright test
TEST_ENV=prod npx playwright test --grep @smoke

Setup Project with Dependencies

Use when: Tests need shared authentication state before running.

ts
// playwright.config.ts
import {defineConfig, devices} from '@playwright/test'

export default defineConfig({
  testDir: './e2e',
  projects: [
    {
      name: 'setup',
      testMatch: /auth\.setup\.ts/,
    },
    {
      name: 'chromium',
      use: {
        ...devices['Desktop Chrome'],
        storageState: 'playwright/.auth/session.json',
      },
      dependencies: ['setup'],
    },
    {
      name: 'firefox',
      use: {
        ...devices['Desktop Firefox'],
        storageState: 'playwright/.auth/session.json',
      },
      dependencies: ['setup'],
    },
  ],
})
ts
// e2e/auth.setup.ts
import {test as setup, expect} from '@playwright/test'

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

setup('authenticate', async ({page}) => {
  await page.goto('/login')
  await page.getByLabel('Username').fill('[email protected]')
  await page.getByLabel('Password').fill(process.env.TEST_PASSWORD!)
  await page.getByRole('button', {name: 'Log in'}).click()
  await expect(page.getByRole('heading', {name: 'Home'})).toBeVisible()
  await page.context().storageState({path: authFile})
})

webServer with Build Step

Use when: Tests need a running application server managed by Playwright.

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

export default defineConfig({
  testDir: './e2e',
  use: {baseURL: 'http://localhost:4000'},
  webServer: {
    command: process.env.CI ? 'npm run build && npm run preview' : 'npm run dev',
    url: 'http://localhost:4000',
    reuseExistingServer: !process.env.CI,
    timeout: 120_000,
    env: {
      NODE_ENV: 'test',
      DB_URL: process.env.DB_URL || 'postgresql://localhost:5432/testdb',
    },
  },
})

globalSetup / globalTeardown

Use when: One-time non-browser work like seeding a database. Runs once per test run.

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

export default defineConfig({
  testDir: './e2e',
  globalSetup: './e2e/setup.ts',
  globalTeardown: './e2e/teardown.ts',
})
ts
// e2e/setup.ts
import {FullConfig} from '@playwright/test'

export default async function globalSetup(config: FullConfig) {
  const {execSync} = await import('child_process')
  execSync('npx prisma db seed', {stdio: 'inherit'})
  process.env.TEST_RUN_ID = `run-${Date.now()}`
}
ts
// e2e/teardown.ts
import {FullConfig} from '@playwright/test'

export default async function globalTeardown(config: FullConfig) {
  const {execSync} = await import('child_process')
  execSync('npx prisma db push --force-reset', {stdio: 'inherit'})
}

Environment Variables with .env

Use when: Managing secrets, URLs, or feature flags without hardcoding.

bash
# .env.example (commit this)
BASE_URL=http://localhost:4000
TEST_PASSWORD=
API_KEY=

# .env.local (gitignored)
BASE_URL=http://localhost:4000
TEST_PASSWORD=secret123
API_KEY=dev-key-abc

# .env.staging (gitignored)
BASE_URL=https://staging.myapp.com
TEST_PASSWORD=staging-pass
API_KEY=staging-key-xyz
bash
# .gitignore
.env
.env.local
.env.staging
.env.production
playwright/.auth/

Install dotenv:

bash
npm install -D dotenv

Tag-Based Test Filtering

Use when: Running subsets of tests in different CI stages (PR vs nightly).

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

export default defineConfig({
  testDir: './e2e',

  // Filter by tags in CI
  grep: process.env.CI ? /@smoke|@critical/ : undefined,
  grepInvert: process.env.CI ? /@flaky/ : undefined,
})

Project-specific filtering:

ts
// playwright.config.ts
import {defineConfig, devices} from '@playwright/test'

export default defineConfig({
  testDir: './e2e',
  projects: [
    {
      name: 'smoke',
      grep: /@smoke/,
      use: {...devices['Desktop Chrome']},
    },
    {
      name: 'regression',
      grepInvert: /@smoke/,
      use: {...devices['Desktop Chrome']},
    },
    {
      name: 'critical-only',
      grep: /@critical/,
      use: {...devices['Desktop Chrome']},
    },
  ],
})
bash
# Run specific project
npx playwright test --project=smoke
npx playwright test --project=regression

Artifact Collection Strategy

SettingLocalCIReason
trace'off''on-first-retry'Traces are large; collect on failure only
screenshot'off''only-on-failure'Useful for CI debugging
video'off''retain-on-failure'Recording slows tests
ts
// playwright.config.ts
import {defineConfig} from '@playwright/test'

export default defineConfig({
  testDir: './e2e',
  use: {
    trace: process.env.CI ? 'on-first-retry' : 'off',
    screenshot: process.env.CI ? 'only-on-failure' : 'off',
    video: process.env.CI ? 'retain-on-failure' : 'off',
  },
})

Anti-Patterns

Don'tProblemDo Instead
timeout: 300_000 globallyMasks flaky tests; slow CIFix root cause; keep 30s default
Hardcoded URLs: page.goto('http://localhost:4000/login')Breaks in other environmentsUse baseURL + relative paths
All browsers on every PR3x CI timeChromium on PRs; all on main
trace: 'on' alwaysHuge artifacts, slow uploadstrace: 'on-first-retry'
video: 'on' alwaysMassive storage; slow testsvideo: 'retain-on-failure'
Config in test files: test.use({ viewport: {...} }) everywhereScattered, inconsistentDefine once in project config
retries: 3 locallyHides flakinessretries: 0 local, retries: 2 CI
No forbidOnly in CICommitted test.only runs single testforbidOnly: !!process.env.CI
globalSetup for browser authNo browser context availableUse setup project with dependencies
Committing .env with credentialsSecurity riskCommit .env.example only

Troubleshooting

baseURL Not Working

Cause: Using absolute URL in page.goto() ignores baseURL.

ts
// Wrong - ignores baseURL
await page.goto('http://localhost:4000/dashboard')

// Correct - uses baseURL
await page.goto('/dashboard')

webServer Starts But Tests Get Connection Refused

Cause: webServer.url doesn't match actual server address or health check returns non-200.

ts
webServer: {
  command: 'npm run dev',
  url: 'http://localhost:4000/api/health',  // use real endpoint
  reuseExistingServer: !process.env.CI,
  timeout: 120_000,
},

Tests Pass Locally But Timeout in CI

Cause: CI machines are slower. Increase timeouts and reduce workers:

ts
export default defineConfig({
  workers: process.env.CI ? '50%' : undefined,
  use: {
    navigationTimeout: process.env.CI ? 30_000 : 15_000,
    actionTimeout: process.env.CI ? 15_000 : 10_000,
  },
})

"Target page, context or browser has been closed"

Cause: Test exceeded timeout and Playwright tore down browser during action.

Fix: Don't increase global timeout. Find slow step using trace:

bash
npx playwright test --trace on
npx playwright show-report