What is Storybook Play function?
Tags: storybook, testing, frontend, webdev
Published: 2024-03-10 07:19:12

This article is about the Play function in Storybook.

## What is the Play function?

> Play functions are small snippets of code executed after the story renders. Enabling you to interact with your components and test scenarios that otherwise required user intervention.
https://storybook.js.org/docs/writing-stories/play-function

The Play function can be described as a feature that **enables the definition of stories that include user operations**.

What exactly is a Story?
> A story captures the rendered state of a UI component. Developers write multiple stories per component that describe all the “interesting” states a component can support.
https://storybook.js.org/docs/get-started/whats-a-story

A **capture of the "interesting states" a component can support**.

Putting it together, the Play function is a feature that **enables the definition of "interesting states" that a component supports, including user operations**.

### Example

In the example below, the Play function is used in a Story called Only Available.
Here, it defines the state after a user clicks a checkbox, showing available stock items.

![A story with play function](https://storage.googleapis.com/zenn-user-upload/45665640b443-20240310.png)
*(Image capture from: https://www.youtube.com/clip/Ugkx9Yn1tl4Nm33eAD6kTD2svqYLq8QA0YP7 at 3:46)*

## Reusable in Tests

If the Play function enables the definition of **interesting states including user operations**, then such states are precisely those that should be tested and **can be reused as test cases**.

It is also easy to reuse in terms of test implementation.
In the following example, the Play function is reused in React tests.

```tsx
// Define a Story with text input
export const InputFieldFilled: Story<InputFieldProps> = {
  play: async ({ canvasElement }) => {
    const canvas = within(canvasElement);
    await userEvent.type(canvas.getByRole('textbox'), 'Hello world!');
  },
};
```
```tsx
const { InputFieldFilled } = composeStories(stories);

test('renders with play function', async () => {
  // 👇 Reuse the above Story
  const { container } = render(<InputFieldFilled />);
  await InputFieldFilled.play({ canvasElement: container });

  const input = screen.getByRole('textbox') as HTMLInputElement;
  expect(input.value).toEqual('Hello world!');
});
```

A benefit of reusing the Play function in tests is the ability to write tests with visual feedback. Typically, tests written with libraries like testing-library are implemented through trial and error without visual feedback, which can be challenging.

For more details on implementation, please refer to the following link (the above code is also quoted from this link):
https://github.com/storybookjs/testing-react

## Summary

- The Play function allows for the definition of interesting states including user operations as Stories.
- It is reusable in tests, offering benefits in both Storybook and testing scenarios.
- The implementation is straightforward.

Thank you for reading this article!
Happy coding! 🚀

## Bonus

I'll share some issues I encountered while experimenting with the Play function.

### Interactions tab does not display interactions

```tsx
Copy code
// 🙅🏻: Was using `within` and `userEvent` from `@testing-library/react`
import { userEvent, within } from '@testing-library/react';
// 🙆: Use `@storybook/testing-library` instead
import { userEvent, within } from '@storybook/testing-library';
```

Reference:
https://stackoverflow.com/questions/75475488/storybook-play-function-doesnt-show-interactions-in-the-bottom-panel

### Cannot invoke an object which is possibly 'undefined'.

```tsx
Copy code
export const CombinedStories: Story = {
  play: async (context) => {
    // 🙅🏻: Error: Cannot invoke an object which is possibly 'undefined'.
    await FirstStory.play(context);
    await SecondStory.play(context);
    // 🙆
    await FirstStory.play?.(context);
    await SecondStory.play?.(context);
  },
};
```
Reference:
https://github.com/storybookjs/storybook/issues/21969#issuecomment-1647287480

### Pass a context when invoking play function of another story

```tsx
Copy code
export const CombinedStories: Story = {
  // 🙅🏻
  play: async ({canvasElement}) => {
    // Error: `Pass a context when invoking play function of another story`
    await FirstStory.play?.(canvasElement);
  }
  // 🙆: Must pass the entire context
  play: async (context) => {
    await FirstStory.play?.(context);
  }
};
```

Reference:
https://github.com/storybookjs/eslint-plugin-storybook/blob/main/docs/rules/context-in-play-function.md

---
If you want to read the article in Japanese here is the link: https://zenn.dev/takuyakikuchi/articles/9459769f675065