1. Diagnose Timeout Triggers: Network vs. DOM vs. Configuration #
Before adjusting thresholds, isolate the failure vector. Auto-wait timeouts typically stem from delayed DOM hydration, unhandled network requests, or misaligned global timeout values. Use Playwright’s trace viewer (npx playwright show-trace trace.zip) and page.on('console') listeners to verify element detachment or overlay interference. Cross-reference network waterfall data with DOM readiness states. This diagnostic step is critical when investigating broader Root Causes of JavaScript Test Flakiness.
2. Optimize Global & Per-Action Timeout Thresholds #
Global timeouts apply uniformly, masking localized bottlenecks. Scope thresholds to specific actions using locator.click({ timeout: 5000 }) or page.waitForSelector(). Align values with your application’s SLA. Set navigation timeouts higher for heavy SPAs, but keep interaction timeouts tight to catch regressions early. Avoid blanket increases that degrade CI feedback loops.
3. Implement Custom Wait Strategies for Async State #
When auto-waiting fails due to framework-specific state hydration, replace implicit waits with explicit, condition-based polling. Use expect(locator).toBeVisible() or page.waitForFunction() to assert state completion. This approach eliminates race conditions without compromising Playwright’s built-in actionability guarantees.
Configuration & Code Examples #
// playwright.config.ts - Global timeout override
import { defineConfig } from '@playwright/test';
export default defineConfig({
use: {
actionTimeout: 5000, // Time for each action (click, fill, etc.)
navigationTimeout: 10000, // Time for page.goto() and similar navigations
},
});
// Per-action scoped timeout — overrides global actionTimeout for this call only
test('submit form with tight timeout', async ({ page }) => {
await page.goto('/form');
await page.locator('#submit-btn').click({ timeout: 3000 });
});
// Explicit state wait (replaces auto-wait for custom DOM conditions)
// page.waitForFunction() runs the predicate in the browser context.
await page.waitForFunction(() => {
const el = document.querySelector('#dynamic-content');
return el !== null && el.textContent !== null && el.textContent.includes('Loaded');
});
// Use expect().toBeVisible() when the element exists but may be hidden.
// Playwright retries this assertion until the timeout is reached.
import { expect } from '@playwright/test';
await expect(page.locator('#dynamic-content')).toBeVisible({ timeout: 8000 });
Common Pitfalls #
- Increasing global timeouts to mask underlying async race conditions
- Using
page.waitForTimeout()for hard delays instead of state-based waits — this is an anti-pattern that hides flakiness - Overriding actionability checks with
{ force: true }, causing false positives on elements that should not be interactable - Failing to account for CSS transitions or layout shifts that temporarily block interaction
FAQ #
Q: Why does Playwright auto-wait timeout even when the element is visible?
A: Visibility alone is insufficient. Playwright requires elements to be stable (no pending CSS animations), actionable (not covered by overlays), and attached to the DOM. Use the trace viewer (--trace on) to inspect actionability blockers — common causes are overlapping modals, loading spinners, and CSS pointer-events: none.
Q: Should I disable auto-waiting for faster test execution? A: No. Disabling auto-waiting removes Playwright’s core reliability engine. Instead, optimize timeout scopes and use explicit waits for framework-specific hydration states.
Q: How do I differentiate between a flaky test and a genuine timeout?
A: Run the test locally with --repeat-each=5 and capture traces. Consistent failures at the same DOM state indicate configuration or app issues; intermittent failures that pass on retry point to race conditions or environment drift.
Reliability Metrics #
| Metric | Target / Tracking Method |
|---|---|
timeout_failure_rate |
Target < 2% of total test runs |
auto_wait_bypass_frequency |
Track explicit wait usage vs. auto-wait reliance |
ci_feedback_latency |
Monitor average test duration post-timeout optimization |
flake_resolution_time |
Measure MTTR for timeout-related failures |