Claude Code Plugins

Community-maintained marketplace

Feedback

Use when tests fail intermittently. Replace arbitrary timeouts with condition polling. Eliminates flaky tests caused by timing assumptions.

Install Skill

1Download skill
2Enable skills in Claude

Open claude.ai/settings/capabilities and find the "Skills" section

3Upload to Claude

Click "Upload skill" and select the downloaded ZIP file

Note: Please verify skill by going through its instructions before using it.

SKILL.md

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:

// ❌ Arbitrary timeout - brittle and slow
await click('#submit-button');
await sleep(2000); // Hope 2 seconds is enough
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:

// ✅ Wait for specific condition
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) {
    // 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);
  }
}

Helper Functions

// 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;
}

Usage Examples

Example 1: Wait for Element

// ❌ 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();
});

Example 2: Wait for API Response

// ❌ 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();
});

Example 3: Wait for State Change

// ❌ 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');
});

Example 4: Wait for Event

// ❌ 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);
});

Example 5: Wait for Multiple Conditions

// ❌ 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');
});

Configuration Guidelines

Timeout Values

// 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

Interval Values

// 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

Framework-Specific Examples

Jest / Testing Library

import { waitFor } from '@testing-library/react';

test('example', async () => {
  render(<Component />);

  // Built-in waitFor
  await waitFor(() => expect(element).toBeVisible(), {
    timeout: 5000
  });
});

Playwright

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');
});

Cypress

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');
});

Laravel / PHP

// 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
    }
}

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

// ❌ Doesn't solve the problem
await waitForCondition(() => element.isVisible());
await sleep(500); // Why? You just waited for the condition!

Mistake 2: Condition too broad

// ❌ Too generic
await waitForCondition(() => elements.length > 0);

// ✅ Specific condition
await waitForCondition(() => elements.length === expectedCount);

Mistake 3: Timeout too short

// ❌ Too short for CI environments
await waitForCondition(condition, { timeout: 100 });

// ✅ Reasonable timeout
await waitForCondition(condition, { timeout: 5000 });

Mistake 4: Checking too infrequently

// ❌ Misses quick state changes
await waitForCondition(condition, { interval: 1000 });

// ✅ Frequent checks
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

# Run tests multiple times
for i in {1..10}; do npm test; done

# Note which tests fail intermittently

Step 2: Find Timeout Usage

# Search for sleep/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

# Run tests 100 times
for i in {1..100}; do npm test || break; done

# Should pass all 100 times

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:

  • I will NEVER use arbitrary timeouts
  • I will ALWAYS wait for specific conditions
  • I will use reasonable timeout values
  • I will poll frequently (10ms default)
  • I will make tests reliable, not just "usually work"

Bottom Line: Arbitrary timeouts are guesses. Condition-based waiting is verification. Wait for what you actually need, and tests become 100% reliable.