React + TypeScriptのアプリケーションにStorybook + Storyshotsを導入する

React + TypeScriptのアプリケーションの環境を構築していて、StorybookとStoryshotsの設定でハマったところのメモ。

# 環境
React `16.12.0`
typescript `3.7.4`
webpack `4.41.5`
storybook `5.2.8`

最初create-react-appでやっていたが、storyshotの導入でwebpackやjestの設定を変える必要が出てきたので、使わないで構築し直した。


#storybookの導入

## セットアップ

### package.json
devDependenciesに以下を追加してインストール

```package.json
    "@storybook/addon-actions": "5.2.8",
    "@storybook/addon-links": "5.2.8",
    "@storybook/addons": "5.2.8",
    "@storybook/preset-typescript": "1.2.0",
    "@storybook/react": "5.2.8",
```

scriptsにスクリプトを追加

```package.json
    "storybook": "start-storybook -p 6006",
```

### /.storybook/config.jsを追加

```/.storybook/config.js
import { configure } from '@storybook/react';

const req = require.context('../src/', true, /\.stories\.(?:js|ts)x?$/);
configure(req, module);
```

### /.storybook/webpack.config.js を追加

ストーリーをTypeScriptで書くには、 `/.storybook/` 内にwebpack.config.jsを追加する。

アプリケーションのwebpack設定を引き継ぎたい場合は
https://storybook.js.org/docs/configurations/custom-webpack-config/#using-your-existing-config
のように書くと短く書ける。

```/webpack.config.js
  resolve: {
    modules: ['src', 'node_modules'],
    extensions: ['.ts', '.tsx', '.js'],
  },
```

この辺の設定も引き継がないと、import文のpath解決がうまく行かなくてちょっとハマった。
stories.tsx内でimport文でエラーになってしまう。


```SomeComponent.stories.tsx
import OtherComponent from 'components/OtherComponent'; // モジュールのrootに指定した`src`がmodulesに設定されてないと Cannot find module
import AppHeader from '..'; //同階層のindex.tsxをインポート。extensionsにtsxが設定されていないと、Can't resolve '..'
```

なので
/.storybook/webpack.config.js はこのようになった。

```/.storybook/webpack.config.js
const path = require('path');
// your app's webpack.config.js
const custom = require('../webpack.config.js');

module.exports = async ({ config, mode }) => {
  return {
    ...config,
    resolve: { ...config.resolve, ...custom.resolve },
    module: { ...config.module, rules: custom.module.rules }
  };
};
```


# Storyshotsの導入

## require-context-hookの設定

セットアップは
https://storybook.js.org/docs/testing/structural-testing/#using-storyshots
の通りだが、

>require.context

の部分でエラーが出てしまうので、require-context-hookの設定をする。

devDependenciesに追加するのはこの辺

```package.json
    "jest": "24.9.0",
    "react-test-renderer": "16.12.0",
    "@storybook/addon-storyshots": "5.2.8",
    "@types/jest": "24.0.25",
    "babel-plugin-require-context-hook": "1.0.0",
```

.babelrcにテスト用の設定を追加

```
  "env": {
    "test": {
      "plugins": [
        "require-context-hook"
      ]
    }
  }
```

`/.storybook/config.js` のrequire.contextの行の前に以下を足す。

```/.storybook/config.js
if (process.env.NODE_ENV === 'test') {
  require('babel-plugin-require-context-hook/register')();
}
```

【追記】 storybook 5.3.1 にアップデートしたらrequire.contextを使わない記述になっていたので、ここの設定は不要っぽい :innocent: 


## cssや画像のインポートでのエラーを解決する

ストーリーの中でcssや画像をインポートしていると、storyshotsの実行時にエラーが出る。

>Jest encountered an unexpected token
This usually means that you are trying to import a file which Jest cannot parse, e.g. it's not plain JavaScript.

https://jestjs.io/docs/en/webpack#handling-static-assets
この通りにmoduleNameMapperを使用して解決できる。

## スナップショットファイルをストーリーごとに分ける

デフォルトでは全てのストーリーのスナップショットが１ファイルに書きだされる。
スナップショットはストーリーごとに分かれた方が圧倒的に差分が見やすいので、分けたい。
そこでmultiSnapshotWithOptionsというオプションを指定する。
https://github.com/storybookjs/storybook/tree/master/addons/storyshots/storyshots-core#multisnapshotwithoptionsoptions

Storyshots.test.jsをこのように書き換える。

```Storyshots.test.js
import initStoryshots, {
  multiSnapshotWithOptions,
} from '@storybook/addon-storyshots';

initStoryshots({
  test: multiSnapshotWithOptions(),
});
```

jestのconfigにはtransformの設定をする。

```
    "transform": {
      "^.+\\.stories\\.tsx$": "@storybook/addon-storyshots/injectFileName",
      "^.+\\.jsx?$": "babel-jest",
      ".+\\.tsx?$": "ts-jest"
    }
```

これで、各ストーリーのフォルダにスナップショットが作成される。 :thumbsup: