| name | condition-based-waiting |
| description | Use when tests fail intermittently. Replace arbitrary timeouts with condition polling. Eliminates flaky tests caused by timing assumptions. |
Condition-Based Waiting
Core Principle
Wait for the actual condition you care about, not a guess about how long it takes.
Overview
Flaky tests often result from arbitrary timeouts (sleep(1000), setTimeout(500)) that assume operations complete within fixed time windows. Condition-based waiting polls for the specific condition you need, making tests reliable regardless of system speed.
When to Use This Skill
- Tests fail intermittently (pass locally, fail in CI)
- Tests use
sleep(), setTimeout(), or arbitrary delays
- Tests check for events, state changes, or async operations
- Debugging reveals race conditions
- Test reliability < 100%
The Problem with Timeouts
Bad Pattern:
await click('#submit-button');
await sleep(2000);
expect(successMessage).toBeVisible();
Why it fails:
- Too short: Test fails on slow systems
- Too long: Wastes time on fast systems
- No verification: Assumes operation completes
- Intermittent: Works 95% of time, fails randomly
The Solution: Condition Polling
Good Pattern:
await click('#submit-button');
await waitFor(() => successMessage.isVisible(), { timeout: 5000 });
expect(successMessage).toBeVisible();
Why it works:
- Fast systems: Completes immediately
- Slow systems: Waits as long as needed (up to timeout)
- Verification: Actually checks the condition
- Reliable: 100% pass rate
Implementation Pattern
Generic Polling Function
async function waitForCondition(
condition: () => boolean | Promise<boolean>,
options: {
timeout?: number; // Maximum wait time (default: 5000ms)
interval?: number; // Check interval (default: 10ms)
message?: string; // Error message if timeout
} = {}
): Promise<void> {
const {
timeout = 5000,
interval = 10,
message = 'Condition not met within timeout'
} = options;
const startTime = Date.now();
while (true) {
if (await condition()) {
return;
}
if (Date.now() - startTime > timeout) {
throw new Error(`${message} (waited ${timeout}ms)`);
}
await sleep(interval);
}
}
Helper Functions
async function waitForElement(
selector: string,
options?: { timeout?: number }
): Promise<Element> {
let element: Element | null = null;
await waitForCondition(
() => {
element = document.querySelector(selector);
return element !== null;
},
{
...options,
message: `Element "${selector}" not found`
}
);
return element!;
}
async function waitForEventCount(
events: any[],
expectedCount: number,
options?: { timeout?: number }
): Promise<void> {
await waitForCondition(
() => events.length >= expectedCount,
{
...options,
message: `Expected ${expectedCount} events, got ${events.length}`
}
);
}
async (): <> {
: = ;
(
{
matchedEvent = events.(predicate);
matchedEvent !== ;
},
{
...options,
:
}
);
matchedEvent;
}
Usage Examples
Example 1: Wait for Element
test('shows success message', async () => {
await click('#submit');
await sleep(1000);
expect(getByText('Success!')).toBeVisible();
});
test('shows success message', async () => {
await click('#submit');
await waitForElement('#success-message', { timeout: 5000 });
expect(getByText('Success!')).toBeVisible();
});
Example 2: Wait for API Response
test('loads user data', async () => {
fetchUserData(userId);
await sleep(2000);
expect(userData).toBeDefined();
});
test('loads user data', async () => {
fetchUserData(userId);
await waitForCondition(() => userData !== null, {
timeout: 5000,
message: 'User data not loaded'
});
expect(userData).toBeDefined();
});
Example 3: Wait for State Change
test('completes upload', async () => {
startUpload(file);
await sleep(3000);
expect(uploadStatus).toBe('complete');
});
test('completes upload', async () => {
startUpload(file);
await waitForCondition(() => uploadStatus === 'complete', {
timeout: 10000,
message: 'Upload did not complete'
});
expect(uploadStatus).toBe('complete');
});
Example 4: Wait for Event
test('emits analytics event', async () => {
const events = [];
analytics.on('event', e => events.push(e));
await performAction();
await sleep(500);
expect(events).toHaveLength(1);
});
test('emits analytics event', async () => {
const events = [];
analytics.on('event', e => events.push(e));
await performAction();
await waitForEventCount(events, 1, { timeout: 2000 });
expect(events).toHaveLength(1);
});
Example 5: Wait for Multiple Conditions
test('completes multi-step process', async () => {
startProcess();
await sleep(1000);
await sleep(1000);
await sleep(1000);
expect(status).toBe('complete');
});
test('completes multi-step process', async () => {
startProcess();
await waitForCondition(() => step1Complete, {
message: 'Step 1 not complete'
});
await waitForCondition(() => step2Complete, {
message: 'Step 2 not complete'
});
await waitForCondition(() => step3Complete, {
message: 'Step 3 not complete'
});
expect(status).toBe('complete');
});
Configuration Guidelines
Timeout Values
{ timeout: 1000 }
{ timeout: 5000 }
{ timeout: 10000 }
{ timeout: 30000 }
Interval Values
{ interval: 10 }
{ interval: 1 }
{ interval: 100 }
Framework-Specific Examples
Jest / Testing Library
import { waitFor } from '@testing-library/react';
test('example', async () => {
render(<Component />);
await waitFor(() => expect(element).toBeVisible(), {
timeout: 5000
});
});
Playwright
test('example', async ({ page }) => {
await page.click('#submit');
await page.waitForSelector('#success', { timeout: 5000 });
await page.waitForFunction(() => window.status === 'ready');
});
Cypress
it('example', () => {
cy.click('#submit');
cy.get('#success', { timeout: 5000 }).should('be.visible');
cy.window().its('status').should('equal', 'ready');
});
Laravel / PHP
test('async operation completes', function () {
$job = new ProcessJob();
$job->dispatch();
expect(fn() => $job->isComplete())
->toBeTrue()
->eventually(timeout: 5);
});
function waitForCondition(callable $condition, int $timeoutMs = 5000): void
{
$start = microtime(true) * 1000;
while (true) {
if ($condition()) {
return;
}
if ((microtime(true) * 1000) - $start > $timeoutMs) {
throw new Exception("Timeout after {$timeoutMs}ms");
}
usleep(10000);
}
}
Real-World Impact
Before condition-based waiting:
- Test suite reliability: 60-80%
- Intermittent failures: 20-40%
- Average test time: Slower (excessive timeouts)
- Developer frustration: High
- CI reruns needed: Frequent
After condition-based waiting:
- Test suite reliability: 100%
- Intermittent failures: 0%
- Average test time: 40% faster (no excessive waits)
- Developer frustration: Low
- CI reruns needed: Rare
Common Mistakes
Mistake 1: Still using timeouts
await waitForCondition(() => element.isVisible());
await sleep(500);
Mistake 2: Condition too broad
await waitForCondition(() => elements.length > 0);
await waitForCondition(() => elements.length === expectedCount);
Mistake 3: Timeout too short
await waitForCondition(condition, { timeout: 100 });
await waitForCondition(condition, { timeout: 5000 });
Mistake 4: Checking too infrequently
await waitForCondition(condition, { interval: 1000 });
await waitForCondition(condition, { interval: 10 });
Integration with Other Skills
Use with:
test-driven-development - Write reliable tests from the start
systematic-debugging - Eliminate flaky test failures
testing-anti-patterns - Avoid async testing mistakes
When to apply:
- Any test with timeouts or sleeps
- Tests that fail intermittently
- CI tests that pass locally but fail remotely
- Tests involving async operations
Migration Strategy
Step 1: Identify Flaky Tests
for i in {1..10}; do npm test; done
Step 2: Find Timeout Usage
grep -r "sleep(" tests/
grep -r "setTimeout" tests/
grep -r "delay(" tests/
Step 3: Replace with Conditions
For each timeout, ask:
- What condition am I actually waiting for?
- How can I check that condition directly?
- What's a reasonable timeout?
Step 4: Verify Improvement
for i in {1..100}; do npm test || break; done
Authority
This skill is based on:
- Test automation best practices
- Industry standard: All modern test frameworks support condition-based waiting
- Real-world evidence: Improves reliability from 60% to 100%
- Performance benefit: 40% faster test execution
Social Proof: Playwright, Cypress, Testing Library all use condition-based waiting as default.
Your Commitment
When writing tests:
Bottom Line: Arbitrary timeouts are guesses. Condition-based waiting is verification. Wait for what you actually need, and tests become 100% reliable.