Files
FancyWordle/.agents/skills/playwright-best-practices/core/configuration.md
T
2026-05-16 13:57:02 -04:00

12 KiB

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

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

Symptom Setting Default Recommended
Test takes too long overall timeout 30s 30-60s (max 120s)
Assertion retries too long/short expect.timeout 5s 5-10s
page.goto() or waitForURL() times out navigationTimeout 30s 10-30s
click(), fill() time out actionTimeout 0 (unlimited) 10-15s
Dev server slow to start webServer.timeout 60s 60-180s

Server Management

Scenario Approach
App in same repo webServer with reuseExistingServer: !process.env.CI
Separate repos Manual start or Docker Compose
Testing deployed environment No webServer; set baseURL via env
Multiple services Array of webServer entries

Single vs Multi-Project

Scenario Approach
Early development Single project (chromium only)
Pre-release validation Multi-project: chromium + firefox + webkit
Mobile-responsive app Add mobile projects alongside desktop
Auth + non-auth tests Setup project with dependencies
Tight CI budget Chromium on PRs; all browsers on main

globalSetup vs Setup Projects vs Fixtures

Need Use
One-time DB seed globalSetup
Shared browser auth Setup project with dependencies
Per-test isolated state Custom fixture via test.extend()
Cleanup after all tests globalTeardown

Production-Ready Config

// 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.

// 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 },
});
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.

// 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'],
    },
  ],
});
// 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('testuser@example.com');
  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.

// 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.

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

export default defineConfig({
  testDir: './e2e',
  globalSetup: './e2e/setup.ts',
  globalTeardown: './e2e/teardown.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()}`;
}
// 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.

# .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
# .gitignore
.env
.env.local
.env.staging
.env.production
playwright/.auth/

Install dotenv:

npm install -D dotenv

Tag-Based Test Filtering

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

// 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:

// 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'] },
    },
  ],
});
# Run specific project
npx playwright test --project=smoke
npx playwright test --project=regression

Artifact Collection Strategy

Setting Local CI Reason
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
// 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't Problem Do Instead
timeout: 300_000 globally Masks flaky tests; slow CI Fix root cause; keep 30s default
Hardcoded URLs: page.goto('http://localhost:4000/login') Breaks in other environments Use baseURL + relative paths
All browsers on every PR 3x CI time Chromium on PRs; all on main
trace: 'on' always Huge artifacts, slow uploads trace: 'on-first-retry'
video: 'on' always Massive storage; slow tests video: 'retain-on-failure'
Config in test files: test.use({ viewport: {...} }) everywhere Scattered, inconsistent Define once in project config
retries: 3 locally Hides flakiness retries: 0 local, retries: 2 CI
No forbidOnly in CI Committed test.only runs single test forbidOnly: !!process.env.CI
globalSetup for browser auth No browser context available Use setup project with dependencies
Committing .env with credentials Security risk Commit .env.example only

Troubleshooting

baseURL Not Working

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

// 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.

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:

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:

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