| name | condition-based-waiting |
| description | Anti-flaky test patterns using condition-based waiting instead of fixed timeouts. Trigger: flaky tests, waitForTimeout, race conditions, test stability, async waiting. |
Condition-Based Waiting
Anti-flaky test patterns using condition-based waiting instead of fixed timeouts.
Quick Reference
| Pattern | Use When |
|---|
locator.waitFor() | Element state changes |
expect(locator).toBeVisible() | Visibility assertions |
page.waitForFunction() | Custom JS conditions |
expect.poll() | Repeated value checks |
page.waitForLoadState() | Page lifecycle events |
Core Principle
Wait for conditions, not time.
await page.waitForTimeout(2000);
await page.locator('.result').waitFor({ state: 'visible' });
await expect(page.locator('.result')).toBeVisible();
Patterns
1. Element State Waiting
await page.locator('.content').waitFor({ state: 'attached' });
await page.locator('.modal').waitFor({ state: 'visible' });
await page.locator('.loading').waitFor({ state: 'hidden' });
await page.locator('.slow-element').waitFor({
state: 'visible',
timeout: 10000
});
2. Assertion-Based Waiting
await expect(page.locator('.status')).toHaveText('Complete');
await expect(page.locator('.count')).toHaveText('5');
await expect(page.locator('.item')).toHaveCount(3);
await expect(page.locator('.loading')).not.toBeVisible();
await expect(page.locator('.error')).not.toBeAttached();
await expect(page.locator('.result')).toBeVisible({ timeout: 10000 });
3. Custom Condition Waiting
await page.waitForFunction(() => {
return document.querySelectorAll('.item').length >= 5;
});
const minCount = 5;
await page.waitForFunction(
(count) => document.querySelectorAll('.item').length >= count,
minCount
);
await page.waitForFunction(
(selector) => document.querySelector(selector)?.textContent?.includes('Done'),
'.status'
);
4. Polling Pattern
await expect.poll(async () => {
const response = await page.request.get('/api/status');
return await response.text();
}).toBe('ready');
await expect.poll(async () => {
return await page.locator('.count').textContent();
}, {
message: 'Item count should increase',
timeout: 15000,
intervals: [500, 1000, 2000]
}).toBe('10');
5. Network-Based Waiting
const responsePromise = page.waitForResponse('**/api/data');
await page.locator('.load-button').click();
const response = await responsePromise;
await Promise.all([
page.waitForResponse(resp =>
resp.url().includes('/api/data') && resp.status() === 200
),
page.locator('.submit').click()
]);
await page.waitForLoadState('networkidle');
6. Navigation Waiting
await page.goto('/dashboard');
await page.waitForLoadState('domcontentloaded');
await page.waitForLoadState('load');
await page.waitForLoadState('networkidle');
await page.waitForURL('**/dashboard');
await page.waitForURL(/\/dashboard/);
7. Download and File Waiting
const downloadPromise = page.waitForEvent('download');
await page.locator('.download-btn').click();
const download = await downloadPromise;
const fileChooserPromise = page.waitForEvent('filechooser');
await page.locator('.upload-btn').click();
const fileChooser = await fileChooserPromise;
await fileChooser.setFiles('document.pdf');
8. Dialog and Popup Waiting
page.on('dialog', async dialog => {
expect(dialog.message()).toBe('Are you sure?');
await dialog.accept();
});
await page.locator('.delete-btn').click();
const popupPromise = page.waitForEvent('popup');
await page.locator('.open-window').click();
const popup = await popupPromise;
await popup.waitForLoadState();
Anti-Patterns to Avoid
❌ Fixed Timeouts
await page.waitForTimeout(2000);
await page.waitForTimeout(5000);
await expect(page.locator('.result')).toBeVisible();
❌ Busy Waiting
while (await page.locator('.loading').isVisible()) {
await page.waitForTimeout(100);
}
await page.locator('.loading').waitFor({ state: 'hidden' });
await expect(page.locator('.loading')).not.toBeVisible();
❌ Missing Await
page.locator('.submit').click();
expect(page.locator('.result')).toBeVisible();
await page.locator('.submit').click();
await expect(page.locator('.result')).toBeVisible();
❌ Wrong Wait Target
await page.locator('.container').waitFor();
await page.locator('.container .item').first().waitFor();
Decision Matrix
Need to wait for... → Use this pattern
─────────────────────────────────────────────────
Element visible/hidden → locator.waitFor({ state })
Element text/value → expect(locator).toHaveText()
Element count → expect(locator).toHaveCount()
JS condition → page.waitForFunction()
API response → page.waitForResponse()
Page navigation → page.waitForURL() / waitForLoadState()
Download/popup → page.waitForEvent()
Repeated value check → expect.poll()
Element NOT present → expect(locator).not.toBeAttached()
Integration with Test Structure
import { test, expect } from '@playwright/test';
test.describe('Feature with proper waiting', () => {
test.beforeEach(async ({ page }) => {
await page.goto('/app');
await page.waitForLoadState('domcontentloaded');
});
test('loads data and displays results', async ({ page }) => {
await page.locator('.load-data').click();
await expect(page.locator('.loading')).not.toBeVisible();
await expect(page.locator('.result-item')).toHaveCount(5);
await expect(page.locator('.result-item').first()).toHaveText(/expected/);
});
test('handles async operations', async ({ page }) => {
const responsePromise = page.waitForResponse('**/api/data');
await page.locator('.fetch-btn').click();
const response = await responsePromise;
expect(response.status()).toBe(200);
await expect(page.locator('.data-display')).toBeVisible();
});
});
References