Interaction tests
Interaction tests enable you to verify how a component responds to user behaviors like click, type, keyboard, and hover. It’s powered by Storybook’s play
function.
Interaction tests require Storybook 6.5.10+. Check which version you have in package.json or open the “About Storybook” page in your Storybook.
How to write interaction tests
Add a play
function to your component’s story to enable interaction tests. For example, if you want to validate a component’s behavior write the following story:
// RangeSlider.stories.ts|tsx
// Adjust this import to match your framework (e.g., nextjs, vue3-vite)
import type { Meta, StoryObj } from "@storybook/your-framework";
/*
* Replace the @storybook/test package with the following if you are using a version of Storybook earlier than 8.0:
* import { within, userEvent } from "@storybook/testing-library";
* import { expect } from "@storybook/jest";
*/
import { within, userEvent, expect } from "@storybook/test";
import { RangeSlider } from "./RangeSlider";
const meta: Meta<typeof RangeSlider> = {
component: RangeSlider,
title: "Library/Charts/RangeSlider",
};
export default meta;
type Story = StoryObj<typeof RangeSlider>;
export const InputRange: Story = {
play: async ({ canvasElement }) => {
// Assigns canvas to the component root element
const canvas = within(canvasElement);
// 🔢 Type into input field
await userEvent.type(canvas.getByTestId("input-max-range"), "15");
// ✅ Assert that component is responding to user behavior
const availableOptions = await canvas.findAllByTestId("highlighted-bar");
await expect(availableOptions.length).toBe(15);
},
};
Chromatic waits for interactions to pass before capturing a snapshot.
Confirm interaction tests are working
Interaction tests run behind the scenes without you having to configure anything. To verify that they are working in Chromatic, publish your Storybook either via CLI or CI. You can confirm that they’re running with the “Interaction” label in the Build page’s Tests section.
Composing stories with the play()
function
Similarly to args
, play()
functions can be composed to reuse and reduce the amount of code being written. This can be helpful for instances where you are looking to snapshot multiple states of a longer interaction, or if you have interactions that can be reused to further compose other states of your component.
There is an important caveat to remember when invoking a play()
function from another story: it is necessary to pass the full context as an argument to the play()
function. The below code uses the correct code for this rule, with canvasContext
being used as context.
// MyComponent.stories.jsx|tsx
import { MyComponent } from "./MyComponent";
import { userEvent, within, waitFor, screen } from "@storybook/testing-library";
import { expect } from "@storybook/jest";
export default {
component: MyComponent,
title: "My Component",
};
export const FirstStory = {
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
const dropdownButton = canvas.getByRole("button");
await userEvent.click(dropdownButton);
},
};
export const SecondStory = {
play: async (canvasContext) => {
const canvas = within(canvasContext.canvasElement);
// 👇 Pass the full context as an argument to the play function:
await FirstStory.play(canvasContext);
const findText = canvas.getByText("some text");
await userEvent.hover(findText);
await waitFor(() => expect(findText.focus()));
},
};
export const ThirdStory = {
play: async (canvasContext) => {
// 👇 SecondStory.play will execute the play functions from FirstStory.play since this is part of the SecondStory.play function:
await SecondStory.play(canvasContext);
const searchbox = screen.getByRole("searchbox", { label: "Search" });
await userEvent.type(searchbox, "text for searchbox");
},
};
Additionally, you can stack multiple play()
functions within a story. Below is a quick pseudocode example.
// MyComponent.stories.jsx|tsx
import { MyComponent } from "./MyComponent";
import { userEvent, within, waitFor, screen } from "@storybook/testing-library";
import { expect } from "@storybook/jest";
export default {
component: MyComponent,
title: "My Component",
};
export const FirstStory = {
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
await userEvent.type(canvas.getByTestId("an-element"), "some text");
},
};
export const SecondStory = {
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
await userEvent.type(
canvas.getByTestId("another-element"),
"some more text",
);
},
};
export const CombinedStories = {
play: async (canvasContext) => {
const canvas = within(canvasContext.canvasElement);
// 👇 This executes FirstStory.play and SecondStory.play before executing the story's play function:
await FirstStory.play(canvasContext);
await SecondStory.play(canvasContext);
await userEvent.type(canvas.getByTestId("another-one"), "even more text");
},
};
Debug test failures
Chromatic notifies you when an interaction errors or an assertion fails. We designate these as critical failures that need immediate attention. You won’t be able to pass the build until the test is fixed.
To find out which steps failed in your interaction test, click on the change to see a snapshot of the state where the error occurred. You’ll see a detailed log and browser environment metadata to help with reproductions.
Reproduce test failures with a URL
Go to your published Storybook to reproduce the exact state of your story when the test failed. Click the “View Storybook” button on the test page to open the failed story with the error message visible. Share the link with teammates to get a second opinion.
PR check for interaction tests
Interaction tests are reported in the UI Tests pull request check. When a test fails, you’ll see a “Failed tests” status message prompting you to fix the test before moving on.
Frequently asked questions
My interactions aren't getting snapshotted consistently with external web fonts?
Interactions run as soon as the DOM loads. But external resources like web fonts can load before or after the interaction runs depending on network latency. This can cause dialogs, tooltips, and menus to change position.
We recommend preloading fonts to ensure they’re available when the DOM renders. If preloading is not possible, try adding a delay before running interactions.
What happens when an interaction test fails?
When interaction tests fail, the story will be badged with “Failed test.” You will not be able to “pass” a build that has failed tests. Fix interaction tests in Storybook and run the build again.
Does Chromatic capture snapshots of every step of an interaction test?
No, Chromatic waits for the entire play function to execute and captures a snapshot only at the end.
If you need a snapshot of a specific step, we recommend breaking your story into multiple stories and using play function composition.