Test component interactions with Storybook
Complete tutorial on how to simulate and verify user behavior
Components fetch data, respond to user interactions, and manage app state. To verify this functional behavior, developers rely on automated tests. But most testing tools are Node and JSDOM based. That means youโre forced to debug visual UIs in a textual command line.
At Storybook, weโre improving component testing by using the browser to run tests. Over the past six months, we introduced several featuresโplay function, test-runner, assertion libraryโto make this a reality. This article walks through the entire Storybook interaction testing workflow.
- ๐ Write tests in a stories file
- ๐ Debug tests in the browser using the interactions panel
- ๐ Reproduce error states via URLs
- ๐ค Automate tests using continuous integration
How does component testing in Storybook work?
Testing interactions is a widespread pattern for verifying user behavior. You provide mock data to set up a test scenario, simulate user interactions using Testing Library, and check the resultant DOM structure.
In Storybook, this familiar workflow happens in your browser. That makes it easier to debug failures because you're running tests in the same environment as you develop componentsโthe browser.
Start by writing a story to set up the component's initial state. Then simulate user behavior such as clicks and form entries using the play function. Finally, use the Storybook test-runner to check whether the UI and component state update correctly. Automate testing via the command line or your CI server.
Tutorial
To demonstrate the testing workflow Iโll use the Taskbox appโa task management app similar to Asana. On its InboxScreen, the user can click on the star icon to pin a task. Or click on the checkbox to archive it. Letโs write tests to ensure that the UI is responding to those interactions correctly.
Grab the code to follow along:
# Clone the template
npx degit chromaui/ui-testing-guide-code#dc9bacae842f5250aad544b139dc9d63a48bbd1e taskbox
cd taskbox
# Install dependencies
yarn
Setup the test-runner
Weโll start by installing the test-runner and related packages (note, it requires Storybook 6.4 or above).
yarn add -D @storybook/testing-library @storybook/jest @storybook/addon-interactions jest @storybook/test-runner
Update your Storybook configuration (in .storybook/main.js
) to include the interactions addon and enable playback controls for debugging.
// .storybook/main.js
module.exports = {
stories: [],
addons: [
// Other Storybook addons
'@storybook/addon-interactions', // ๐ addon is registered here
],
features: {
interactionsDebugger: true, // ๐ enable playback controls
},
};
Then add a test task to your projectโs package.json
:
{
"scripts": {
"test-storybook": "test-storybook"
}
}
Lastly, start up your Storybook (the test-runner runs against a running Storybook instance):
yarn storybook
Write stories to set up a test case
The first step of writing a test is to set up a scenario by providing props or mock data to a component. That's exactly what a story is, so let's write one for the InboxScreen component.
InboxScreen fetches data via /tasks
API request, which we'll mock using the MSW addon.
// src/InboxScreen.stories.js;
import React from 'react';
import { rest } from 'msw';
import { within, userEvent, findByRole } from '@storybook/testing-library';
import { expect } from '@storybook/jest';
import { InboxScreen } from './InboxScreen';
import { Default as TaskListDefault } from './components/TaskList.stories';
export default {
component: InboxScreen,
title: 'InboxScreen',
};
const Template = (args) => <InboxScreen {...args} />;
export const Default = Template.bind({});
Default.parameters = {
msw: {
handlers: [
rest.get('/tasks', (req, res, ctx) => {
return res(ctx.json(TaskListDefault.args));
}),
],
},
};
Write an interaction test using the play function
Testing Library offers a convenient API for simulating user interactionsโclick, drag, tap, type, etc. Whereas Jest provides assertion utilities. We'll use Storybook-instrumented versions of these two tools to write the test. Therefore, you get a familiar developer-friendly syntax to interact with the DOM, but with extra telemetry to help with debugging.
The test itself will be housed inside a play function. This snippet of code gets attached to a story and runs after the story is rendered.
Let's add in our first interaction test to verify that the user can pin a task:
export const PinTask = Template.bind({});
PinTask.parameters = { ...Default.parameters };
PinTask.play = async ({ canvasElement }) => {
const canvas = within(canvasElement);
const getTask = (name) => canvas.findByRole('listitem', { name });
// Find the task to pin
const itemToPin = await getTask('Export logo');
// Find the pin button
const pinButton = await findByRole(itemToPin, 'button', { name: 'pin' });
// Click the pin button
await userEvent.click(pinButton);
// Check that the pin button is now a unpin button
const unpinButton = within(itemToPin).getByRole('button', { name: 'unpin' });
await expect(unpinButton).toBeInTheDocument();
};
Each play function receives the Canvas elementโthe top-level container of the story. You can scope your queries to just within this element, making it easier to find DOM nodes.
We're looking for the "Export logo" task in our case. Then find the pin button within it and click it. Finally, we check to see if the button has updated to the unpinned state.
When Storybook finishes rendering the story, it executes the steps defined within the play function, interacting with the component and pinning a taskโsimilar to how a user would do it. If you check your interactions panel, you'll see the step-by-step flow. It also offers a handy set of UI controls to pause, resume, rewind, and step through each interaction.
Execute tests with test-runner
Now that we have our first test down, we'll also add tests for the archive, edit and delete task functionality.
export const ArchiveTask = Template.bind({});
ArchiveTask.parameters = { ...Default.parameters };
ArchiveTask.play = async ({ canvasElement }) => {
const canvas = within(canvasElement);
const getTask = (name) => canvas.findByRole('listitem', { name });
const itemToArchive = await getTask('QA dropdown');
const archiveCheckbox = await findByRole(itemToArchive, 'checkbox');
await userEvent.click(archiveCheckbox);
await expect(archiveCheckbox.checked).toBe(true);
};
export const EditTask = Template.bind({});
EditTask.parameters = { ...Default.parameters };
EditTask.play = async ({ canvasElement }) => {
const canvas = within(canvasElement);
const getTask = (name) => canvas.findByRole('listitem', { name });
const itemToEdit = await getTask('Fix bug in input error state');
const taskInput = await findByRole(itemToEdit, 'textbox');
userEvent.type(taskInput, ' and disabled state');
await expect(taskInput.value).toBe(
'Fix bug in input error state and disabled state'
);
};
export const DeleteTask = Template.bind({});
DeleteTask.parameters = { ...Default.parameters };
DeleteTask.play = async ({ canvasElement }) => {
const canvas = within(canvasElement);
const getTask = (name) => canvas.findByRole('listitem', { name });
const itemToDelete = await getTask('Build a date picker');
const deleteButton = await findByRole(itemToDelete, 'button', {
name: 'delete',
});
await userEvent.click(deleteButton);
expect(canvas.getAllByRole('listitem').length).toBe(5);
};
You should now see stories for these scenarios. Storybook only runs the interaction test when youโre viewing a story. Therefore, you'd have to go through each story to run all your checks.
It's unrealistic to manually review the entire Storybook whenever you make a change. Storybook test-runner automates that process. It's a standalone utilityโpowered by Playwrightโthat runs all your interactions tests and catches broken stories.
Start the test-runner (in a separate terminal window): yarn test-storybook --watch
. It verifies whether all stories rendered without any errors and that all assertions are passed.
If a test fails, you get a link that opens up the failing story in the browser.
You've got the local development workflow sorted. Storybook and test-runner run side-by-side, allowing you to build components in isolation and test their underlying logic in one go.
Automate Storybook interaction tests
Once you're ready to merge your code, you'll want to automatically run all your checks using a Continuous Integration (CI) server. You have two options for integrating Storybook interaction tests into your test automation pipeline: by using the test-runner in CI or combining it with visual tests using Chromatic.
Run test-runner in CI
You can build and serve the Storybook on your CI server and execute the test-runner against it. Here's a recipe that uses concurrently, http-server and wait-on libraries.
# .github/workflows/ui-tests.yml
name: 'Storybook Tests'
on: push
jobs:
test:
timeout-minutes: 60
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: actions/setup-node@v2
with:
node-version: '14.x'
- name: Install dependencies
run: yarn
- name: Install Playwright
run: npx playwright install --with-deps
- name: Build Storybook
run: yarn build-storybook --quiet
- name: Serve Storybook and run tests
run: |
npx concurrently -k -s first -n "SB,TEST" -c "magenta,blue" \
"npx http-server storybook-static --port 6006 --silent" \
"npx wait-on tcp:6006 && yarn test-storybook"
You can also run tests against a published Storybook. For more on that and other CI configuration options, refer to the test-runner documentation.
Combine interaction and visual tests using Chromatic
Catching unintentional UI changes has always been a challenge. A line of leaky CSS can break multiple pages. That's why leading teams at Auth0, Twilio, Adobe, and Peloton rely on visual testing. Chromatic is a cloud-based visual testing tool purpose-built for Storybook. It can also execute your interaction tests.
Chromatic works by capturing an image snapshot of every storyโas it appears in the browser. Then when you open a pull request, it compares it to the previously accepted baseline and presents you with a diff.
Chromatic supports Storybook interaction tests out-of-the-box. It waits for the interaction test to run before capturing the snapshot. This way, you can verify both the visual appearance and the underlying logic for a component in one go. Any test failures get reported via the Chromatic UI.
Here's a sample workflow for running Chromatic using Github Actions. For other CI services, refer to the Chromatic docs.
# .github/workflows/ui-tests.yml
name: 'Chromatic'
on: push
jobs:
chromatic-deployment:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
with:
fetch-depth: 0 # Required to retrieve git history
- name: Install dependencies
run: yarn
- name: Publish to Chromatic
uses: chromaui/action@v1
with:
# Grab this from the Chromatic manage page
projectToken: ${{ secrets.CHROMATIC_PROJECT_TOKEN }}
Test components in the browser with Storybook
Components aren't static. Users can interact with the UI and trigger state updates. You have to write tests that simulate user behavior to verify this behavior.
Storybook interaction tests is our vision for what component testing should be: fast, intuitive, and integrated with tools you already use. It combines the intuitive debugging environment of a live browser with the performance and scriptability of headless browsers.
If you've been coding along, your repository should look like this: GitHub repository.
Want more? Here are some additional helpful resources:
- Storybook interaction tests docs
- Test runner docs
- Interaction panel docs
- UI Testing Handbook for a deep dive into testing UIs