How Playwright's Auto‑Waiting Stops Flaky UI Tests (and When to Add Your Own Waits)
Learn how Playwright’s auto‑waiting removes flaky UI tests, see a concrete search‑widget example, and understand the tracing and WebKit limits you need to manage.
12 Dec 2025, 08:24 UTC

Problem: Flaky clicks on dynamically loaded buttons
When a test clicks a button that appears after an AJAX request, the interaction can happen before the button is ready, causing intermittent failures. This flakiness forces teams to sprinkle explicit waits or sleep statements throughout their test suites, which makes tests slower and harder to maintain.
How Playwright’s auto‑waiting works
Before any action (click, fill, selectOption, etc.) Playwright performs a series of actionability checks on the target element: it verifies that the element is attached to the DOM, visible, stable, not obscured, and enabled. If any check fails, Playwright retries the checks automatically until the element becomes actionable or the default timeout (30 seconds) is exceeded. This built‑in retry loop eliminates the need for most manual waitForSelector calls.
Web‑first assertions automatically retry
Playwright’s expect assertions are “web‑first”: they repeatedly evaluate the condition until it passes or the assertion timeout is reached. For example, await expect(page.locator('#results')).toHaveText(/hello/) will keep polling the text content, handling updates from network responses without extra code.
Worked example: testing a search widget
Consider a search widget that shows results after a fetch request. The test below demonstrates how auto‑waiting and web‑first assertions interact.
// tests/search.spec.ts
import { test, expect } from '@playwright/test';
test('displays results after search', async ({ page }) => {
// Navigate to the page containing the widget
await page.goto('https://example.com/search-widget');
// Fill the search input – Playwright waits for the input to be actionable
await page.fill('#search-input', 'playwright');
// Press Enter – the button click is also auto‑waited
await page.press('#search-input', 'Enter');
// Assertion retries until the results contain the expected text
await expect(page.locator('#results')).toHaveText(/playwright/i);
});
To capture the waiting behavior for later inspection, run the test with the trace flag:
# Run from the project root
npx playwright test tests/search.spec.ts --trace-on-first-retry
This command requires no special permissions; it executes the test in the local Node environment. If the test passes, the process exits with code 0 and a trace file (trace.zip) is created in the playwright-test-results directory. You can examine the trace with:
npx playwright show-trace playwright-test-results/trace.zip
The trace shows timestamps for each actionability check and each assertion retry, confirming that Playwright waited for the button to become actionable before clicking and that the assertion retried until the results appeared.
Trade‑off and limitation
While auto‑waiting reduces flakiness, it introduces two practical considerations:
- Trace and video size: Enabling
--trace-on-first-retryor video recording increases CI artifact storage. In resource‑constrained pipelines you may need to limit retention (e.g., keep traces only for failed tests) or disable tracing for fast‑running suites. - WebKit lag: Some newer WebKit features (e.g., certain CSS properties or JavaScript APIs) are not yet fully supported, which can cause auto‑waiting to time out on Safari‑specific scenarios. Verify compatibility by running a subset of tests against WebKit and checking for timeout errors.
To check whether a test is hitting the auto‑waiting timeout, look at the test output for messages like Timeout 30000ms exceeded. If you see such a message, increase the timeout for that action (await page.click('#btn', { timeout: 5000 })) or investigate whether the element ever becomes actionable.
Actionable closing
Leverage Playwright’s auto‑waiting and web‑first assertions to write stable cross‑browser tests with fewer explicit waits. Start by adding --trace-on-first-retry to your test runs to verify that waiting behaves as expected, then adjust trace retention policies to balance debugging visibility with CI storage costs. When testing Safari‑specific features, run a dedicated WebKit suite and treat any timeout as a signal to polyfill or adjust the test rather than raising the global timeout.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.