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

Timeouts

Three independent timeout layers: test timeout (30s — how long the whole test can run), expect timeout (5s — how long an assertion retries), and action timeout (none by default — per-click/fill/goto). The mistake I see most often: raising retries when the real problem is the 5-second expect timeout hitting a slow API.

Three independent layers

The three timeout layers are independent — raising one does not affect the others

Playwright has three timeout layers that work independently. Increasing one doesn't affect the others.

ts
// Шар 1 — тест-тайм-аут: скільки може виконуватися весь тест
// За замовчуванням: 30_000 ms
// Включає: тіло тесту + beforeEach + setup фікстур

// Шар 2 — expect-тайм-аут: скільки авто-повторює перевірка
// За замовчуванням: 5_000 ms
// Застосовується до: expect(locator).toBeVisible(), toHaveText(), тощо

// Шар 3 — action-тайм-аут: скільки чекати на одну дію
// За замовчуванням: немає (наслідує тест-тайм-аут)
// Застосовується до: click(), fill(), goto()

Test timeout — 30 seconds by default

The test timeout covers everything: test body, beforeEach hooks, and fixture setup. After teardown, the same timeout value applies again for fixture teardown and afterEach hooks.

When this timeout fires you see: Timeout of 30000ms exceeded. For slow tests I use test.slow() instead of hardcoding a large number — it triples the current timeout without touching config.

ts
// playwright.config.ts — глобальний тайм-аут
export default defineConfig({
  timeout: 60_000, // 60 секунд для всіх тестів
})
ts
// test.slow() — потроїти тайм-аут для одного тесту
test('generate full report', async ({ page }) => {
  test.slow() // 30s * 3 = 90s для цього тесту
  await page.goto('/reports/generate')
  await page.getByRole('button', { name: 'Generate' }).click()
  await expect(page.getByTestId('report-ready')).toBeVisible()
})

// test.setTimeout() — задати явно
test('import large dataset', async ({ page }) => {
  test.setTimeout(120_000) // 2 хвилини тільки для цього тесту
  await page.goto('/import')
  await page.getByRole('button', { name: 'Import all' }).click()
  await expect(page.getByTestId('import-complete')).toBeVisible()
})

// Розширити тайм-аут з beforeEach (наприклад, якщо setup повільний)
test.beforeEach(async ({ page }, testInfo) => {
  testInfo.setTimeout(testInfo.timeout + 30_000)
})

Expect timeout — 5 seconds by default

This is the one that surprises people most. expect(locator).toBeVisible() doesn't just check once — it retries for up to 5 seconds. If the element appears in 4 seconds, the test passes. If it never appears, you get a timeout error.

For slow APIs or heavy animations I raise this to 10-15s globally, or pass a custom timeout per assertion.

ts
// playwright.config.ts
export default defineConfig({
  expect: {
    timeout: 10_000, // 10 секунд замість 5 для всіх перевірок
  },
})
ts
// Кастомний тайм-аут для одного assertion
test('dashboard loads slow widget', async ({ page }) => {
  await page.goto('/dashboard')

  // Цей віджет завантажується з зовнішнього API — до 15 секунд
  await expect(page.getByTestId('analytics-widget'))
    .toBeVisible({ timeout: 15_000 })

  // Для звичайних елементів — стандартні 5 секунд
  await expect(page.getByRole('heading', { name: 'Dashboard' })).toBeVisible()
})

Action timeout — none by default

Actions like click(), fill(), goto() have no independent timeout by default — they're bounded by the test timeout. You can set a global action timeout in config, or pass a timeout per action. I use actionTimeout: 10_000 on CI so a hung click fails fast instead of eating the full test timeout.

ts
// playwright.config.ts
export default defineConfig({
  use: {
    // Кожна дія (click, fill, тощо) — максимум 10 секунд
    actionTimeout: 10_000,
    // Кожна навігація (goto, reload) — максимум 30 секунд
    navigationTimeout: 30_000,
  },
})
ts
// Кастомний тайм-аут для однієї дії
test('submit order', async ({ page }) => {
  await page.goto('/orders/new', { timeout: 30_000 })
  await page.getByLabel('Item').fill('Laptop Stand')

  // Клік по кнопці — 5 секунд (швидка дія)
  await page.getByRole('button', { name: 'Create' }).click({ timeout: 5_000 })

  await expect(page.getByTestId('order-created-banner')).toBeVisible()
})

Global timeout — safety net for the whole run

No default. I set it to 1 hour on CI — if the whole suite takes longer than an hour, something is clearly wrong and I want CI to stop instead of running for hours eating up CI minutes.

ts
// playwright.config.ts
export default defineConfig({
  // Страховка: якщо весь запуск займає більше 60 хвилин — стоп
  globalTimeout: 60 * 60 * 1000, // 1 година
})

Fixture timeout — for slow worker-scoped setup

By default, fixtures share the test timeout. For slow worker-scoped fixtures — like seeding a database before the worker starts — I give them their own timeout so a slow seed doesn't eat into the test's 30 seconds.

ts
// Фікстура зі своїм тайм-аутом
export const test = base.extend({
  // Worker-scoped: запускається один раз перед усіма тестами воркера
  dbSeed: [async ({}, use) => {
    // Сидинг бази може займати до хвилини
    await seedTestDatabase()
    await use(null)
    await cleanupTestDatabase()
  }, { scope: 'worker', timeout: 60_000 }], // 60с — тільки для цієї фікстури
})

test('create order', async ({ page, dbSeed }) => {
  // Тест сам має стандартні 30с — фікстура їх не з'їдає
  await page.goto('/orders')
  await expect(page.getByTestId('order-list')).toBeVisible()
})