Utilities for testing your stories inside play functions
—
Powerful assertion library based on @vitest/expect with testing-library DOM matchers and full chai compatibility. The expect function is instrumented for Storybook's addon-interactions debugging.
The main assertion function that supports jest-compatible API with additional testing-library matchers.
/**
* Create an assertion for the given value
* @param actual - The value to assert against
* @param message - Optional error message
* @returns Assertion object with chainable matchers
*/
function expect<T>(actual: T, message?: string): Assertion<T>;
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;
}Usage Examples:
import { expect, fn } from '@storybook/test';
// Basic assertions
expect(42).toBe(42);
expect('hello').toEqual('hello');
expect([1, 2, 3]).toContain(2);
// Mock function assertions
const mockFn = fn();
mockFn('arg1', 'arg2');
expect(mockFn).toHaveBeenCalledWith('arg1', 'arg2');
expect(mockFn).toHaveBeenCalledOnce();
// Async assertions
await expect(Promise.resolve('value')).resolves.toBe('value');
await expect(Promise.reject('error')).rejects.toBe('error');
// DOM assertions (with testing-library matchers)
const button = document.createElement('button');
button.textContent = 'Click me';
button.disabled = false;
expect(button).toBeInTheDocument();
expect(button).toHaveTextContent('Click me');
expect(button).toBeEnabled();The assertion interface provides all jest-compatible matchers plus testing-library DOM matchers.
interface Assertion<T> extends PromisifyObject<JestAssertion<T>>, TestingLibraryMatchers<ReturnType<ExpectStatic['stringContaining']>, Promise<void>> {
// Standard jest matchers
toBe(expected: T): Promise<void>;
toEqual(expected: T): Promise<void>;
toStrictEqual(expected: T): Promise<void>;
toContain(expected: any): Promise<void>;
toContainEqual(expected: any): Promise<void>;
toHaveLength(expected: number): Promise<void>;
toMatch(expected: string | RegExp): Promise<void>;
toMatchObject(expected: Record<string, any>): Promise<void>;
toThrow(expected?: string | RegExp | Error): Promise<void>;
toThrowError(expected?: string | RegExp | Error): Promise<void>;
// Mock-specific matchers
toHaveBeenCalled(): Promise<void>;
toHaveBeenCalledOnce(): Promise<void>;
toHaveBeenCalledTimes(expected: number): Promise<void>;
toHaveBeenCalledWith(...expected: any[]): Promise<void>;
toHaveBeenLastCalledWith(...expected: any[]): Promise<void>;
toHaveBeenNthCalledWith(nth: number, ...expected: any[]): Promise<void>;
toHaveReturned(): Promise<void>;
toHaveReturnedTimes(expected: number): Promise<void>;
toHaveReturnedWith(expected: any): Promise<void>;
toHaveLastReturnedWith(expected: any): Promise<void>;
toHaveNthReturnedWith(nth: number, expected: any): Promise<void>;
// Testing Library DOM matchers
toBeInTheDocument(): Promise<void>;
toBeVisible(): Promise<void>;
toBeEmptyDOMElement(): Promise<void>;
toBeDisabled(): Promise<void>;
toBeEnabled(): Promise<void>;
toBeInvalid(): Promise<void>;
toBeRequired(): Promise<void>;
toBeValid(): Promise<void>;
toBeChecked(): Promise<void>;
toBePartiallyChecked(): Promise<void>;
toHaveAccessibleDescription(expected?: string | RegExp): Promise<void>;
toHaveAccessibleName(expected?: string | RegExp): Promise<void>;
toHaveAttribute(attribute: string, expected?: string | RegExp): Promise<void>;
toHaveClass(...expected: string[]): Promise<void>;
toHaveFocus(): Promise<void>;
toHaveFormValues(expected: Record<string, any>): Promise<void>;
toHaveStyle(expected: string | Record<string, any>): Promise<void>;
toHaveTextContent(expected?: string | RegExp): Promise<void>;
toHaveValue(expected?: string | string[] | number): Promise<void>;
toHaveDisplayValue(expected: string | RegExp | string[] | RegExp[]): Promise<void>;
// Additional matchers
toSatisfy<E>(matcher: (value: E) => boolean, message?: string): Promise<void>;
// Async assertion helpers
resolves: Assertion<T>;
rejects: Assertion<T>;
// Negation
not: Assertion<T>;
}Create soft assertions that don't immediately fail the test but collect errors.
/**
* Create a soft assertion that collects errors instead of immediately failing
* @param actual - The value to assert against
* @param message - Optional error message
* @returns Assertion object for soft testing
*/
function soft<T>(actual: T, message?: string): Assertion<T>;Usage Example:
import { expect } from '@storybook/test';
// Soft assertions collect errors without stopping execution
expect.soft(1).toBe(2); // This won't throw immediately
expect.soft(2).toBe(3); // This won't throw immediately
expect.soft(3).toBe(3); // This passes
// All collected soft assertion errors will be reported at the endAdd custom matchers to the expect function.
/**
* Extend expect with custom matchers
* @param expects - Object containing custom matcher functions
*/
function extend(expects: MatchersObject): void;
interface MatchersObject {
[key: string]: (this: MatcherState, actual: any, ...expected: any[]) => MatcherResult;
}
interface MatcherResult {
message: () => string;
pass: boolean;
}
interface MatcherState {
isNot: boolean;
promise: string;
assertEquals: (actual: any, expected: any, message?: string) => void;
assertType: (value: any, type: string, message?: string) => void;
}Usage Example:
import { expect } from '@storybook/test';
expect.extend({
toBeWithinRange(received, floor, ceiling) {
const pass = received >= floor && received <= ceiling;
if (pass) {
return {
message: () => `expected ${received} not to be within range ${floor} - ${ceiling}`,
pass: true,
};
} else {
return {
message: () => `expected ${received} to be within range ${floor} - ${ceiling}`,
pass: false,
};
}
},
});
// Usage
expect(100).toBeWithinRange(90, 110);Create asymmetric matchers for flexible assertions.
/**
* Match any value of the given constructor type
* @param constructor - Constructor function to match against
* @returns Asymmetric matcher
*/
function any(constructor: unknown): any;
/**
* Match any truthy value
* @returns Asymmetric matcher
*/
function anything(): any;Usage Examples:
import { expect } from '@storybook/test';
// Asymmetric matchers for flexible matching
expect({ name: 'John', age: 30 }).toEqual({
name: expect.any(String),
age: expect.any(Number),
});
expect(['apple', 'banana']).toEqual([
expect.anything(),
expect.anything(),
]);Validate the number of assertions executed during a test.
/**
* Expect exactly N assertions to be called during the test
* @param expected - Expected number of assertions
*/
function assertions(expected: number): Promise<void>;
/**
* Expect at least one assertion to be called during the test
*/
function hasAssertions(): Promise<void>;Usage Examples:
import { expect } from '@storybook/test';
export const TestStory = {
play: async () => {
expect.assertions(2); // Expect exactly 2 assertions
expect(1).toBe(1);
expect(2).toBe(2);
// Test will fail if not exactly 2 assertions are made
},
};
export const AnotherStory = {
play: async () => {
expect.hasAssertions(); // Expect at least one assertion
const condition = Math.random() > 0.5;
if (condition) {
expect(true).toBe(true);
}
// Test will fail if no assertions are made
},
};Mark code paths that should never be reached.
/**
* Mark a code path as unreachable - will always fail if executed
* @param message - Optional error message
* @throws Always throws an error
*/
function unreachable(message?: string): Promise<never>;Usage Example:
import { expect } from '@storybook/test';
export const TestStory = {
play: async () => {
const value = 'valid';
switch (value) {
case 'valid':
expect(true).toBe(true);
break;
default:
expect.unreachable('Should never reach default case');
}
},
};Install with Tessl CLI
npx tessl i tessl/npm-storybook--test