番外編：Storybook で UI コンポーネントをカタログ化する

本編はこちらです。

* [開発コンテナで快適！<del>ひきこもり生活</del>フロントエンド開発 - Qiita](https://qiita.com/YokoKen/items/70a517b09c6cf00c15f3)

---
Storybook というフレームワークを導入すると、作成した UI コンポーネントをカタログのように管理することができます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/413915/97d8e669-641c-6528-c614-90dcb69dd4bc.png)
UI コンポーネントをカタログ化することで再利用が促進されますし、例えばアプリを操作して特定の手順を踏まないと表示されないような UI コンポーネントも簡単に確認できるようになります。
また、カタログ化された各コンポーネントのスクリーンショットを記録することができるため、視覚情報としての差分検出も可能です。

カタログにはコンポーネント毎に登録を行う必要がありますが、必要な作業は MDX (Markdown と JSX を組み合わせて書ける) ファイルの作成だけで特別な操作は不要ですし、本記事では MDX の雛形をコードスニペット化してしまいますので本当に簡単に作成できます。


# 本題に入る前に
せっかくカタログ化を行うので、ここで下記のようなフォルダ階層[^を導入してみます。ついでに新しく Message コンポーネントも用意してしまいます。


* **[src]**
    * **[client]**
        * **[pages]**
            * Home.tsx
            * Home.spec.tsx
            * HomeWork.tsx
            * HomeWork.spec.tsx
        * **[parts]**
            * Message.tsx
            * Message.spec.tsx
        * App.tsx
        * App.spec.tsx
        * App.styl
        * favicon.ico
        * index.html
        * index.tsx

`pages` フォルダと `parts` フォルダを作成し、Home.* と HomeWork.* を `pages` フォルダに移動してください。

`App.tsx` と `App.spec.tsx` は下記のようにリンクを修正します。

```tsx:App.tsx
import * as React from "react";
import { Switch, Route, Link } from "react-router-dom";

import * as styles from "./App.styl";
import Home from "./pages/Home";
import HomeWork from "./pages/HomeWork";

const App = () => {
    return (
        <>
            <Link className={styles.menuButton} to="/">Home</Link>
            <Link className={styles.menuButton} to="/homework">Home Work</Link>
            <Switch>
                <Route exact path="/" component={Home} />
                <Route exact path="/homework" component={HomeWork} />
            </Switch>
        </>
    );
};

export default App;

```

```tsx:App.spec.tsx
import * as React from "react";
import { MemoryRouter } from "react-router-dom";
import { render, cleanup, fireEvent } from "@testing-library/react";
import "@testing-library/jest-dom/extend-expect";

const homeMock = jest.fn(() => <></>);
jest.mock("./pages/Home", () => ({ __esModule: true, default: homeMock } as any));

const homeworkMock = jest.fn(() => <></>);
jest.mock("./pages/HomeWork", () => ({ __esModule: true, default: homeworkMock } as any));

import App from "./App";

afterEach(cleanup);
afterEach(jest.clearAllMocks);
afterAll(() => {
    jest.unmock("./pages/Home");
    jest.unmock("./pages/HomeWork");
});

describe("App", () => {
    it("最初に Home が表示されること", () => {
        render(<MemoryRouter><App /></MemoryRouter>);
        expect(homeMock).toBeCalled();
    });

    it("メニューから Home を表示できること", () => {
        const root = render(<MemoryRouter><App /></MemoryRouter>);
        expect(homeMock).toBeCalled();
        homeMock.mockClear();

        const menuHomeButton = root.getByText("Home", { exact: true });
        fireEvent.click(menuHomeButton);

        expect(homeMock).toBeCalled();
    });

    it("メニューから Home Work を表示できること", () => {
        const root = render(<MemoryRouter><App /></MemoryRouter>);
        expect(homeworkMock).not.toBeCalled();

        const menuHomeWorkButton = root.getByText("Home Work", { exact: true });
        fireEvent.click(menuHomeWorkButton);

        expect(homeworkMock).toBeCalled();
    });
});

```

Home.tsx と HomeWork.tsx は下記のように Message コンポーネントを使用するよう修正します。

```tsx:pages/Home.tsx
import * as React from "react";
import Message from "../parts/Message";

const Home = () => {
    return (
        <>
            <Message message="Hello World." />
        </>
    );
};

export default Home;

```

```tsx:pages/HomeWork.tsx
import * as React from "react";
import { useState, useEffect } from "react";
import Message from "../parts/Message";

const HomeWork = () => {
    const [message, setMessage] = useState("Loading...");
    useEffect(() => {
        fetch("/api/hello")
            .then(response => response.json())
            .then(json => setMessage(json.message));
    }, []);
    return (
        <>
            <Message message={message} />
        </>
    );
};

export default HomeWork;

```

Message.tsx と Message.spec.tsx は下記の内容で作成してください。

```tsx:parts/Message.tsx
import * as React from "react";

interface MessageProps {
    /** メッセージテキスト。 */
    message: string;
}

const Message = (props: MessageProps) => {
    return (
        <h1>{props.message}</h1>
    );
};

export default Message;

```

```tsx:parts/Message.spec.tsx
import * as React from "react";
import { render, cleanup } from "@testing-library/react";
import "@testing-library/jest-dom/extend-expect";

import Message from "./Message";

afterEach(cleanup);
afterEach(jest.clearAllMocks);

describe("Message", () => {
    it("メッセージが表示されること", () => {
        const root = render(<Message message="Hello." />);
        const heading = root.getByRole("heading");
        expect(heading).toHaveTextContent("Hello.");
    });
});

```

準備が整いました。
ここからが本題となります。


# 開発コンテナの設定変更
### Dockerfile
スナップショットはヘッドレス Chrome (GUI としての表示を行わずにバックグラウンドで Chrome を起動するモード) を使用して記録されます。
Chrome をインストールするよう Dockerfile を変更しておきます。

`.devcontainer/Dockerfile` ファイルを下記の内容に書き換えてください。

```dockerfile
FROM node:12.16-buster-slim
ENV DEBIAN_FRONTEND=noninteractive
RUN apt-get update \
    #
    # Verify git, process tools installed
    && apt-get -y install --no-install-recommends git openssh-client iproute2 procps \
    #
    #####
    # https://github.com/Microsoft/vscode-dev-containers/tree/master/containers/docker-in-docker#how-it-works--adapting-your-existing-dev-container-config
    # Note that no recommended packages are required, except for gnupg-agent.
    #
    # Install Docker CE CLI
    && apt-get install -y --no-install-recommends apt-transport-https ca-certificates curl software-properties-common lsb-release jq \
    && apt-get install -y gnupg-agent \
    && curl -fsSL https://download.docker.com/linux/$(lsb_release -is | tr '[:upper:]' '[:lower:]')/gpg | apt-key add - 2>/dev/null \
    && add-apt-repository "deb [arch=amd64] https://download.docker.com/linux/$(lsb_release -is | tr '[:upper:]' '[:lower:]') $(lsb_release -cs) stable" \
    && apt-get update \
    && apt-get install -y --no-install-recommends docker-ce-cli \
    #
    # Install Docker Compose
    && curl -sSL "https://github.com/docker/compose/releases/download/$(curl https://api.github.com/repos/docker/compose/releases/latest | jq .name -r)/docker-compose-$(uname -s)-$(uname -m)" -o /usr/local/bin/docker-compose \
    && chmod +x /usr/local/bin/docker-compose \
    #
    #####
    # Install latest chrome dev package and fonts to support major charsets (Chinese, Japanese, Arabic, Hebrew, Thai and a few others)
    # Note: this installs the necessary libs to make the bundled version of Chromium that Puppeteer
    # installs, work.
    && curl -fsSL https://dl-ssl.google.com/linux/linux_signing_key.pub | apt-key add - \
    && sh -c 'echo "deb [arch=amd64] http://dl.google.com/linux/chrome/deb/ stable main" >> /etc/apt/sources.list.d/google.list' \
    && apt-get update \
    && apt-get install -y google-chrome-unstable fonts-ipafont-gothic fonts-wqy-zenhei fonts-thai-tlwg fonts-kacst fonts-freefont-ttf --no-install-recommends \
    #
    #####
    # Clean up
    && apt-get autoremove -y \
    && apt-get clean -y \
    && rm -rf /var/lib/apt/lists/*
ENV DEBIAN_FRONTEND=dialog

```


### devcontainer.json
Storybook は Web アプリケーションです。ポート6006を使用するのでポートフォワーディング設定を追加します。

`.devcontainer/devcontainer.json` ファイルの `forwardPorts` に下記を追加してください。

```json-doc
        6006, // Storybook server
```

MDX 形式ファイル用の拡張機能 (シンタックスハイライトのみですが) も追加します。

`.devcontainer/devcontainer.json` ファイルの `extensions` に下記を追加してください。

```json-doc
        "silvenon.mdx",
```


### リビルド
開発コンテナの設定を変えましたので、一度[リビルド](https://qiita.com/YokoKen/items/70a517b09c6cf00c15f3#%E9%96%8B%E7%99%BA%E3%82%B3%E3%83%B3%E3%83%86%E3%83%8A%E3%81%AE%E3%83%AA%E3%83%93%E3%83%AB%E3%83%89) を行ってください。


# パッケージの設定変更
### Node モジュールのインストール
下記のコマンドにて、Storybook 関係のモジュールをインストールします。

```bash
npm i -D \
    @storybook/cli \
    @storybook/react \
    @storybook/addons \
    @storybook/addon-actions \
    @storybook/addon-docs \
    @storybook/addon-knobs \
    @storybook/addon-links \
    @storybook/addon-storyshots \
    @storybook/addon-storyshots-puppeteer \
    @storybook/addon-viewport \
    react-docgen-typescript-loader \
    @babel/core \
    babel-loader \
    puppeteer \
    @types/puppeteer

```


### スクリプトの変更
`package.json` ファイルの `scripts` を下記の内容に書き換えてください。(`test` コマンドの変更及び `test-snapshots` コマンド、`accept-snapshots` コマンド、`storybook` コマンドの追加。)

```json
    "scripts": {
        "start": "            export NODE_ENV=production  && ts-node ./src/server/server.ts",
        "start:dev": "        export NODE_ENV=development && ts-node-dev --nolazy --inspect=9229 ./src/server/server.ts",
        "build": "            export NODE_ENV=production  && webpack --config ./webpack.config.ts",
        "build:dev": "        export NODE_ENV=development && webpack --config ./webpack.config.ts",
        "run": "              export NODE_ENV=production  && npm run build     && npm start",
        "run:dev": "          export NODE_ENV=development && npm run build:dev && npm run start:dev",
        "run:hmr": "          export NODE_ENV=development && export HMR=true   && concurrently \"npm run start:dev\" \"webpack-dev-server --config ./webpack.config.hmr.ts\"",
        "test": "             export NODE_ENV=test        && jest --coverage --testPathIgnorePatterns=src/client/storyshots/",
        "test-snapshots": "   export NODE_ENV=test        && jest src/client/storyshots/",
        "accept-snapshots": " export NODE_ENV=test        && jest -u src/client/storyshots/",
        "storybook": "        export NODE_ENV=development && start-storybook -p 6006"
    },
```


### Git Ignore の変更
`.gitignore` ファイルの末尾に下記を追加してください。(先頭ではなく末尾です。)

```config
storybook-static/
__snapshots__/
__image_snapshots__/
!.vscode/*.code-snippets
```


### Docker Ignore の変更
`.dockerignore` ファイルに下記を追加してください。

```config
**/storybook-static
**/__snapshots__
**/__image_snapshots__

```


# Storybook の設定
### 基本設定 (main.ts)
`.storybook/main.ts` ファイルを作成し、下記の内容で保存してください。

```ts
import { StorybookConfig } from "@storybook/core/types";
import myConfig from "../webpack.config";


module.exports = <StorybookConfig>{
    stories: ["../src/client/**/*.stories.mdx"],
    addons: [
        "@storybook/addon-actions",
        "@storybook/addon-docs",
        "@storybook/addon-knobs",
        "@storybook/addon-links",
        "@storybook/addon-viewport",
    ],
    webpackFinal: (config, options) => {
        config.resolve = config.resolve ?? {};
        config.resolve.extensions = config.resolve.extensions ?? [];
        config.resolve.modules = config.resolve.modules ?? [];
        config.module = config.module ?? { rules: [] };
        return {
            ...config,
            resolve: {
                ...config.resolve,
                extensions: [
                    ...config.resolve.extensions,
                    ...myConfig.resolve!.extensions!,
                ],
                modules: [
                    ...config.resolve.modules,
                    ...myConfig.resolve!.modules!,
                ],
            },
            module: {
                ...config.module,
                rules: [
                    ...config.module.rules,
                    myConfig.module!.rules!.find(rule => (rule.test as RegExp)?.source === /\.tsx?$/.source)!,
                    myConfig.module!.rules!.find(rule => (rule.test as RegExp)?.source === /\.styl$/.source)!,
                ],
            },
            performance: false,
        };
    }
};

```


### リバースプロキシの設定 (middleware.ts)
`.storybook/middleware.js` ファイルを作成し、下記の内容で保存してください。(`.ts` ではなく `.js` です。`.ts` は非対応のようです。)

```js
const proxy = require("http-proxy-middleware");

module.exports = router =>
    router.use(
        "^/api",
        proxy({
            target: "http://localhost:3000"
        })
    );

```


# スナップショットテストの設定
### Jest 設定
`jest.config.json` ファイルを下記の内容に書き換えてください。

```json
{
    "preset": "ts-jest",
    "modulePaths": [
        "src"
    ],
    "moduleNameMapper": {
        "\\.(css|styl)$": "<rootDir>/node_modules/jest-css-modules"
    },
    "transform": {
        "\\.mdx$": "@storybook/addon-docs/jest-transform-mdx",
        "\\.jsx?$": [
            "babel-jest",
            {
                "presets": [
                    "@babel/preset-env",
                    "@babel/preset-react"
                ]
            }
        ]
    },
    "coveragePathIgnorePatterns": [
        "/node_modules/",
        "/\\.storybook/",
        "\\.stories\\."
    ]
}

```


### スナップショットテストの作成
`src/client/storyshots/default.spec.js` ファイルを作成し、下記の内容で保存してください。

```tsx
import initStoryshots from "@storybook/addon-storyshots";
import { imageSnapshot } from "@storybook/addon-storyshots-puppeteer";

initStoryshots({
    suite: "Image storyshots",
    test: imageSnapshot({
        storybookUrl: "http://localhost:6006/",
        beforeScreenshot: (page, options) => {
            return new Promise(resolve => setTimeout(() => resolve(), 1000));
        }
    })
});

```


# コードスニペットの設定
`.vscode/storybook.code-snippets` ファイルを作成し、下記の内容で保存してください。

```json-doc
{
    "Body": {
        "scope": "markdown",
        "prefix": "sb/body",
        "body": [
            "import { Meta, Story, Preview, Props } from \"@storybook/addon-docs/blocks\";",
            "import * as knobs from \"@storybook/addon-knobs\";",
            "import { action } from \"@storybook/addon-actions\";",
            "import { linkTo } from \"@storybook/addon-links\";",
            "import { MemoryRouter } from \"react-router-dom\";",
            "import ${TM_FILENAME_BASE/\\..*//} from \"./${TM_FILENAME_BASE/\\..*//}\";",
            "",
            "",
            "<Meta title=\"${TM_DIRECTORY/(.*)(?<=src\\/client)(\\/?)(.*)/$3$2/}${TM_FILENAME_BASE/\\..*//}\" component={${TM_FILENAME_BASE/\\..*//}} />",
            "",
            "",
            "# ${TM_FILENAME_BASE/\\..*//}",
            "",
            "## 概要",
            "",
            "$1${TM_FILENAME_BASE/\\..*//} の概要。",
            "",
            "",
            "## サンプル",
            "",
            "<Preview withSource=\"open\">",
            "    <Story name=\"default\">",
            "        <MemoryRouter>",
            "            <${TM_FILENAME_BASE/\\..*//} />",
            "        </MemoryRouter>",
            "    </Story>",
            "</Preview>",
            "",
            "",
            "## プロパティ",
            "",
            "<Props of={${TM_FILENAME_BASE/\\..*//}} />",
            "",
        ]
    },
    "Preview": {
        "scope": "markdown",
        "prefix": "sb/preview",
        "body": [
            "<Preview withSource=\"open\">",
            "    <Story name=\"${1}\">",
            "        <MemoryRouter>",
            "            <${TM_FILENAME_BASE/\\..*//} />",
            "        </MemoryRouter>",
            "    </Story>",
            "</Preview>",
        ]
    },
    "Text": {
        "scope": "mdx",
        "prefix": "sb/prop/text",
        "body": [
            "${1:name}={knobs.text(\"${1:name}\", \"${2:default value}\")}",
        ]
    },
    "Boolean": {
        "scope": "mdx",
        "prefix": "sb/prop/boolean",
        "body": [
            "${1:name}={knobs.boolean(\"${1:name}\", ${2|true,false|})}",
        ]
    },
    "Number": {
        "scope": "mdx",
        "prefix": "sb/prop/number",
        "body": [
            "${1:name}={knobs.number(\"${1:name}\", ${2:123})}",
        ]
    },
    "Slider": {
        "scope": "mdx",
        "prefix": "sb/prop/slider",
        "body": [
            "${1:name}={knobs.number(\"${1:name}\", ${2:123}, { range: true, min: ${3:0}, max: ${4:255}, step: ${5:1}})}",
        ]
    },
    "Color": {
        "scope": "mdx",
        "prefix": "sb/prop/color",
        "body": [
            "${1:name}={knobs.color(\"${1:name}\", \"${2:#ffffff}\")}",
        ]
    },
    "Object": {
        "scope": "mdx",
        "prefix": "sb/prop/object",
        "body": [
            "${1:name}={knobs.object(\"${1:name}\", ${2:{\\}})}",
        ]
    },
    "Array": {
        "scope": "mdx",
        "prefix": "sb/prop/array",
        "body": [
            "${1:name}={knobs.array(\"${1:name}\", ${2:[]}, \",\")}",
        ]
    },
    "Select box": {
        "scope": "mdx",
        "prefix": "sb/prop/select",
        "body": [
            "${1:name}={knobs.select(\"${1:name}\", ${2:{ Opt1: \"Option 1\", Opt2: \"Option 2\"\\}}, ${3:\"Option 1\"})}",
        ]
    },
    "Radio buttons": {
        "scope": "mdx",
        "prefix": "sb/prop/radios",
        "body": [
            "${1:name}={knobs.radios(\"${1:name}\", ${2:{ Opt1: \"Option 1\", Opt2: \"Option 2\"\\}}, ${3:\"Option 1\"})}",
        ]
    },
    "Options": {
        "scope": "mdx",
        "prefix": "sb/prop/options",
        "body": [
            "${1:name}={knobs.optionsKnob(\"${1:name}\", ${2:{ Opt1: \"Option 1\", Opt2: \"Option 2\"\\}}, ${3:\"Option 1\"}, { display: \"${4|radio,inline-radio,check,inline-check,select,multi-select|}\" })}",
        ]
    },
    "File": {
        "scope": "mdx",
        "prefix": "sb/prop/file",
        "body": [
            "${1:name}={knobs.files(\"${1:name}\", \"${2:.jpg, .png}\", [])}",
        ]
    },
    "Date": {
        "scope": "mdx",
        "prefix": "sb/prop/date",
        "body": [
            "${1:name}={knobs.date(\"${1:name}\", new Date(\"Jan 20 2017\"))}",
        ]
    },
}

```


# MDX の作成
カタログには全ての UI コンポーネントを登録します。

### App コンポーネント
`src/client` フォルダに `App.stories.mdx` ファイルを作成してください。
Ctrl + Space を押して `sb/body` を選択すると下記の内容が自動挿入されます。

```md
import { Meta, Story, Preview, Props } from "@storybook/addon-docs/blocks";
import * as knobs from "@storybook/addon-knobs";
import { action } from "@storybook/addon-actions";
import { linkTo } from "@storybook/addon-links";
import { MemoryRouter } from "react-router-dom";
import App from "./App";


<Meta title="App" component={App} />


# App

## 概要

App の概要。


## サンプル

<Preview withSource="open">
    <Story name="default">
        <MemoryRouter>
            <App />
        </MemoryRouter>
    </Story>
</Preview>


## プロパティ

<Props of={App} />

```

概要を "アプリケーション。" に変更しておきます。

```md
## 概要

アプリケーション。

```


### Home コンポーネント
`src/client/pages` フォルダに `Home.stories.mdx` ファイルを作成してください。
Ctrl + Space を押して `sb/body` を選択すると下記の内容が自動挿入されます。

```md
import { Meta, Story, Preview, Props } from "@storybook/addon-docs/blocks";
import * as knobs from "@storybook/addon-knobs";
import { action } from "@storybook/addon-actions";
import { linkTo } from "@storybook/addon-links";
import { MemoryRouter } from "react-router-dom";
import Home from "./Home";


<Meta title="pages/Home" component={Home} />


# Home

## 概要

Home の概要。


## サンプル

<Preview withSource="open">
    <Story name="default">
        <MemoryRouter>
            <Home />
        </MemoryRouter>
    </Story>
</Preview>


## プロパティ

<Props of={Home} />

```

こちらも概要を "Home ページ。" に変更しておいてください。


### HomeWork コンポーネント
`src/client/pages` フォルダに `HomeWork.stories.mdx` ファイルを作成してください。
Ctrl + Space を押して `sb/body` を選択すると下記の内容が自動挿入されます。

```md
import { Meta, Story, Preview, Props } from "@storybook/addon-docs/blocks";
import * as knobs from "@storybook/addon-knobs";
import { action } from "@storybook/addon-actions";
import { linkTo } from "@storybook/addon-links";
import { MemoryRouter } from "react-router-dom";
import HomeWork from "./HomeWork";


<Meta title="pages/HomeWork" component={HomeWork} />


# HomeWork

## 概要

HomeWork の概要。


## サンプル

<Preview withSource="open">
    <Story name="default">
        <MemoryRouter>
            <HomeWork />
        </MemoryRouter>
    </Story>
</Preview>


## プロパティ

<Props of={HomeWork} />

```

概要を "HomeWork ページ。" に変更しておいてください。


### Message コンポーネント
`src/client/parts` フォルダに `Message.stories.mdx` ファイルを作成してください。
Ctrl + Space を押して `sb/body` を選択すると下記の内容が自動挿入されます。

```md
import { Meta, Story, Preview, Props } from "@storybook/addon-docs/blocks";
import * as knobs from "@storybook/addon-knobs";
import { action } from "@storybook/addon-actions";
import { linkTo } from "@storybook/addon-links";
import { MemoryRouter } from "react-router-dom";
import Message from "./Message";


<Meta title="parts/Message" component={Message} />


# Message

## 概要

Message の概要。


## サンプル

<Preview withSource="open">
    <Story name="default">
        <MemoryRouter>
            <Message />
        </MemoryRouter>
    </Story>
</Preview>


## プロパティ

<Props of={Message} />

```

Message コンポーネントは `message` というプロパティを持っていて、このプロパティに設定されたテキストを UI 表示するようになっています。
このままですと `message` プロパティに何も設定していないため、カタログには何のメッセージも表示されません。
ここでプロパティに適当なテキストを設定すれば、勿論カタログに反映されます。それでも良いのですが、本記事では Knobs というアドオンを利用し、カタログ上で自由にテキストを設定できるよう対応させます。

対応させるには、プロパティを設定する際に `knobs.text` 関数を通して値を渡せば良いのですが、これもコードスニペットを用意してあります。
`<Message />` タグの中にカーソルを移動し、Ctrl + Space を押して `sb/props/text` を選択してください。プロパティ名と初期値を入力する必要がありますので、プロパティ名を入力したら **Tab キーを押して**カーソルを移動させ初期値を入力します。

![ezgif-4-72f05305211a.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/413915/6f7cad56-2400-9c14-200a-c15fd95e1486.gif)

```md
import { Meta, Story, Preview, Props } from "@storybook/addon-docs/blocks";
import * as knobs from "@storybook/addon-knobs";
import { action } from "@storybook/addon-actions";
import { linkTo } from "@storybook/addon-links";
import { MemoryRouter } from "react-router-dom";
import Message from "./Message";


<Meta title="parts/Message" component={Message} />


# Message

## 概要

Message パーツ。


## サンプル

<Preview withSource="open">
    <Story name="default">
        <MemoryRouter>
            <Message message={knobs.text("message", "Hello.")} />
        </MemoryRouter>
    </Story>
</Preview>


## プロパティ

<Props of={Message} />

```


# カタログの閲覧
### Storybook の起動
Storybook を起動するには、Explorer サイドバーの NPM Scripts から、storybook を実行します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/413915/67275a92-01ab-132a-076a-35c1e7809e2a.png)
Storybook も HMR (Hot Module Replacement) で動作するので、常時起動させておいて大丈夫です。

> <注意>
> HomeWork コンポーネントのようにサーバーサイドの API にアクセスするコンポーネントを正常に表示させるために、開発サーバーも起動 (run:hmr) させておく必要があります。

### Storybook にアクセス
ブラウザで `http://localhost:6006` にアクセスすると Storybook が開きます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/413915/a89c2e9a-2c34-16a0-3739-52c8201eb43c.png)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/413915/1f987a34-b7cc-0a73-9e1c-623628027409.png)

Message コンポーネントの `message` プロパティも変更可能になっています。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/413915/b63ead27-a1c6-cfc0-c0f6-737e884c87af.png)

ちなみにプロパティの説明文は JS Doc から自動で読み込まれます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/413915/97d8e669-641c-6528-c614-90dcb69dd4bc.png)


# スナップショットテスト
### テスト実行
スナップショットテストを実行するには、Explorer サイドバーの NPM Scripts から、test-snapshots を実行します。 (予め Storybook を起動しておく必要があります。)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/413915/b2175fb5-979a-cade-a01c-750d380c44db.png)

スナップショットがまだ記録されていない場合は記録が行われます。
test-snapshots を実行しスナップショットを記録したら、試しに `Message.tsx` を下記のように変更し、再び test-snapshots を実行してみてください。

```tsx
import * as React from "react";

interface MessageProps {
    /** メッセージテキスト。 */
    message: string;
}

const Message = (props: MessageProps) => {
    return (
        <h1>Message: {props.message}</h1>
    );
};

export default Message;

```

表示されるテキストが増えたため差分が発生したことが検出されます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/413915/4a908db8-38e2-be06-bf67-40b8fba8b243.png)
差分を記録した画像ファイルへのリンクが一緒に出力されますので、Ctrl を押しながらクリックして画像を開き、差分を確認します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/413915/138ea289-b649-babb-96e2-569cf437fc02.png)
差分が無い部分は白色、差分がある部分は赤色で表示されます。


### スナップショットの更新
全ての差分画像について、意図した通りのものであることを確認できたら、accept-snapshots を実行しスナップショットを更新します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/413915/6235144d-77be-70cf-6edc-c939bae66fee.png)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/413915/60cba887-e99a-c9b1-ee1c-7679dfe32481.png)


# おわりに
如何でしたでしょうか。
一度環境構築してしまえば、後は UI コンポーネント作成と同時に MDX ファイルを用意していくだけで、カタログ化・スナップショットテストが行えます。
どちらも恩恵は非常に大きい上に、本記事のようにコードスニペットを使えば MDX ファイルの作成も非常に簡単に行えます。
プロジェクトの規模が大きくなるにつれ Storybook の重要性は大きくなりますので、是非 Storybook 導入してみてください。

-----
# 変更履歴
* 2020/07/23: `<BrowserRouter>` ではカタログ内で `History` が共有されてしまうため `<MemoryRouter>` に変更。また、`<MemoryRouter>` が常に配置されるようコードスニペットを変更。その他細かな修正。
* 2020/08/20: `package.json` 内の `test` 系スクリプト定義で `NODE_ENV` に `development` ではなくより適切な `test` を設定するよう変更。
* 2020/09/12: `main.ts` を、`webpack.config` の設定を参照する形に変更。