もしかしてStorybookの「Play function」まだ使ったことない？

キャッチアップできてなかったStorybookの`Play function`について調べます。

https://storybook.js.org/docs/writing-tests/interaction-testing?renderer=react

:::note
Storybookの理解を深めるための一助になると幸いです。
:::

## 「Play function」とは？
Play関数は、コンポーネントを操作したり、ユーザーの介入を必要とするシナリオをテストしたりできるようになります。

## 設定する
今回は既存環境に導入する想定で進めていきます！
まずは`addon-interactions`をインストールします。

```bash
pnpm add -D @storybook/addon-interactions
```

次にStorybookの設定を更新します。

```javascript
addons: [
  '@storybook/addon-interactions', // 追加する
],
```

formを用意します。

```tsx
import { FC } from 'react';

export const Form: FC = () => {
  return (
    <form
      className="bg-white shadow-md rounded px-8 pt-6 pb-8 mb-4"
    >
      <div className="mb-4">
        <label
          className="block text-gray-700 text-sm font-bold mb-2"
          htmlFor="email"
        >
          メールアドレス
        </label>
        <input
          className="shadow appearance-none border rounded w-full py-2 px-3 text-gray-700 leading-tight focus:outline-none focus:shadow-outline"
          id="email"
          type="email"
          placeholder="メールアドレス"
          data-testid="email"
        />
      </div>
      <div className="mb-6">
        <label
          className="block text-gray-700 text-sm font-bold mb-2"
          htmlFor="password"
        >
          パスワード
        </label>
        <input
          className="shadow appearance-none border rounded w-full py-2 px-3 text-gray-700 mb-3 leading-tight focus:outline-none focus:shadow-outline"
          id="password"
          type="password"
          placeholder="パスワード"
          data-testid="password"
        />
      </div>
      <div className="flex items-center justify-between">
        <button
          className="bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded focus:outline-none focus:shadow-outline"
          type="submit"
        >
          サブミット
        </button>
      </div>
    </form>
  );
};
```

次にStoryファイルを用意します。

```tsx
import type { Meta, StoryObj } from '@storybook/react';
import { userEvent, within } from '@storybook/test';
import { Form } from './Form';

const meta = {
  title: 'Components/Form',
  component: Form,
  parameters: {
    layout: 'centered',
  },
  tags: ['autodocs'],
} satisfies Meta<typeof Form>;

export default meta;
type Story = StoryObj<typeof meta>;

export const Default: Story = {
  play: async ({ canvasElement }) => {
    const canvas = within(canvasElement);
    await userEvent.type(canvas.getByTestId('email'), 'email@provider.com');
    await userEvent.type(canvas.getByTestId('password'), 'a-random-password');
  },
};
```

`play`関数を実行するためにはファイル上部で下記の記述を追加します！

```jsx
import { userEvent, within } from '@storybook/test';
```

ここまで書くとStorybookで下記のように動作することが確認できます！

![画面収録-2024-07-17-0.17.03.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/146612/7426c833-8f73-ec58-4ccb-c6bbaef3304d.gif)

めっちゃすごいですよね！？
結構手軽にかけてしまったので驚きました…。

サブミットボタンもクリックさせてみます！
下記を追加します。

```tsx
await userEvent.click(canvas.getByRole('button'));
```

するとこんなのが出ました…

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/146612/63ea7c5e-8712-3d1b-4b0f-60508c77d0dc.png)

ストーリーがないか、ストーリーが選択できてないと怒られました…。

:::note warn
formタグに`action`を設定するのを忘れておりました…
:::

`action`属性を追加します。

```tsx
<form action="/">
```

するとStorybookの一番上にあるコンポーネントのDocsが表示されるようになりました。

:::note
ただ、どうせならFormコンポーネントのDocsが表示されてほしいですね…
試してみましたが、解決できなかったのでわかったら追記しておきます…。
:::

### `step `を追加する

`play`関数の部分を下記のように変えてみました。

```tsx
play: async ({ canvasElement, step }) => {
    const canvas = within(canvasElement);
    await step('Enter credentials', async () => {
      await userEvent.type(canvas.getByTestId('email'), 'hi@example.com');
      await userEvent.type(canvas.getByTestId('password'), 'supersecret');
    });

    await step('Submit form', async () => {
      await userEvent.click(canvas.getByRole('button'));
    });
  },
```

`step`でグルーピングすることで画像のようなネスト構造になって見やすくなります！

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/146612/aafe8825-c60d-7d43-319b-9ca2101f0b73.png)

## まとめ

コンポーネントがレンダリングされる前にコードを実行する`mount`メソッド内の関数を使用することで、レンダリング前にコードを実行できます。

:::note
`mount`を利用してモックデータの作成することも可能です！
:::

公式ドキュメントに記載があるイベントは一部だけなようなので詳細はこちらのサイトから確認できます！

https://testing-library.com/docs/user-event/utility/

まだわからない部分は多いですが、簡単なテストであればStorybookで実施できてしまうのは良いですね！

さらに深くキャッチアップしていきたいと思います！

## 参考

https://storybook.js.org/docs/writing-stories/play-function

https://storybook.js.org/docs/writing-tests/interaction-testing?renderer=react