Instrumented version of Testing Library for Storybook Interactions addon
—
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.
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;
}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>;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);
}
};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
);
}
};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();
}
};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();
});
}
};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();
});
}
};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);
}
};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
);
}
};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();
});
}
};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);
});
}
};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);
}
};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 }
);
}
};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