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

Configuration

playwright.config.ts is the single place that controls browsers, base URL, parallelism, retries, artifacts, and timeouts. I keep one config for all environments — CI vs local is handled by process.env.CI conditions inside the same file.

A real config that actually works

This is the shape I use in every project. The key split: top-level options control the test runner (retries, workers, parallelism), use controls what every test gets by default (baseURL, viewport, credentials).

One mistake I see often: putting retries or workers inside use: {}. They don't work there — they must be at the top level.

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

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

  // Запустити всі тести паралельно
  fullyParallel: true,

  // На CI — заборонити test.only (забудеш прибрати)
  forbidOnly: !!process.env.CI,

  // На CI — 2 повтори при падінні; локально — 0
  retries: process.env.CI ? 2 : 0,

  // На CI — 1 воркер (стабільніше); локально — авто (= кількість CPU)
  workers: process.env.CI ? 1 : undefined,

  reporter: 'html',

  use: {
    baseURL: 'http://localhost:3000',

    // Зберігати trace при першому повторі — відкриваєш у npx playwright show-trace
    trace: 'on-first-retry',

    // Screenshot при падінні — видно в HTML репорті
    screenshot: 'only-on-failure',
  },

  projects: [
    { name: 'chromium', use: { ...devices['Desktop Chrome'] } },
    { name: 'firefox',  use: { ...devices['Desktop Firefox'] } },
    { name: 'webkit',   use: { ...devices['Desktop Safari'] } },
  ],

  // Запустити dev-сервер перед тестами (якщо не запущений)
  webServer: {
    command: 'npm run dev',
    url: 'http://localhost:3000',
    reuseExistingServer: !process.env.CI,
  },
})

The use section — defaults for every test

use options apply to every test automatically. The most important: baseURL lets you write page.goto('/orders') instead of the full URL everywhere. Other useful defaults:

ts
use: {
  baseURL: 'http://localhost:3000',

  // Viewport для всіх тестів
  viewport: { width: 1280, height: 720 },

  // Зберігати стан авторизації — не логінитися в кожному тесті
  storageState: 'playwright/.auth/admin.json',

  // Headless або ні (false = бачиш браузер)
  headless: true,

  // Записати відео при падінні
  video: 'on-first-retry',

  // Trace — детальна запис кожного кроку
  trace: 'on-first-retry',

  // Locale і timezone для тестів дат
  locale: 'uk-UA',
  timezoneId: 'Europe/Kyiv',

  // Extra HTTP заголовок для всіх запитів
  extraHTTPHeaders: {
    'x-test-run': 'playwright',
  },
},

Projects — multiple browsers or configs

Each project runs the full test suite with its own browser and settings — one config, many environments

Each project runs the full test suite with its own settings. The common use case: run on Chromium, Firefox, and WebKit. Another pattern: separate projects for authenticated and unauthenticated tests.

ts
// Три браузери — один рядок кожен
projects: [
  { name: 'chromium', use: { ...devices['Desktop Chrome'] } },
  { name: 'firefox',  use: { ...devices['Desktop Firefox'] } },
  { name: 'webkit',   use: { ...devices['Desktop Safari'] } },

  // Мобільний — viewport і touch автоматично
  { name: 'mobile-chrome', use: { ...devices['Pixel 5'] } },
  { name: 'mobile-safari', use: { ...devices['iPhone 13'] } },
],
ts
// Патерн: setup project логіниться, решта залежить від нього
projects: [
  // 1. Спочатку логінимось і зберігаємо стан
  {
    name: 'setup',
    testMatch: /.*\.setup\.ts/,
  },

  // 2. Тести адміна — залежать від setup
  {
    name: 'chromium',
    use: {
      ...devices['Desktop Chrome'],
      storageState: 'playwright/.auth/admin.json',
    },
    dependencies: ['setup'],
  },
],

Timeouts — test, expect, action

There are three independent timeouts to know. Mixing them up is a common source of confusion.

ts
export default defineConfig({
  // Максимальний час одного тесту (за замовчуванням: 30000ms)
  timeout: 30000,

  expect: {
    // Максимальний час очікування для expect(locator).toBeVisible() тощо
    // (за замовчуванням: 5000ms)
    timeout: 5000,
  },

  use: {
    // Максимальний час для однієї дії: click, fill, goto тощо
    // (за замовчуванням: не обмежено — береться з timeout тесту)
    actionTimeout: 10000,
    navigationTimeout: 15000,
  },
})

// Перевизначити для одного тесту:
test('slow report generation', async ({ page }) => {
  test.setTimeout(120000) // 2 хвилини для цього тесту
  await page.goto('/reports/generate')
  await expect(page.getByTestId('report-ready')).toBeVisible({ timeout: 90000 })
})

Global setup and teardown

Global setup runs once before any test starts. I use it to seed the database, create test users, or save an auth state file that all tests share. Global teardown runs once after all tests finish — clean up.

ts
// playwright.config.ts
export default defineConfig({
  globalSetup: require.resolve('./global-setup'),
  globalTeardown: require.resolve('./global-teardown'),
})

// global-setup.ts
import { chromium } from '@playwright/test'

export default async function globalSetup() {
  // Логінимось один раз, зберігаємо стан для всіх тестів
  const browser = await chromium.launch()
  const page = await browser.newPage()
  await page.goto('http://localhost:3000/login')
  await page.getByLabel('Email').fill('admin@example.com')
  await page.getByLabel('Password').fill(process.env.ADMIN_PASSWORD!)
  await page.getByRole('button', { name: 'Sign in' }).click()
  await page.waitForURL('/dashboard')

  // Зберігаємо cookies + localStorage
  await page.context().storageState({ path: 'playwright/.auth/admin.json' })
  await browser.close()
}