Storybook上でインタラクションテストを実行する

## はじめに

フロントエンド開発において[Storybook](https://storybook.js.org/)を導入しているが、ユーザー操作による**コンポーネントの動作を機械的にテストできていない**課題があった。　　

Storybookを使って、コンポーネントの動作をテストできないか調べると`@storybook/test-runner`が良さげだった。  
この記事は、`@storybook/test-runner`を使った**インタラクションテスト**の実装方法の備忘録。

:::note info
参考記事
Storybook: https://storybook.js.org/
@storybook/test-runner: https://storybook.js.org/addons/@storybook/test-runner/
インタラクションテスト: https://storybook.js.org/tutorials/intro-to-storybook/react/ja/test/
:::

## サンプルコード

今回は**曜日を選択するとメッセージが表示されるVueコンポーネント**をサンプルに`@storybook/test-runner`でインタラクションテストを書く。

- サンプル（曜日を選択するとメッセージが表示されるVueコンポーネント）

```vue:Sample.vue
<template>
  <div>
    <select v-model="value">
      <option value="" disabled>選択してください</option>
      <option value="Mon">月曜日</option>
      <option value="Tue">火曜日</option>
      <option value="Wed">水曜日</option>
      <option value="Thu">木曜日</option>
      <option value="Fri">金曜日</option>
      <option value="Sat">土曜日</option>
      <option value="Sun">日曜日</option>
    </select>
    <div class="message">
      <div v-if="value === 'Mon'">Monday</div>
      <div v-if="value === 'Tue'">Tuesday</div>
      <div v-if="value === 'Wed'">Wednesday</div>
      <div v-if="value === 'Thu'">Thursday</div>
      <div v-if="value === 'Fri'">Friday</div>
      <div v-if="['Sat', 'Sun'].includes(value)" class="message_holiday">
        土日だワッショイ！！
      </div>
    </div>
  </div>
</template>

<script setup lang="ts">
import { ref } from 'vue';
const value = ref('');
</script>

<style lang="scss" scoped>
.message {
  margin-top: 16px;

  &_holiday {
    color: red;
  }
}
</style>
```

:::note info
土日を選択すると「土日だワッショイ！！」というメッセージが表示されます。
:::


- 動作キャプチャ  
  - 曜日を選択するとメッセージが表示される

![Kapture 2023-01-30 at 16.08.04.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/397734/1384f90d-e3a1-92fe-14fc-c90edd1b89e7.gif)


## ストーリーの追加

サンプルコードのストーリーを追加する。

```js:Sample.stories.js
import Sample from '../Sample.vue';
import { within, userEvent } from '@storybook/testing-library';

const meta = {
  title: 'Components/Sample',
  component: Sample,
  render: () => ({
    components: { Sample },
    template: '<Sample />',
  }),
};

export default meta;

export const Default = {};
```

このストーリーだけだと、各曜日を選択した際に特定のメッセージが表示される**コンポーネント動作**を**機械的に確認できない**。

:::note warn
ストーリー上でセレクトボックスを選択して**目視での確認が必要**。（コンポーネントのpropsの持たせ方を工夫すれば、初期描画時に選択された状態にできそうですが、選択肢の数だけストーリが増えるので面倒ですね。）
:::

![Kapture 2023-01-30 at 16.39.15.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/397734/18009cd5-5da6-e42d-4679-540492620bf0.gif)

## インタラクションの追加

ここでいう**インタラクション**とは、ストーリー上でクリックやフォーム入力の操作のことを指す。`play関数`を定義することで、ストーリー上でのクリックやセレクトボックスの選択が定義できる。

> `play関数`：StorybookのCSF3.0から新機能として追加された関数。

```js:Sample.stories.js
...
...
export const Monday = {
  play: async ({ canvasElement }) => {
    const canvas = within(canvasElement);
    // セレクトボックスで月曜日を選択
    await userEvent.selectOptions(canvas.getByRole('combobox'), 'Mon');
  },
};

export const Tuesday = {
  play: async ({ canvasElement }) => {
    const canvas = within(canvasElement);
    await userEvent.selectOptions(canvas.getByRole('combobox'), 'Tue');
  },
};

export const Sunday = {
  play: async ({ canvasElement }) => {
    const canvas = within(canvasElement);
    await userEvent.selectOptions(canvas.getByRole('combobox'), 'Sun');
  },
};
```

上記のインタラクションが定義されたストーリーを追加することで、**セレクトボックスが選択**された結果の画面が描画される。

![Kapture 2023-01-30 at 16.49.13.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/397734/ea13943a-a3e7-41d8-498c-fc55f6ade36d.gif)

上記キャプチャでは全ての曜日のストーリーを定義している

:::note warn
インタラクションの操作結果はスナップショットに反映されないため、特定のメッセージが出力されていることを機械的に確認することはできていません。
:::




## （本題）@storybook/test-runnerでインタラクションテストを書く

### 1. 必要なライブラリのインポート

```
yarn add -D @storybook/test-runner @storybook/jest
```

### 2. インタラクションテストの追加

```js:Sample.stories.js
...
...
export const Monday = {
  play: async ({ canvasElement }) => {
    const canvas = within(canvasElement);
    await userEvent.selectOptions(canvas.getByRole('combobox'), 'Mon');
    // 月曜日を選択した場合は「Monday」が表示されること
    await expect(await canvas.queryByText('Monday')).toBeTruthy();
  },
};

export const Tuesday = {
  play: async ({ canvasElement }) => {
    const canvas = within(canvasElement);
    await userEvent.selectOptions(canvas.getByRole('combobox'), 'Tue');
    // 火曜日を選択した場合は「Tuesday」が表示されること
    await expect(await canvas.queryByText('Tuesday')).toBeTruthy();
  },
};

export const Sunday = {
  play: async ({ canvasElement }) => {
    const canvas = within(canvasElement);
    await userEvent.selectOptions(canvas.getByRole('combobox'), 'Sun');
    // 日曜日を選択した場合は「土日だワッショイ！！」が表示されること
    await expect(await canvas.queryByText('土日だワッショイ！！')).toBeTruthy();
  },
};
```

ここでは以下のインタクションテストを追加。  
- 月曜日を選択した場合は **Monday** が表示されること
- 火曜日を選択した場合は **Tuesday** が表示されること
- 日曜日を選択した場合は **土日だワッショイ！！** が表示されること

### 3. インタラクションテストの実行方法

- 実行コマンド

```
# 6006番ポートでStorybookを起動
yarn storybook dev --port 6006

# 起動しているStorybook上でtest-runnerを実行（インタラクションテストの実行）
yarn test-storybook --url http://localhost:6006
```

- 実行結果

```
$ test-storybook --url http://localhost:6006
 PASS   browser: chromium  ../hogehoge/Sample.stories.js
  Components/Sample
    Default
      ✓ smoke-test (27 ms)
    Monday
      ✓ play-test (34 ms)
    Sunday
      ✓ play-test (7 ms)

Test Suites: 1 passed, 1 total
Tests:       3 passed, 3 total
Snapshots:   0 total
Time:        3.172 s
Ran all test suites.
✨  Done in 5.93s.
```

:::note info
テストが全て成功
:::


- 試しにSample.vueのメッセージ内容を変更して**インタクションテストを実行してみる**
  - （Sample.stories.jsのメッセージ内容は修正しない）

```diff:Sample.vue
（省略...）
  <div>
    <select v-model="value">
      <option value="" disabled>選択してください</option>
      <option value="Mon">月曜日</option>
      <option value="Tue">火曜日</option>
      <option value="Wed">水曜日</option>
      <option value="Thu">木曜日</option>
      <option value="Fri">金曜日</option>
      <option value="Sat">土曜日</option>
      <option value="Sun">日曜日</option>
    </select>
    <div class="message">
      <div v-if="value === 'Mon'">Monday</div>
      <div v-if="value === 'Tue'">Tuesday</div>
      <div v-if="value === 'Wed'">Wednesday</div>
      <div v-if="value === 'Thu'">Thursday</div>
      <div v-if="value === 'Fri'">Friday</div>
      <div v-if="['Sat', 'Sun'].includes(value)" class="message_holiday">
-        土日だワッショイ！！
+        土日だワッチョイ！！
      </div>
    </div>
  </div>
...
```

- テスト結果
  - インタラクションテストでは「土日だワッショイ！！」が出力されることを期待しているので、失敗するはず
```
yarn test-storybook --url http://localhost:6006

$ test-storybook --url http://localhost:6006
 FAIL   browser: chromium  app/frontend/stories/components/Sample.stories.js
  Components/Sample
    Default
      ✓ smoke-test (27 ms)
    Monday
      ✓ play-test (33 ms)
    Sunday
      ✕ play-test (14 ms)
```


:::note info
Sunday（日曜日を選択した場合）のテストコードが失敗した。👍
:::




## まとめ

- Storybookを利用してユーザー操作によるUIテストを行えた
- Storybookを起動しないでインタラクションテストできるとより良い（要調査）
- CIでインタラクションテストを実行することは可能
  - 参考記事  
    - [Storybook でインタラクションテストを作成して Jest で再利用する](https://zenn.dev/azukiazusa/articles/df307292037265)
    - [CI上でテストを実行する](https://azukiazusa.dev/blog/storybook-testing/#ci-%E4%B8%8A%E3%81%A7%E3%83%86%E3%82%B9%E3%83%88%E3%82%92%E5%AE%9F%E8%A1%8C%E3%81%99%E3%82%8B)
    - [Storybook 単体でインタラクションテストを実施する](https://zenn.dev/azukiazusa/articles/storybook-interaction-testing)