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

Parallelism

By default: test files run in parallel across workers, tests within one file run sequentially. That's almost always what you want. I set workers: 2 on CI for predictability, and use workerIndex to isolate test data between parallel workers.

How workers work

Each worker is an isolated OS process; files run sequentially inside a worker, workers run in parallel

Each worker is a separate OS process — its own Node.js instance, its own browser, no shared memory with other workers. Workers can't talk to each other. Playwright reuses a worker for multiple test files to avoid the browser startup cost — when one file finishes, the next file runs in the same worker.

After a test failure, the worker is always killed and a fresh one starts — guaranteeing no stale state bleeds into subsequent tests.

text
Запуск: 6 файлів, 3 воркери

Worker 1: [orders.spec.ts] → [reports.spec.ts]
Worker 2: [login.spec.ts] → [dashboard.spec.ts]
Worker 3: [filters.spec.ts] → [checkout.spec.ts]

Тести всередині orders.spec.ts — послідовно в Worker 1

Control the number of workers

By default Playwright uses half the CPU cores. On CI I pin workers to a fixed number — too many workers on a shared runner causes resource contention and flaky tests. Locally I let it use defaults.

ts
// playwright.config.ts
export default defineConfig({
  // На CI — 2 воркери (predictable); локально — дефолт (усі ядра / 2)
  workers: process.env.CI ? 2 : undefined,
})
bash
# Перевизначити через CLI
npx playwright test --workers 4

# Запустити без паралелізму (для дебагу)
npx playwright test --workers=1

fullyParallel — run tests within a file in parallel too

By default, tests within one file always run sequentially — that's fine for most cases. fullyParallel: true makes every individual test run in its own worker. Useful if you have large files with many independent tests. The downside: shared state (like let orderId between tests) breaks because each test runs in a separate process.

ts
// Глобально — кожен тест у своєму воркері
export default defineConfig({
  fullyParallel: true,
})
ts
// Тільки для конкретного файлу або describe
test.describe.configure({ mode: 'parallel' })

test('filter by status', async ({ page }) => {
  await page.goto('/orders')
  await page.getByRole('combobox', { name: 'Status' }).selectOption('pending')
  await expect(page.getByTestId('order-row')).not.toHaveCount(0)
})

test('filter by date', async ({ page }) => {
  await page.goto('/orders')
  await page.getByLabel('From date').fill('2024-01-01')
  await page.getByRole('button', { name: 'Apply' }).click()
  await expect(page.getByTestId('order-row')).toHaveCount(5)
})
ts
// Якщо fullyParallel увімкнено глобально — можна відключити для одного describe
test.describe('checkout flow', () => {
  test.describe.configure({ mode: 'default' }) // Sequential навіть при fullyParallel

  test('step 1: add to cart', async ({ page }) => { /* ... */ })
  test('step 2: enter shipping', async ({ page }) => { /* ... */ })
})

workerIndex — isolate test data between workers

When multiple workers run at the same time, they can't share a test user — they'd overwrite each other's data. I use workerIndex to give each worker its own user. Each worker gets a unique index (starting at 0), and I create a separate user per index.

ts
// fixtures.ts
import { test as base } from '@playwright/test'

export const test = base.extend({
  // Worker-scoped: один раз на воркер, не на кожен тест
  testUser: [async ({}, use, workerInfo) => {
    const userName = `worker-${workerInfo.workerIndex}@example.com`

    // Створити юзера для цього воркера
    await createUser(userName)
    await use(userName)

    // Прибрати після всіх тестів воркера
    await deleteUser(userName)
  }, { scope: 'worker' }],
})
ts
// test-worker-index через process.env (якщо не використовуєш фікстуру)
test('create order', async ({ page }, testInfo) => {
  const workerIdx = testInfo.workerIndex
  await page.goto(`/login?user=worker-${workerIdx}`)
  // ... тест використовує ізольованого юзера
})

maxFailures — stop early when things break

If 50 tests fail in the first minute, there's no point running the other 950. maxFailures stops the whole run after hitting the limit — saves CI time when there's a fundamental problem.

ts
// playwright.config.ts
export default defineConfig({
  // На CI: зупинитися після 10 падінь — не витрачати ресурси на зламаний suite
  maxFailures: process.env.CI ? 10 : undefined,
})
bash
# Або через CLI
npx playwright test --max-failures=5