skills/testing/condition-based-waiting/SKILL.md
Use when tests fail intermittently. Replace arbitrary timeouts with condition polling. Eliminates flaky tests caused by timing assumptions.
npx skillsauth add liauw-media/codeassist condition-based-waitingInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
Wait for the actual condition you care about, not a guess about how long it takes.
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.
sleep(), setTimeout(), or arbitrary delaysBad Pattern:
// ❌ Arbitrary timeout - brittle and slow
await click('#submit-button');
await sleep(2000); // Hope 2 seconds is enough
expect(successMessage).toBeVisible();
Why it fails:
Good Pattern:
// ✅ Wait for specific condition
await click('#submit-button');
await waitFor(() => successMessage.isVisible(), { timeout: 5000 });
expect(successMessage).toBeVisible();
Why it works:
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) {
// Check condition
if (await condition()) {
return; // Success!
}
// Check timeout
if (Date.now() - startTime > timeout) {
throw new Error(`${message} (waited ${timeout}ms)`);
}
// Wait before next check
await sleep(interval);
}
}
// Wait for element to exist
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!;
}
// Wait for event count
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}`
}
);
}
// Wait for event matching condition
async function waitForEventMatch(
events: any[],
predicate: (event: any) => boolean,
options?: { timeout?: number }
): Promise<any> {
let matchedEvent: any = null;
await waitForCondition(
() => {
matchedEvent = events.find(predicate);
return matchedEvent !== undefined;
},
{
...options,
message: 'No event matched predicate'
}
);
return matchedEvent;
}
// ❌ Bad: Arbitrary timeout
test('shows success message', async () => {
await click('#submit');
await sleep(1000);
expect(getByText('Success!')).toBeVisible();
});
// ✅ Good: Wait for condition
test('shows success message', async () => {
await click('#submit');
await waitForElement('#success-message', { timeout: 5000 });
expect(getByText('Success!')).toBeVisible();
});
// ❌ Bad: Arbitrary timeout
test('loads user data', async () => {
fetchUserData(userId);
await sleep(2000);
expect(userData).toBeDefined();
});
// ✅ Good: Wait for condition
test('loads user data', async () => {
fetchUserData(userId);
await waitForCondition(() => userData !== null, {
timeout: 5000,
message: 'User data not loaded'
});
expect(userData).toBeDefined();
});
// ❌ Bad: Arbitrary timeout
test('completes upload', async () => {
startUpload(file);
await sleep(3000);
expect(uploadStatus).toBe('complete');
});
// ✅ Good: Wait for condition
test('completes upload', async () => {
startUpload(file);
await waitForCondition(() => uploadStatus === 'complete', {
timeout: 10000,
message: 'Upload did not complete'
});
expect(uploadStatus).toBe('complete');
});
// ❌ Bad: Arbitrary timeout
test('emits analytics event', async () => {
const events = [];
analytics.on('event', e => events.push(e));
await performAction();
await sleep(500);
expect(events).toHaveLength(1);
});
// ✅ Good: Wait for condition
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);
});
// ❌ Bad: Multiple timeouts
test('completes multi-step process', async () => {
startProcess();
await sleep(1000); // Wait for step 1
await sleep(1000); // Wait for step 2
await sleep(1000); // Wait for step 3
expect(status).toBe('complete');
});
// ✅ Good: Wait for each condition
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');
});
// Quick operations (DOM updates)
{ timeout: 1000 } // 1 second
// Network requests
{ timeout: 5000 } // 5 seconds
// Heavy operations (file uploads, processing)
{ timeout: 10000 } // 10 seconds
// Very slow operations
{ timeout: 30000 } // 30 seconds
// Default: Good for most cases
{ interval: 10 } // 10ms - checks 100 times per second
// Fast polling: UI updates
{ interval: 1 } // 1ms - checks 1000 times per second
// Slow polling: Network/disk operations
{ interval: 100 } // 100ms - checks 10 times per second
import { waitFor } from '@testing-library/react';
test('example', async () => {
render(<Component />);
// Built-in waitFor
await waitFor(() => expect(element).toBeVisible(), {
timeout: 5000
});
});
test('example', async ({ page }) => {
await page.click('#submit');
// Built-in auto-waiting
await page.waitForSelector('#success', { timeout: 5000 });
// Or with custom condition
await page.waitForFunction(() => window.status === 'ready');
});
it('example', () => {
cy.click('#submit');
// Built-in retry assertions
cy.get('#success', { timeout: 5000 }).should('be.visible');
// Or custom condition
cy.window().its('status').should('equal', 'ready');
});
// Using Laravel's eventually() helper (Laravel 10+)
test('async operation completes', function () {
$job = new ProcessJob();
$job->dispatch();
// Wait for condition
expect(fn() => $job->isComplete())
->toBeTrue()
->eventually(timeout: 5);
});
// Manual implementation
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); // 10ms
}
}
Before condition-based waiting:
After condition-based waiting:
// ❌ Doesn't solve the problem
await waitForCondition(() => element.isVisible());
await sleep(500); // Why? You just waited for the condition!
// ❌ Too generic
await waitForCondition(() => elements.length > 0);
// ✅ Specific condition
await waitForCondition(() => elements.length === expectedCount);
// ❌ Too short for CI environments
await waitForCondition(condition, { timeout: 100 });
// ✅ Reasonable timeout
await waitForCondition(condition, { timeout: 5000 });
// ❌ Misses quick state changes
await waitForCondition(condition, { interval: 1000 });
// ✅ Frequent checks
await waitForCondition(condition, { interval: 10 });
Use with:
test-driven-development - Write reliable tests from the startsystematic-debugging - Eliminate flaky test failurestesting-anti-patterns - Avoid async testing mistakesWhen to apply:
# Run tests multiple times
for i in {1..10}; do npm test; done
# Note which tests fail intermittently
# Search for sleep/timeout usage
grep -r "sleep(" tests/
grep -r "setTimeout" tests/
grep -r "delay(" tests/
For each timeout, ask:
# Run tests 100 times
for i in {1..100}; do npm test || break; done
# Should pass all 100 times
This skill is based on:
Social Proof: Playwright, Cypress, Testing Library all use condition-based waiting as default.
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.
development
Use when decomposing complex work. Dispatch fresh subagent per task, review between tasks. Flow: Load plan → Dispatch task → Review output → Apply feedback → Mark complete → Next task. No skipping reviews, no parallel dispatch.
development
# Server Documentation System Set up a documentation system that tracks changes and maintains server/project documentation with Claude Code hooks. ## When to Use - Setting up a new server or development environment - Need to track configuration changes over time - Want automatic documentation of work sessions - Maintaining changelog for infrastructure ## Directory Structure ``` ~/docs/ # User home directory (cross-platform) ├── changelog.md # Global over
development
Delegate tasks to remote Claude Code agent containers for parallel execution, long-running analysis, or resource-intensive operations.
development
Use when working on multiple features simultaneously. Creates isolated workspaces without branch switching, enabling parallel development.