Stop Flaky UI Tests with Playwright Web-First Locator Assertions
Use Playwright web-first assertions to wait for dynamic DOM updates automatically instead of adding fixed sleeps that cause flaky end-to-end tests.
25 Sept 2025, 16:06 UTC

Desired outcome
End-to-end tests should proceed as soon as the application renders the expected element, without fixed pauses that are either too short or unnecessarily long. The useful takeaway is to replace page.waitForTimeout() and manual polling with Playwright web-first assertions such as expect(locator).toBeVisible(). These assertions retry automatically until the condition matches or the configured timeout expires.
Prerequisites
You need a Node.js project with Playwright Test installed. A locator is Playwright's description of how to find an element; auto-retry means the assertion re-runs the check at intervals until it passes or times out.
# Run in the project root. Requires write access to package.json and node_modules.
npm install -D @playwright/test
Check the installed major version before relying on version-specific timeout defaults:
# Run in the project root.
npx playwright --version
Replace fixed waits with web-first assertions
Suppose a dashboard shows a welcome banner only after login completes. A common mistake is to wait two seconds and then check the banner. If the network is slow, the test fails; if the network is fast, the test wastes time.
import { test, expect } from '@playwright/test';
test('shows welcome banner after login', async ({ page }) => {
await page.goto('/login');
await page.locator('button[type="submit"]').click();
// Avoid fixed sleeps for synchronization.
// await page.waitForTimeout(2000);
// Web-first assertion: retries until visible or timeout expires.
await expect(page.locator('#welcome-banner')).toBeVisible();
await expect(page.locator('#welcome-banner')).toHaveText('Welcome, Maya');
});
toBeVisible waits for the element to be present in the DOM and visible to the user. toHaveText retries until the element's text matches the expected value. If the banner never appears, Playwright retries until the assertion timeout and then fails with the last observed state.
Configure assertion timeouts
Playwright Test applies a default expect timeout to web-first assertions. You can change it globally in the configuration file.
import { defineConfig } from '@playwright/test';
export default defineConfig({
expect: {
timeout: 10000,
},
});
Override the timeout for a single assertion when an element is known to load slowly:
await expect(page.locator('#status')).toHaveText('Processing', { timeout: 20000 });
Place configuration changes in the repository's playwright.config.ts or playwright.config.js at the project root. No special permissions are required beyond editing project files.
Expected checks
- Add a test that renders an element after a deliberate delay, such as a mocked API response, and confirm it passes without
waitForTimeout. - Run
npx playwright testfrom the project root and observe that the test waits for the element instead of failing immediately. - Change
toHaveTextto an intentionally wrong value and run the test. The failure report should show the retry timeout and the last observed text, which helps distinguish a timing problem from an application defect. - Compare the installed version against the current Playwright documentation because default timeouts and message formats can change between major versions.
Limitations and recovery
- Auto-retry applies only to Playwright web-first assertions on locators. Ordinary JavaScript assertions such as
assert.equal()do not retry and may fail prematurely on dynamic content. - Negative tests take longer because the assertion retries until timeout before failing. Use shorter timeouts for assertions that verify absence or error states.
- If a test hangs longer than expected, increase the timeout temporarily only after confirming the application under test is responding. Do not use long timeouts as a permanent substitute for fixing slow application behavior.
- There is no production state to roll back because this change modifies test code. Revert the commit if the new assertions introduce regressions.
Diagnostic decision: locator assertion versus manual wait
| Situation | Recommended approach |
|---|---|
| Element appears after navigation or user action | expect(locator).toBeVisible() |
| Element text updates asynchronously | expect(locator).toHaveText() |
| Need to pause for a non-UI reason, such as rate limiting | page.waitForTimeout() sparingly |
| Element must not appear | expect(locator).not.toBeVisible() with a shorter timeout |
Start by converting one flaky test that uses waitForTimeout. Run it repeatedly to confirm the failure disappears, then apply the same pattern to the rest of the suite.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.