CtrlK
BlogDocsLog inGet started
Tessl Logo

tessl/npm-storybook--testing-library

Instrumented version of Testing Library for Storybook Interactions addon

Pending
Overview
Eval results
Files

async-utilities.mddocs/

Async Utilities

Asynchronous utilities for waiting for DOM changes and element state transitions, with instrumentation for Storybook interaction tracking. These utilities help test dynamic content and asynchronous operations.

Capabilities

Wait For Function

Waits for a condition to become true by repeatedly calling a callback function until it succeeds or times out. Instrumented for Storybook interactions.

/**
 * Wait for a condition to be true by repeatedly calling callback
 * @param callback - Function to call repeatedly until it succeeds
 * @param options - Timeout and retry configuration
 * @returns Promise that resolves with callback result or rejects on timeout
 */
function waitFor<T>(
  callback: () => T | Promise<T>,
  options?: WaitForOptions
): Promise<T>;

/**
 * Configuration options for waitFor function
 */
interface WaitForOptions {
  /** Maximum time to wait in milliseconds (default: 1000) */
  timeout?: number;
  /** Time between retries in milliseconds (default: 50) */
  interval?: number;
  /** Custom error handler for timeout scenarios */
  onTimeout?: (error: Error) => Error;
  /** Show original stack trace in errors */
  showOriginalStackTrace?: boolean;
  /** Custom error message for timeout */
  errorMessage?: string;
}

Wait For Element To Be Removed

Waits for an element to be removed from the DOM. Instrumented for Storybook interactions.

/**
 * Wait for an element to be removed from the DOM
 * @param callback - Function returning element to wait for removal, or the element itself
 * @param options - Timeout and retry configuration  
 * @returns Promise that resolves when element is removed or rejects on timeout
 */
function waitForElementToBeRemoved<T>(
  callback: (() => T) | T,
  options?: WaitForOptions
): Promise<void>;

Usage Examples

Basic Wait For Usage

import { within, waitFor, userEvent } from "@storybook/testing-library";

export const BasicWaitForExample = {
  play: async ({ canvasElement }) => {
    const canvas = within(canvasElement);
    
    // Click button that triggers async operation
    const loadButton = canvas.getByRole('button', { name: /load data/i });
    await userEvent.click(loadButton);
    
    // Wait for loading to complete and data to appear
    await waitFor(() => {
      expect(canvas.getByText(/data loaded successfully/i)).toBeInTheDocument();
    });
    
    // Verify data is displayed
    const dataItems = canvas.getAllByTestId('data-item');
    expect(dataItems).toHaveLength(3);
  }
};

Wait With Custom Timeout

import { within, waitFor, userEvent } from "@storybook/testing-library";

export const CustomTimeoutExample = {
  play: async ({ canvasElement }) => {
    const canvas = within(canvasElement);
    
    // Operation that takes longer than default timeout
    const slowButton = canvas.getByRole('button', { name: /slow operation/i });
    await userEvent.click(slowButton);
    
    // Wait with extended timeout
    await waitFor(
      () => {
        expect(canvas.getByText(/operation completed/i)).toBeInTheDocument();
      },
      { timeout: 5000, interval: 200 } // Wait up to 5 seconds, check every 200ms
    );
  }
};

Wait For Element Removal

import { within, waitForElementToBeRemoved, userEvent } from "@storybook/testing-library";

export const ElementRemovalExample = {
  play: async ({ canvasElement }) => {
    const canvas = within(canvasElement);
    
    // Find element that will be removed
    const loadingSpinner = canvas.getByTestId('loading-spinner');
    expect(loadingSpinner).toBeInTheDocument();
    
    // Trigger action that removes the element
    const startButton = canvas.getByRole('button', { name: /start/i });
    await userEvent.click(startButton);
    
    // Wait for loading spinner to be removed
    await waitForElementToBeRemoved(loadingSpinner);
    
    // Verify content appears after loading
    expect(canvas.getByText(/content loaded/i)).toBeInTheDocument();
  }
};

Wait For Multiple Conditions

import { within, waitFor, userEvent } from "@storybook/testing-library";

export const MultipleConditionsExample = {
  play: async ({ canvasElement }) => {
    const canvas = within(canvasElement);
    
    const submitButton = canvas.getByRole('button', { name: /submit/i });
    await userEvent.click(submitButton);
    
    // Wait for multiple conditions to be true
    await waitFor(() => {
      // All these conditions must be true
      expect(canvas.getByText(/form submitted/i)).toBeInTheDocument();
      expect(canvas.getByTestId('success-icon')).toBeInTheDocument();
      expect(canvas.queryByTestId('loading')).not.toBeInTheDocument();
    });
  }
};

Async Form Validation

import { within, waitFor, userEvent } from "@storybook/testing-library";

export const AsyncValidationExample = {
  play: async ({ canvasElement }) => {
    const canvas = within(canvasElement);
    
    const emailInput = canvas.getByLabelText(/email/i);
    
    // Type invalid email
    await userEvent.type(emailInput, 'invalid-email');
    await userEvent.tab(); // Trigger validation
    
    // Wait for validation error to appear
    await waitFor(() => {
      expect(canvas.getByText(/invalid email format/i)).toBeInTheDocument();
    });
    
    // Clear and type valid email
    await userEvent.clear(emailInput);
    await userEvent.type(emailInput, 'user@example.com');
    
    // Wait for error to disappear
    await waitFor(() => {
      expect(canvas.queryByText(/invalid email format/i)).not.toBeInTheDocument();
    });
  }
};

API Response Waiting

import { within, waitFor, userEvent } from "@storybook/testing-library";

export const ApiResponseExample = {
  play: async ({ canvasElement }) => {
    const canvas = within(canvasElement);
    
    const searchInput = canvas.getByLabelText(/search/i);
    const searchButton = canvas.getByRole('button', { name: /search/i });
    
    // Perform search
    await userEvent.type(searchInput, 'test query');
    await userEvent.click(searchButton);
    
    // Wait for loading state
    await waitFor(() => {
      expect(canvas.getByTestId('loading')).toBeInTheDocument();
    });
    
    // Wait for results to load
    await waitFor(
      () => {
        expect(canvas.queryByTestId('loading')).not.toBeInTheDocument();
        expect(canvas.getByText(/search results/i)).toBeInTheDocument();
      },
      { timeout: 3000 }
    );
    
    // Verify results
    const results = canvas.getAllByTestId('search-result');
    expect(results.length).toBeGreaterThan(0);
  }
};

Animation Waiting

import { within, waitFor, userEvent } from "@storybook/testing-library";

export const AnimationExample = {
  play: async ({ canvasElement }) => {
    const canvas = within(canvasElement);
    
    const toggleButton = canvas.getByRole('button', { name: /toggle panel/i });
    await userEvent.click(toggleButton);
    
    // Wait for animation to start
    await waitFor(() => {
      const panel = canvas.getByTestId('animated-panel');
      expect(panel).toHaveClass('animating');
    });
    
    // Wait for animation to complete
    await waitFor(
      () => {
        const panel = canvas.getByTestId('animated-panel');
        expect(panel).toHaveClass('visible');
        expect(panel).not.toHaveClass('animating');
      },
      { timeout: 2000 } // Animations can take time
    );
  }
};

Error Handling

import { within, waitFor, userEvent } from "@storybook/testing-library";

export const ErrorHandlingExample = {
  play: async ({ canvasElement }) => {
    const canvas = within(canvasElement);
    
    const failButton = canvas.getByRole('button', { name: /trigger error/i });
    await userEvent.click(failButton);
    
    try {
      // This will timeout and throw an error
      await waitFor(
        () => {
          expect(canvas.getByText(/this will never appear/i)).toBeInTheDocument();
        },
        { 
          timeout: 1000,
          onTimeout: (error) => new Error(`Custom error: ${error.message}`)
        }
      );
    } catch (error) {
      // Handle the timeout error
      console.log('Expected timeout occurred:', error.message);
    }
    
    // Verify error state instead
    await waitFor(() => {
      expect(canvas.getByText(/error occurred/i)).toBeInTheDocument();
    });
  }
};

Wait For Custom Condition

import { within, waitFor, userEvent } from "@storybook/testing-library";

export const CustomConditionExample = {
  play: async ({ canvasElement }) => {
    const canvas = within(canvasElement);
    
    const incrementButton = canvas.getByRole('button', { name: /increment/i });
    
    // Click button multiple times
    await userEvent.click(incrementButton);
    await userEvent.click(incrementButton);
    await userEvent.click(incrementButton);
    
    // Wait for counter to reach specific value
    await waitFor(() => {
      const counter = canvas.getByTestId('counter');
      const value = parseInt(counter.textContent || '0');
      expect(value).toBeGreaterThanOrEqual(3);
    });
    
    // Wait for element to have specific style
    await waitFor(() => {
      const progressBar = canvas.getByTestId('progress-bar');
      const width = getComputedStyle(progressBar).width;
      expect(parseInt(width)).toBeGreaterThan(50);
    });
  }
};

Polling Pattern

import { within, waitFor, userEvent } from "@storybook/testing-library";

export const PollingExample = {
  play: async ({ canvasElement }) => {
    const canvas = within(canvasElement);
    
    const refreshButton = canvas.getByRole('button', { name: /auto refresh/i });
    await userEvent.click(refreshButton);
    
    // Poll for status changes
    let attempts = 0;
    await waitFor(
      () => {
        attempts++;
        const status = canvas.getByTestId('status');
        console.log(`Attempt ${attempts}: Status is ${status.textContent}`);
        expect(status).toHaveTextContent('Complete');
      },
      { 
        timeout: 10000, 
        interval: 500 // Check every 500ms
      }
    );
    
    expect(attempts).toBeGreaterThan(1);
  }
};

Advanced Patterns

Retry Logic

import { within, waitFor } from "@storybook/testing-library";

export const RetryLogicExample = {
  play: async ({ canvasElement }) => {
    const canvas = within(canvasElement);
    
    // Custom retry logic with exponential backoff
    let retryCount = 0;
    await waitFor(
      () => {
        retryCount++;
        const element = canvas.queryByTestId('flaky-element');
        
        if (!element && retryCount < 3) {
          throw new Error(`Attempt ${retryCount} failed`);
        }
        
        expect(element).toBeInTheDocument();
      },
      { timeout: 5000, interval: 1000 }
    );
  }
};

Combination with findBy Queries

import { within, waitFor, userEvent } from "@storybook/testing-library";

export const FindByWaitForExample = {
  play: async ({ canvasElement }) => {
    const canvas = within(canvasElement);
    
    const loadButton = canvas.getByRole('button', { name: /load/i });
    await userEvent.click(loadButton);
    
    // findBy* queries include built-in waiting
    const result = await canvas.findByText(/loaded content/i);
    expect(result).toBeInTheDocument();
    
    // Use waitFor for more complex conditions
    await waitFor(() => {
      const items = canvas.getAllByTestId('item');
      expect(items).toHaveLength(5);
      expect(items.every(item => item.textContent?.includes('data'))).toBe(true);
    });
  }
};

Install with Tessl CLI

npx tessl i tessl/npm-storybook--testing-library

docs

async-utilities.md

configuration.md

events.md

index.md

queries.md

scoping.md

user-interactions.md

tile.json