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

user-interactions.mddocs/

User Interactions

Instrumented user-event functionality for simulating realistic user interactions with full keyboard, mouse, and input device support. All user interactions are captured by Storybook's instrumentation for display in the Interactions addon.

Capabilities

User Event Object

The main interface for simulating user interactions. All methods are asynchronous and return Promises.

/**
 * Instrumented user-event object for simulating realistic user interactions
 * All methods are wrapped with Storybook instrumentation for interaction tracking
 */
interface UserEvent {
  /**
   * Click on an element
   * @param element - Element to click
   * @param options - Click configuration options
   */
  click(element: Element, options?: ClickOptions): Promise<void>;
  
  /**
   * Double-click on an element
   * @param element - Element to double-click
   * @param options - Click configuration options
   */
  dblClick(element: Element, options?: ClickOptions): Promise<void>;
  
  /**
   * Type text into an element
   * @param element - Input element to type into
   * @param text - Text to type
   * @param options - Typing configuration options
   */
  type(element: Element, text: string, options?: TypeOptions): Promise<void>;
  
  /**
   * Clear the content of an input element
   * @param element - Input element to clear
   */
  clear(element: Element): Promise<void>;
  
  /**
   * Select options in a select element
   * @param element - Select element
   * @param values - Option values to select (string or array)
   * @param options - Selection options
   */
  selectOptions(element: Element, values: string | string[], options?: SelectOptions): Promise<void>;
  
  /**
   * Deselect options in a select element
   * @param element - Select element
   * @param values - Option values to deselect (string or array)
   * @param options - Selection options
   */
  deselectOptions(element: Element, values: string | string[], options?: SelectOptions): Promise<void>;
  
  /**
   * Upload files to a file input
   * @param element - File input element
   * @param file - File or array of files to upload
   */
  upload(element: Element, file: File | File[]): Promise<void>;
  
  /**
   * Tab to the next focusable element
   * @param options - Tab options
   */
  tab(options?: TabOptions): Promise<void>;
  
  /**
   * Hover over an element
   * @param element - Element to hover over
   * @param options - Hover options
   */
  hover(element: Element, options?: HoverOptions): Promise<void>;
  
  /**
   * Stop hovering over an element
   * @param element - Element to unhover
   * @param options - Hover options
   */
  unhover(element: Element, options?: HoverOptions): Promise<void>;
  
  /**
   * Press and hold keyboard keys
   * @param keys - Key or combination of keys to press
   * @param options - Keyboard options
   */
  keyboard(keys: string, options?: KeyboardOptions): Promise<void>;
  
  /**
   * Copy selected text to clipboard
   */
  copy(): Promise<void>;
  
  /**
   * Cut selected text to clipboard
   */
  cut(): Promise<void>;
  
  /**
   * Paste text from clipboard
   */
  paste(): Promise<void>;
}

declare const userEvent: UserEvent;

Click Interactions

/**
 * Options for click interactions
 */
interface ClickOptions {
  /** Mouse button to use (0=left, 1=middle, 2=right) */
  button?: number;
  /** Control key modifier */
  ctrlKey?: boolean;
  /** Shift key modifier */
  shiftKey?: boolean;
  /** Alt key modifier */
  altKey?: boolean;
  /** Meta key modifier (Cmd on Mac, Windows key on PC) */
  metaKey?: boolean;
  /** Skip default behavior (e.g., don't focus element) */
  skipDefaultPrevented?: boolean;
}

Usage Examples:

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

export const ClickInteractions = {
  play: async ({ canvasElement }) => {
    const canvas = within(canvasElement);
    
    // Basic click
    const button = canvas.getByRole('button', { name: /submit/i });
    await userEvent.click(button);
    
    // Click with modifiers
    const link = canvas.getByRole('link', { name: /external/i });
    await userEvent.click(link, { ctrlKey: true }); // Ctrl+click
    
    // Right-click (context menu)
    await userEvent.click(button, { button: 2 });
    
    // Double-click
    const item = canvas.getByTestId('double-click-item');
    await userEvent.dblClick(item);
  }
};

Typing Interactions

/**
 * Options for typing interactions
 */
interface TypeOptions {
  /** Delay between keystrokes in milliseconds */
  delay?: number;
  /** Skip clicking the element before typing */
  skipClick?: boolean;
  /** Skip auto-closing of characters like quotes and brackets */
  skipAutoClose?: boolean;
  /** Initial selection start position */
  initialSelectionStart?: number;
  /** Initial selection end position */
  initialSelectionEnd?: number;
}

Usage Examples:

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

export const TypingInteractions = {
  play: async ({ canvasElement }) => {
    const canvas = within(canvasElement);
    
    // Basic typing
    const input = canvas.getByLabelText(/username/i);
    await userEvent.type(input, 'john.doe@example.com');
    
    // Typing with delay
    const slowInput = canvas.getByLabelText(/description/i);
    await userEvent.type(slowInput, 'Slow typing...', { delay: 100 });
    
    // Clear and type
    const existingInput = canvas.getByDisplayValue('existing text');
    await userEvent.clear(existingInput);
    await userEvent.type(existingInput, 'new text');
    
    // Type special characters
    const passwordInput = canvas.getByLabelText(/password/i);
    await userEvent.type(passwordInput, 'P@ssw0rd!23');
  }
};

Selection Interactions

/**
 * Options for select interactions
 */
interface SelectOptions {
  /** Skip clicking the element before selecting */
  skipClick?: boolean;
}

Usage Examples:

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

export const SelectionInteractions = {
  play: async ({ canvasElement }) => {
    const canvas = within(canvasElement);
    
    // Select single option
    const countrySelect = canvas.getByLabelText(/country/i);
    await userEvent.selectOptions(countrySelect, 'USA');
    
    // Select multiple options
    const skillsSelect = canvas.getByLabelText(/skills/i);
    await userEvent.selectOptions(skillsSelect, ['JavaScript', 'TypeScript', 'React']);
    
    // Deselect options
    await userEvent.deselectOptions(skillsSelect, 'JavaScript');
    
    // Select by value
    await userEvent.selectOptions(countrySelect, { target: { value: 'canada' } });
  }
};

File Upload Interactions

Usage Examples:

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

export const FileUploadInteractions = {
  play: async ({ canvasElement }) => {
    const canvas = within(canvasElement);
    
    // Upload single file
    const fileInput = canvas.getByLabelText(/upload file/i);
    const file = new File(['file content'], 'test.txt', { type: 'text/plain' });
    await userEvent.upload(fileInput, file);
    
    // Upload multiple files
    const multiFileInput = canvas.getByLabelText(/upload multiple/i);
    const files = [
      new File(['image1'], 'image1.png', { type: 'image/png' }),
      new File(['image2'], 'image2.jpg', { type: 'image/jpeg' })
    ];
    await userEvent.upload(multiFileInput, files);
  }
};

Keyboard Interactions

/**
 * Options for keyboard interactions
 */
interface KeyboardOptions {
  /** Skip auto-releasing pressed keys */
  skipAutoClose?: boolean;
}

/**
 * Options for tab navigation
 */
interface TabOptions {
  /** Tab backwards (Shift+Tab) */
  shift?: boolean;
}

Usage Examples:

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

export const KeyboardInteractions = {
  play: async ({ canvasElement }) => {
    const canvas = within(canvasElement);
    
    // Tab navigation
    await userEvent.tab(); // Tab forward
    await userEvent.tab({ shift: true }); // Tab backward
    
    // Keyboard shortcuts
    await userEvent.keyboard('{ctrl}a'); // Select all
    await userEvent.keyboard('{ctrl}c'); // Copy
    await userEvent.keyboard('{ctrl}v'); // Paste
    
    // Special keys
    await userEvent.keyboard('{Enter}'); // Enter key
    await userEvent.keyboard('{Escape}'); // Escape key
    await userEvent.keyboard('{ArrowDown}'); // Arrow down
    
    // Key combinations
    await userEvent.keyboard('{ctrl}{shift}k'); // Ctrl+Shift+K
    
    // Use convenience methods
    await userEvent.copy();
    await userEvent.cut();
    await userEvent.paste();
  }
};

Hover Interactions

/**
 * Options for hover interactions
 */
interface HoverOptions {
  /** Skip pointer events */
  skipPointerEvents?: boolean;
}

Usage Examples:

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

export const HoverInteractions = {
  play: async ({ canvasElement }) => {
    const canvas = within(canvasElement);
    
    // Hover over element
    const tooltip = canvas.getByTestId('tooltip-trigger');
    await userEvent.hover(tooltip);
    
    // Wait for tooltip to appear
    await waitFor(() => {
      expect(canvas.getByRole('tooltip')).toBeVisible();
    });
    
    // Stop hovering
    await userEvent.unhover(tooltip);
    
    // Tooltip should disappear
    await waitFor(() => {
      expect(canvas.queryByRole('tooltip')).not.toBeInTheDocument();
    });
  }
};

Advanced Usage Patterns

Realistic User Flows

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

export const CompleteUserFlow = {
  play: async ({ canvasElement }) => {
    const canvas = within(canvasElement);
    
    // Fill out a form realistically
    const emailInput = canvas.getByLabelText(/email/i);
    const passwordInput = canvas.getByLabelText(/password/i);
    const submitButton = canvas.getByRole('button', { name: /sign in/i });
    
    // Focus and type email
    await userEvent.click(emailInput);
    await userEvent.type(emailInput, 'user@example.com');
    
    // Tab to password field
    await userEvent.tab();
    await userEvent.type(passwordInput, 'securepassword');
    
    // Submit form
    await userEvent.click(submitButton);
    
    // Wait for success message
    await waitFor(() => {
      expect(canvas.getByText(/welcome back/i)).toBeInTheDocument();
    });
  }
};

Error Handling

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

export const ErrorHandling = {
  play: async ({ canvasElement }) => {
    const canvas = within(canvasElement);
    
    try {
      // Attempt to interact with element that might not exist
      const button = canvas.queryByRole('button', { name: /optional/i });
      if (button) {
        await userEvent.click(button);
      }
    } catch (error) {
      console.error('User interaction failed:', error);
    }
  }
};

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