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 — глобальний тайм-аутexportdefault 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()
awaitexpect(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()
awaitexpect(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.tsexportdefault defineConfig({
expect: {
timeout: 10_000, // 10 секунд замість 5 для всіх перевірок
},
})
ts
// Кастомний тайм-аут для одного assertiontest('dashboard loads slow widget', async ({ page }) => {
await page.goto('/dashboard')
// Цей віджет завантажується з зовнішнього API — до 15 секундawaitexpect(page.getByTestId('analytics-widget'))
.toBeVisible({ timeout: 15_000 })
// Для звичайних елементів — стандартні 5 секундawaitexpect(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.tsexportdefault defineConfig({
use: {
// Кожна дія (click, fill, тощо) — максимум 10 секунд
actionTimeout: 10_000,
// Кожна навігація (goto, reload) — максимум 30 секунд
navigationTimeout: 30_000,
},
})
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.tsexportdefault 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
// Фікстура зі своїм тайм-аутомexportconsttest = 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')
awaitexpect(page.getByTestId('order-list')).toBeVisible()
})