Back to blog

Test component interactions with Storybook

Complete tutorial on how to simulate and verify user behavior

loading
Varun Vachhar
โ€” @winkerVSbecks
Last updated:

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:

Join the Storybook mailing list

Get the latest news, updates and releases

6,616 developers and counting

Weโ€™re hiring!

Join the team behind Storybook and Chromatic. Build tools that are used in production by 100s of thousands of developers. Remote-first.

View jobs

Popular posts

Storybook Lazy Compilation for Webpack

Lightning fast local development experience for large Storybooks
loading
Tom Coleman

Figma plugin for Storybook

Integrate design and code, side by side
loading
Dominic Nguyen

Figma plugin beta

Connect stories to design components
loading
Dominic Nguyen
Join the community
6,616 developers and counting
WhyWhy StorybookComponent-driven UI
DocsGuidesTutorialsChangelogTelemetry
CommunityAddonsGet involvedBlog
ShowcaseExploreProjectsComponent glossary
Open source software
Storybook

Maintained by
Chromatic
Special thanks to Netlify and CircleCI