Utilities for testing your stories inside play functions
npx @tessl/cli install tessl/npm-storybook--test@8.6.0The @storybook/test package provides instrumented testing utilities specifically designed for Storybook stories' play functions. It exports enhanced versions of popular testing libraries including @vitest/spy, @vitest/expect, @testing-library/dom, and @testing-library/user-event, with instrumentation that enables debugging capabilities in the addon-interactions panel.
npm install -D @storybook/testimport { expect, fn, userEvent, within } from '@storybook/test';For CommonJS:
const { expect, fn, userEvent, within } = require('@storybook/test');import { expect, fn, userEvent, within } from '@storybook/test';
import { Button } from './Button';
export default {
component: Button,
args: {
onClick: fn(),
},
};
export const Demo = {
play: async ({ args, canvasElement }) => {
const canvas = within(canvasElement);
await userEvent.click(canvas.getByRole('button'));
await expect(args.onClick).toHaveBeenCalled();
},
};Storybook Test is built around several key components:
Powerful assertion library based on @vitest/expect with testing-library DOM matchers and full chai compatibility.
interface Expect extends AsymmetricMatchersContaining {
<T>(actual: T, message?: string): Assertion<T>;
unreachable(message?: string): Promise<never>;
soft<T>(actual: T, message?: string): Assertion<T>;
extend(expects: MatchersObject): void;
assertions(expected: number): Promise<void>;
hasAssertions(): Promise<void>;
anything(): any;
any(constructor: unknown): any;
getState(): MatcherState;
setState(state: Partial<MatcherState>): void;
not: AsymmetricMatchersContaining;
}Mock functions and spies with reactive instrumentation for Storybook integration. Includes automatic cleanup and enhanced debugging.
function fn<T extends Procedure = Procedure>(implementation?: T): Mock<T>;
function spyOn<T, K extends keyof T>(
object: T,
method: K
): MockInstance;
function clearAllMocks(): void;
function resetAllMocks(): void;
function restoreAllMocks(): void;
function onMockCall(callback: Listener): () => void;Complete DOM testing utilities including queries, user interactions, and async utilities. All functions are instrumented for Storybook debugging.
// Query functions (get, find, query variants)
function getByRole(container: HTMLElement, role: string, options?: ByRoleOptions): HTMLElement;
function findByText(container: HTMLElement, text: string, options?: SelectorMatcherOptions): Promise<HTMLElement>;
function queryByTestId(container: HTMLElement, testId: string, options?: MatcherOptions): HTMLElement | null;
// Scoping utility
function within(element: HTMLElement): BoundFunctions<typeof queries>;
// User interactions
interface UserEvent {
click(element: Element, options?: ClickOptions): Promise<void>;
type(element: Element, text: string, options?: TypeOptions): Promise<void>;
clear(element: Element): Promise<void>;
selectOptions(element: Element, values: string | string[]): Promise<void>;
upload(element: Element, file: File | File[]): Promise<void>;
}
// Async utilities
function waitFor<T>(callback: () => T | Promise<T>, options?: WaitForOptions): Promise<T>;
function waitForElementToBeRemoved(callback: () => Element | Element[]): Promise<void>;type Procedure = (...args: any[]) => any;
type MockV2<T extends Procedure> = MockInstance<Parameters<T>, ReturnType<T>> & T;
type Mock<T extends Procedure | any[] = any[], R = any> = T extends Procedure
? MockV2<T>
: T extends any[]
? MockV2<(...args: T) => R>
: never;
interface MockInstance<TArgs extends any[] = any[], TReturns = any> {
getMockName(): string;
mockName(name: string): this;
mockClear(): this;
mockReset(): this;
mockRestore(): void;
mockImplementation(fn?: (...args: TArgs) => TReturns): this;
mockImplementationOnce(fn: (...args: TArgs) => TReturns): this;
mockReturnValue(value: TReturns): this;
mockReturnValueOnce(value: TReturns): this;
mockResolvedValue(value: Awaited<TReturns>): this;
mockResolvedValueOnce(value: Awaited<TReturns>): this;
mockRejectedValue(value: any): this;
mockRejectedValueOnce(value: any): this;
}
// Jest-compatible assertion types
interface JestAssertion<T = any> {
toBe(expected: T): void;
toEqual(expected: T): void;
toStrictEqual(expected: T): void;
toContain(expected: any): void;
toHaveLength(expected: number): void;
// ... other Jest matchers
}
// Testing Library DOM matchers
interface TestingLibraryMatchers<R, T> {
toBeInTheDocument(): R;
toBeVisible(): R;
toBeEmpty(): R;
toBeDisabled(): R;
toBeEnabled(): R;
toBeRequired(): R;
toBeValid(): R;
toBeInvalid(): R;
toHaveAttribute(attr: string, value?: string): R;
toHaveClass(...classNames: string[]): R;
toHaveStyle(css: string | Record<string, any>): R;
toHaveTextContent(text: string | RegExp): R;
toHaveValue(value: string | string[] | number): R;
toHaveDisplayValue(value: string | RegExp | Array<string | RegExp>): R;
toBeChecked(): R;
toHaveFormValues(expectedValues: Record<string, any>): R;
toHaveFocus(): R;
toHaveAccessibleName(expectedAccessibleName?: string | RegExp): R;
toHaveAccessibleDescription(expectedAccessibleDescription?: string | RegExp): R;
toHaveErrorMessage(expectedErrorMessage?: string | RegExp): R;
}
// Expect static type
interface ExpectStatic {
<T>(actual: T): JestAssertion<T>;
stringContaining(expected: string): any;
arrayContaining<T = any>(expected: readonly T[]): any;
objectContaining(expected: Record<string, any>): any;
any(constructor: any): any;
anything(): any;
}
// Utility types
type Promisify<Fn> = Fn extends (...args: infer A) => infer R
? (...args: A) => R extends Promise<any> ? R : Promise<R>
: Fn;
type PromisifyObject<O> = { [K in keyof O]: Promisify<O[K]> };
interface Assertion<T> extends PromisifyObject<JestAssertion<T>>, TestingLibraryMatchers<ReturnType<ExpectStatic['stringContaining']>, Promise<void>> {
toHaveBeenCalledOnce(): Promise<void>;
toSatisfy<E>(matcher: (value: E) => boolean, message?: string): Promise<void>;
resolves: Assertion<T>;
rejects: Assertion<T>;
not: Assertion<T>;
}
type BoundFunctions<T> = {
[K in keyof T]: T[K] extends (...args: any[]) => any
? (...args: Parameters<T[K]>) => ReturnType<T[K]>
: T[K];
};
// DOM Testing option types
interface MatcherOptions {
exact?: boolean;
normalizer?: (text: string) => string;
}
interface SelectorMatcherOptions extends MatcherOptions {
selector?: string;
}
interface ByRoleOptions extends MatcherOptions {
checked?: boolean;
selected?: boolean;
expanded?: boolean;
pressed?: boolean;
level?: number;
name?: string | RegExp;
description?: string | RegExp;
}
interface WaitForOptions {
timeout?: number;
interval?: number;
onTimeout?: (error: Error) => Error;
mutationObserverOptions?: MutationObserverInit;
}
interface ClickOptions {
altKey?: boolean;
button?: number;
buttons?: number;
clientX?: number;
clientY?: number;
ctrlKey?: boolean;
detail?: number;
metaKey?: boolean;
relatedTarget?: Element | null;
screenX?: number;
screenY?: number;
shiftKey?: boolean;
}
interface TypeOptions {
delay?: number;
skipClick?: boolean;
skipAutoClose?: boolean;
initialSelectionStart?: number;
initialSelectionEnd?: number;
}type MaybeMocked<T> = T & {
[K in keyof T]: T[K] extends (...args: any[]) => any
? MockInstance<Parameters<T[K]>, ReturnType<T[K]>>
: T[K];
};
type MaybeMockedDeep<T> = T & {
[K in keyof T]: T[K] extends (...args: any[]) => any
? MockInstance<Parameters<T[K]>, ReturnType<T[K]>>
: MaybeMockedDeep<T[K]>;
};
type MaybePartiallyMocked<T> = {
[K in keyof T]?: T[K] extends (...args: any[]) => any
? MockInstance<Parameters<T[K]>, ReturnType<T[K]>>
: T[K];
};
type MaybePartiallyMockedDeep<T> = {
[K in keyof T]?: T[K] extends (...args: any[]) => any
? MockInstance<Parameters<T[K]>, ReturnType<T[K]>>
: MaybePartiallyMockedDeep<T[K]>;
};
type Promisify<Fn> = Fn extends (...args: infer A) => infer R
? (...args: A) => R extends Promise<any> ? R : Promise<R>
: Fn;
type PromisifyObject<O> = { [K in keyof O]: Promisify<O[K]> };