Storybookで完結！UIコンポーネントのインタラクション管理

この記事はエイチーム引越し侍 / エイチームコネクトの社員による、[Ateam Hikkoshi samurai Inc.× Ateam Connect Inc. Advent Calendar 2021](https://qiita.com/advent-calendar/2021/hikkoshizamurai-connect) 22日目の記事です。

22日は、Storybookにどハマりしてひたすらコンポーネントの作成と管理をしてたら、
プロダクト側の事何もわからなくなった@diaが担当します。

## はじめに

Component Story Format (CSF)最新の形式である「CFS3.0」がStorybook 6.4から利用可能となりました。

CSF3.0では新機能の**Play functions**が登場。
見た目だけでなく、コンポーネントのインタラクション管理も可能になっています。

今まで複雑な挙動のコンポーネントを新規で作成する際は、手動で操作して動作確認をしていましたが、CFS3.0ではその必要もなくなります。便利ですね。

動作パターンをコード上に残せるので俗人化の解消としても有用そうです。

更に、作成したストーリーは**Jestへの転用が可能**です。


## Play functionsのセットアップ

:::note warn
Create React Appしたプロジェクト + Storybook 6.4以上の環境を前提としています。
:::

Storybook 6.4にするだけではPlay functionsは使えません。まずはアドオンを追加します。
基本的には[公式](https://storybook.js.org/addons/@storybook/addon-interactions)に書いてある通りにやっているだけです。


####  1. Storybook Addon Interactionsの追加

```bash
yarn add -D @storybook/addon-interactions @storybook/jest @storybook/testing-library
```

#### 2. main.jsの更新

```.storybook/main.js:
module.exports = {
  addons: ['@storybook/addon-interactions'],
  features: {
    interactionsDebugger: true,
  },
};
```

#### 3. Storybook再起動

![スクリーンショット 2021-12-22 15.40.03.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/184969/6ad22f6d-1c52-863d-f5e4-e6b8f681c2fb.png)


addonの項目に「Interactions」が増えていればOKです。

## ストーリーにインタラクション追加

簡単なフォームを題材にインタラクションを追加してみます。

![sampleform.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/184969/0fa97887-5ab5-f9c4-0112-737c8106b98c.gif)


```SampleForm.stories.tsx
import React from "react";
import { ComponentStoryObj, ComponentMeta } from "@storybook/react";
import { within, userEvent, screen } from "@storybook/testing-library";
import { SampleForm } from "./SampleForm";

// CFS3.0ではMetaにtitleが不要になった。
// コンポーネントのディレクトリと同じ階層構造でStorybook側にも表示される。
export default { 
  component: SampleForm,
  render: () => <SampleForm />, // CFS3.0ではrender関数の中に表示したいStoryを記載する。
} as ComponentMeta<typeof SampleForm>;

// CFS3.0ではStoryが関数→オブジェクトに変更された
// オブジェクトになったことで、他Storyを継承して拡張するのが容易となった。
export const Success: ComponentStoryObj<typeof SampleForm> = {
  play: async ({ canvasElement }) => {
    const canvas = within(canvasElement);

    await userEvent.type(canvas.getByTestId("name"), "田中 太郎", {
      delay: 300, // 文字が入力される間隔の指定
    });
    await userEvent.type(canvas.getByTestId("age"), "20", {
      delay: 300,
    });
    await userEvent.selectOptions(canvas.getByTestId("sex"), "1");
    await screen.findByText("登録");
  },
};
```

上記では、
"フォームを全て入力すると送信ボタンのテキストが「登録」に変化する"
というストーリーを作成ています。

Play functionsの実行タイミングはストーリーがレンダリングされた直後です。


```ts
await screen.findByText("登録");
```

一連の動作の結果、UI上どうあるべきか定義しておく事で、予期せぬデグレを防ぐこともできます。

![スクリーンショット 2021-12-22 18.07.01.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/184969/ab93410c-9159-3750-984f-dec926ab1f06.png)

失敗すると「FAIL」となって教えてくれます。
正直この辺はJestのコンポーネントテスト側との使い分けがいまいち分かっていませんが・・・。


```html
<input data-testid="name" />
```

`getByTestId`の参照先は`id属性`ではなく、`data-testid`というカスタムデータ属性の方を参照してるので注意。
コンポーネント側にフックできるカスタムデータ属性や`role`属性を追加しておきましょう。


## Jestへの転用

先ほど作成したインタラクションをJestのコンポーネントテストに流用できます。

```SampleForm.spec.tsx
import React from "react";
import { findByText } from "@testing-library/dom";
import { render, screen } from "@testing-library/react";
import { composeStories } from "@storybook/testing-react";
import * as stories from "./SampleForm.stories";

const { Success } = composeStories(stories);

describe("sampleForm", () => {
  test("全項目入力すると、ボタンのテキストが「登録」となる", async () => {
    const { container } = render(<Success />);
    await Success.play({ canvasElement: container });

    expect(await screen.findByText("登録")).toBeInTheDocument();
  });
});

```

Storybookで作成したストーリーを`composeStories`経由で取得、`play()`でPlay functionsの実行が可能です。
Jest側行なっていたフォーム操作の過程をまるごとカット出来ます。

UI管理系~インラタクションまでをStorybookに集約、それらをJestで用いテストをするというのは見通しが良くて良いですね。

### 補足：エラー系トラブルシューティング備忘録

テスト実行以前にエラーでかなり詰まったので備忘録として対応したことを記しておきます。
誰かの参考になれば幸いです。

#### Syntax Error

```bash
 FAIL  src/App.test.tsx
  ● Test suite failed to run

    Jest encountered an unexpected token

    Jest failed to parse a file. This happens e.g. when your code or its dependencies use non-standard JavaScript syntax, or when Jest is not configured to support such syntax.

    Out of the box Jest supports Babel, which will be used to transform your files into valid JS based on your Babel configuration.

    By default "node_modules" folder is ignored by transformers.

    Here's what you can do:
     • If you are trying to use ECMAScript Modules, see https://jestjs.io/docs/ecmascript-modules for how to enable it.
     • If you are trying to use TypeScript, see https://jestjs.io/docs/getting-started#using-typescript
     • To have some of your "node_modules" files transformed, you can specify a custom "transformIgnorePatterns" in your config.
     • If you need a custom transformation specify a "transform" option in your config.
     • If you simply want to mock your non-JS modules (e.g. binary assets) you can stub them out with the "moduleNameMapper" config option.

    You'll find more details and examples of these config options in the docs:
    https://jestjs.io/docs/configuration
    For information about custom transformations, see:
    https://jestjs.io/docs/code-transformation

    Details:
```

storybook initすると発生します。
これは以下の環境変数を追加することで治りました。

```.env
SKIP_PREFLIGHT_CHECK=true
```

（[公式ドキュメントのチュートリアル](https://storybook.js.org/tutorials/intro-to-storybook/react/ja/get-started/)に書いてありました。）

#### Watch plugin jest-watch-typeahead/filename cannot be found. Make sure the watchPlugins configuration option points to an existing node module.

```bash
yarn add --exact jest-watch-typeahead@0.6.5
```

とりあえず上記を追加したら治りました。
参考：[issue](https://github.com/facebook/create-react-app/issues/11043)






# 明日
[Ateam Hikkoshi samurai Inc.× Ateam Connect Inc. Advent Calendar 2021](https://qiita.com/advent-calendar/2021/hikkoshizamurai-connect) 22日目の記事は、いかがでしたでしょうか。
明日は @dd511805がTerraform Providerについて書いてくれるようです。