Storybookをメンテナンスする（バージョンアップ〜簡易テスト追加）

# 概要
以前プロジェクトにStorybookを導入しました。
その後、ある程度規模の大きい開発が入ったためコンポーネントが追加されましたが、Storybookの対応ができていませんでした。
今回まとめてテスト追加等含めてメンテナンスしてみます。

Storybook関連の記事はこちら↓です。

https://qiita.com/kotobuki5991/items/dcf411789b359d99f82a

https://qiita.com/kotobuki5991/items/3a0ea99d4ef0bff22939

# バージョン更新

まずは新しいバージョンが出ていたので、バージョン更新します。

```
$ npx sb upgrade
```
※マイグレーションについて聞かれるのでyと答える

# 自動テスト追加

## ひとまずのゴール

- stories.mdxがcromium環境で正常に開けるかチェック（破損していないか）

上記はだけでは、npm run storybobk実行後、ブラウザで確認できるコンポーネントしかテスト対象になりません。
つまり、必要なstoriesファイルが不足しており、そもそもブラウザで確認できない場合はテスト対象外となります。
そこで以下の対応も追加します。

- atomsのvueコンポーネントファイルに対応するstories.mdxが全て存在するかチェック

## 自動テストの候補

### StoryShots

以前よく使われていたようなので使おうと思いましたが、非推奨になるので移行ガイドに従ってみます。

https://storybook.js.org/docs/writing-tests/storyshots-migration-guide

マイグレーション先は２種類。

### 1. Portable Stories

こちらは情報が少ないため2のtest-runnerを使う。

https://storybook.js.org/docs/writing-tests/storyshots-migration-guide#with-portable-stories

### 2. test-runner（情報が多いためこちらを使う）

こちらを使用すると実際にchromeなどのブラウザで、Storyが正常に開けるか等のテストが行えます。

> Storybook test-runner は、あなたのストーリーをすべて実行可能なテストに変換します。JestとPlaywrightを搭載しています。スタンドアロンでフレームワークに依存しないユーティリティで、Storybookと並行して動作します。play関数によるインタラクションテスト、DOMスナップショット、アクセシビリティテストなど、複数のテストパターンをマルチブラウザ環境で実行できます。

https://storybook.js.org/docs/writing-tests/test-runner#setup

https://zenn.dev/sa2knight/books/storybook-7-with-vue-3/viewer/test-runner

#### 導入
```
$ npm install @storybook/test-runner --save-dev
```
#### 設定
package.jsonに以下を追加
```package.json
"test-storybook": "test-storybook --url http://localhost:6006",
```

stories.mdxを使用している場合は、上記のままでは以下のようにテスト対象ファイルを見つけられない場合があります。

```
$ npm run storybook-test                       

> client_vue@0.1.0 storybook-test
> test-storybook --url http://localhost:6006

No tests found, exiting with code 1
Run with `--passWithNoTests` to exit with code 0
In /your/project/path
  485 files checked.
  testMatch: /your/project/path/stories/**/*.stories.mdx, /your/project/path/stories/**/*.stories.@(js|jsx|ts|tsx) - 0 matches
  testPathIgnorePatterns: /node_modules/ - 485 matches
  testRegex:  - 0 matches
Pattern:  - 0 matches
```

そこで、--stories-jsonを追加しておきます。

```package.json
"test-storybook": "test-storybook --stories-json --url http://localhost:6006",
```
さらに.storybook/main.jsに以下を追加します。
```.storybook/main.js
module.exports = {
	// 省略
	features: {
	    buildStoriesJson: true　// これを追加
	  },
	// 省略
}
```

#### テスト実行

まずstroybookを起動させてからテストを実施します。

```
$ npm run storybook
$ npm run test-storybook
```

テスト実行時に以下のようなエラーが発生した場合、
```
Error: Executable doesn't exist at /Users/yourname/Library/Caches/ms-playwright/chromium-1091/chrome-mac/Chromium.app/Contents/MacOS/Chromium
╔═════════════════════════════════════════════════════════════════════════╗
║ Looks like Playwright Test or Playwright was just installed or updated. ║
║ Please run the following command to download new browsers:              ║
║                                                                         ║
║     npx playwright install                                              ║
║                                                                         ║
║ <3 Playwright Team                                                      ║
╚═════════════════════════════════════════════════════════════════════════╝ Failed to launch browser.
```

必要に応じて以下のコマンドを実行し、最新のchromiumをインストール
```
$ npx playwright install
```

再度テスト実行

```
$ npm run test-storybook

> client_vue@0.1.0 test-storybook
> test-storybook --stories-json --url http://localhost:6006

 PASS   browser: chromium  ../../../../../../../private/var/folders/pb/9tvfmpzd5gx6lzfb0yppqs3w0000gp/T/06b3322848fb97ab5618598b36fe0209/components-atoms-vkselectbtn.test.js
 PASS   browser: chromium  ../../../../../../../private/var/folders/pb/9tvfmpzd5gx6lzfb0yppqs3w0000gp/T/06b3322848fb97ab5618598b36fe0209/components-atoms-vkbtn.test.js
 PASS   browser: chromium  ../../../../../../../private/var/folders/pb/9tvfmpzd5gx6lzfb0yppqs3w0000gp/T/06b3322848fb97ab5618598b36fe0209/components-atoms-vkstickyfooter.test.js

~~~~~~~~~~~~~~~~~~~~~~~~~~~
        省略
~~~~~~~~~~~~~~~~~~~~~~~~~~~


Test Suites: 53 passed, 53 total
Tests:       74 passed, 74 total
Snapshots:   0 total
Time:        11.905 s
Ran all test suites.
[test-storybook] Cleaning up /private/var/folders/pb/9tvfmpzd5gx6lzfb0yppqs3w0000gp/T/06b3322848fb97ab5618598b36fe0209
```

### 対象stories.mdxが全て存在するかチェックするスクリプトを追加

以下のスクリプトを追加します。

```package.json
"test-all-storybook": "chmod +x .storybook/scripts/check-stories-exist && .storybook/scripts/check-stories-exist",
```

次に、/.storybook/scripts/check-stories-existを追加します。
```sh
#!/bin/bash

# このスクリプトは、client_vueディレクトリで
# run test-all-storybookによって実行してください。

# ANSIエスケープコードを変数に格納
GREEN='\033[0;32m'
RED='\033[0;31m'
NC='\033[0m' # No Color

# atomsに存在するコンポーネントのファイル名を取得
atoms_file_names=()
for file in $PWD/src/components/atoms/*.vue; do
    filename=$(basename -- "$file")
    atoms_file_names+=("${filename%.vue}")
done

# atomsに存在するstoryファイル名を取得
stories_file_names=()
for file in $PWD/stories/components/atoms/*.stories.mdx; do
    filename=$(basename -- "$file")
    stories_file_names+=("${filename%.stories.mdx}")
done


missing_stories_names=($(comm -23 <(printf "%s\n" "${atoms_file_names[@]}" | sort) <(printf "%s\n" "${stories_file_names[@]}" | sort)))
missing_stories_file_names=("${missing_stories_names[@]/%/.stories.mdx}")
wrong_stories_file_names=($(comm -13 <(printf "%s.stories.mdx\n" "${atoms_file_names[@]}" | sort) <(printf "%s.stories.mdx\n" "${stories_file_names[@]}" | sort)))

is_pass=false
is_pass_missing_file_test=false
is_pass_wrong_file_test=false

if [ ${#missing_stories_file_names[@]} -eq 0 ]; then
    echo -e "${GREEN}PASS: 不足stories.mdxファイルはありません${NC}"
    is_pass_missing_file_test=true
else
    echo -e "${RED}ERROR:stories.mdxが不足しています、以下のファイルを自動生成します。${NC}"
    for file in "${missing_stories_file_names[@]}"; do
        echo -e "   ${RED}./stories/components/atoms/${file}${NC}"
    done
    is_pass_missing_file_test=false
fi

if [ ${#wrong_stories_file_names[@]} -eq 0 ]; then
    echo -e "${GREEN}PASS: 不要なstories.mdxファイルはありません${NC}"
    is_pass_wrong_file_test=true
else
    echo -e "${RED}ERROR: 以下のstories.mdxのファイル名が間違っている、または不要です。削除・修正してください。${NC}"
    for file in "${wrong_stories_file_names[@]}"; do
        echo -e "   ${RED}./stories/components/atoms/${file}${NC}"
    done
    is_pass_wrong_file_test=false
fi

if [ "$is_pass_wrong_file_test" = true ] && [ "$is_pass_missing_file_test" = true ]; then
    is_pass=true
fi

if $is_pass; then
    npm run test-storybook
else
    # 足りないstoriesファイルを自動生成する
    for file_name in ${missing_stories_names[@]}; do
        $(grep -q "slot" ${PWD}/src/components/atoms/${file_name}.vue)
        has_slot=$?
        if [ ${has_slot} -eq 0 ]; then
            has_slot="y"
        else
            has_slot="n"
        fi
        expect -c "
            set timeout 5
            spawn npm run hygen:stories
            expect \"コンポーネントの名前を指定してください\"
            send \"${file_name}\n\"

            expect \"storyを生成する対象のコンポーネントが配置されているディレクトリの、src以下のパスを指定してください。\"
            send \"components\/atoms\n\"

            expect "Slotsは持ちますか?"
            send \"${has_slot}\n\"

            interact
        "
    done
fi
```

#### テスト実施
テストの結果、不足しているstories.mdxファイルがある場合以下のようになります。

さらに、以下の記事で設定したstories.mdxファイルの自動生成スクリプトが実行され、
不足ファイルが生成されます。

https://qiita.com/kotobuki5991/items/3a0ea99d4ef0bff22939#storiesmdx%E3%83%95%E3%82%A1%E3%82%A4%E3%83%AB%E3%81%AE%E9%9B%9B%E5%BD%A2%E3%82%92%E8%87%AA%E5%8B%95%E7%94%9F%E6%88%90%E3%81%97%E3%81%9F%E3%81%84

```
$ npm run test-all-storybook

> client_vue@0.1.0 test-all-storybook
> chmod +x .storybook/scripts/check-stories-exist && .storybook/scripts/check-stories-exist

ERROR: stories.mdxが不足しています、以下のファイルを自動生成します。
   VkAudioTag.stories.mdx
   VkCaution.stories.mdx
   VkLoading.stories.mdx
   VkSecondaryBtn.stories.mdx
   VkSpacer.stories.mdx
   VkSubTitle.stories.mdx
PASS: 不要なstories.mdxファイルはありません
```

成功した場合は、test-runnnerまで実行されます。
```
npm run test-all-storybook

> client_vue@0.1.0 test-all-storybook
> chmod +x .storybook/scripts/check-stories-exist && .storybook/scripts/check-stories-exist

PASS: 不足stories.mdxファイルはありません
PASS: 不要なstories.mdxファイルはありません

> client_vue@0.1.0 test-storybook
> test-storybook --stories-json --url http://localhost:6006

 PASS   browser: chromium  ../../../../../../../private/var/folders/pb/9tvfmpzd5gx6lzfb0yppqs3w0000gp/T/f6cf69158ba11254bc13750363cde930/components-atoms-vkstepprogressarrow.test.js
 PASS   browser: chromium  ../../../../../../../private/var/folders/pb/9tvfmpzd5gx6lzfb0yppqs3w0000gp/T/f6cf69158ba11254bc13750363cde930/components-atoms-vkstickyfooter.test.js
 PASS   browser: chromium  ../../../../../../../private/var/folders/pb/9tvfmpzd5gx6lzfb0yppqs3w0000gp/T/f6cf69158ba11254bc13750363cde930/components-atoms-searchresultnotfound.test.js
 PASS   browser: chromium  ../../../../../../../private/var/folders/pb/9tvfmpzd5gx6lzfb0yppqs3w0000gp/T/f6cf69158ba11254bc13750363cde930/components-atoms-vksmallcheckboxarray.test.js

~~~~~~~~~~~~~~~~~~~
~~~~~~~~~~~~~~~~~~~

Test Suites: 58 passed, 58 total
Tests:       82 passed, 82 total
Snapshots:   0 total
Time:        31.387 s
Ran all test suites.
```

# まとめ

新機能等の開発で忙しくなると、Storybookのメンテナンスがどうしても後回しになってしまうことが分かってきました。
更新忘れがあると後々負債になってしまうので、少しずつメンテナビリティが高くなるように改善をしていきたいですね。運用は難しい...