Claude Code Plugins

Community-maintained marketplace

Feedback

condition-based-waiting

@liauw-media/CodeAssist
0
0

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.