ゼロからVue.jsでビジュアルリグレッションテストするまでpart1/3

Part1 ここ
Part2 [https://qiita.com/senku/items/20e21033edd512be1d4d](https://qiita.com/senku/private/20e21033edd512be1d4d)
Part3 [https://qiita.com/senku/items/08d547eda2c6ff818108](https://qiita.com/senku/private/08d547eda2c6ff818108)

Vue.jsでビジュアルリグレッションテストをするためにいろいろやったナレッジを公開しまーす。

## やりたいこと

- Vue.jsのコンポーネントのビジュアルリグレッションテスト（画像回帰テスト）をしたい。
- viewsレベルのテストもしたい。（Vuex/Vue Routerが絡む）
- 多言語対応もしたい。（Vue I18nが絡む）
- テストはなるべくローカルで完結したい。サーバ立てたりしたくない。

## Summary

Storybook(ビジュアルの元ネタ) + storycap(画像生成) + reg-suit(画像比較＆レポート) でやる。
Storyを作るために悪戦苦闘する

- Vue.js開発環境の構築
- StorybookでコンポーネントのStoryをつくろう
  - Vue I18nと戦う
  - Vue Routerはかんたん(ここまで)
  - Vuexはモックする
- Storycapで画像を生成
- reg-suitで比較＆レポート

## Vue.jsのセットアップ

ゼロからやりますと言ったものの、みんな流石にNode.jsは持ってるよね。

### Vue CLI いれる

公式の[インストール](https://cli.vuejs.org/guide/installation.html)に従います。

```bash
$ npm install -g @vue/cli
(略)
+ @vue/cli@4.1.1
```

```bash
$ vue --version
@vue/cli 4.1.1
```

Vue CLI 4だ！

### プロジェクトをつくる

公式の[プロジェクトの作成](https://cli.vuejs.org/guide/creating-a-project.html#vue-create)に従う。はろーわーるど！

```bash
$ vue create hello-world
```

とりあえずデフォルトでいきましょう。babelとESLintだけ。

```plaintext
? Please pick a preset: (Use arrow keys)
❯ default (babel, eslint)
```

言われるがままに`cd`して`npm run serve`します。

```bash
$ cd hello-world
$ npm run serve
```

http://localhost:8080 ではろーわーるどできましたね。

## Storybook導入

### Storybookセットアップ

[Storybook for Vue](https://github.com/storybookjs/storybook/tree/next/app/vue)は見なかったことにして、[Vue CLI plugin for Storybook](https://github.com/storybookjs/vue-cli-plugin-storybook)の手順で入れます。Vue CLI Pluginとしてセットアップしないと`vue.config.js`を参照してくれないので後で詰みます(１敗)。Vue CLIに逆らってはならぬ。

```bash
$ vue add storybook
```

`Initial Framework`には無言でEnterを押します。

インストール後、言われるがままに`npm run storybook:serve`します。

```bash
$ npm run storybook:serve
```

自動でブラウザが起動して、[http://localhost:6006/](http://localhost:6006/)でStorybookが表示されますね。

かなり後で使うので、ブラウザが起動しないコマンドもここで作っておきましょう。`--ci`オプションが使えます。
オプションにはStorybookの[CLI Option](https://storybook.js.org/docs/configurations/cli-options/)がそのまま渡せるので覚えておくといつか役に立つかもしれません。

```diff
diff --git a/package.json b/package.json
index dfba14f..533efd0 100644
--- a/package.json
+++ b/package.json
@@ -7,7 +7,8 @@
     "build": "vue-cli-service build",
     "lint": "vue-cli-service lint",
     "storybook:build": "vue-cli-service storybook:build -c config/storybook",
-    "storybook:serve": "vue-cli-service storybook:serve -p 6006 -c config/storybook"
+    "storybook:serve": "vue-cli-service storybook:serve -p 6006 -c config/storybook",
+    "storybook:ci": "vue-cli-service storybook:serve -p 6006 -c config/storybook --ci"
   },
   "dependencies": {
     "core-js": "^3.4.3",
```

### HelloWorldコンポーネントのStoryを書いてみる

Storyの書き方はいろいろありますが、とりあえずこんな感じで作ってみます。

```js:src/stories/HelloWorld.stories.js
import { storiesOf } from "@storybook/vue";
import HelloWorld from "@/components/HelloWorld.vue";

storiesOf("HelloWorld", module)
  .add("test", () => {
  return {
    components: { HelloWorld },
    template: `
    <hello-world msg="from storybook" />
    `
  };
});
```

Storybookに表示されますね。`msg`で渡した`from storybook`もちゃんと表示されています。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/543568/10440028-3f4a-3d97-6a10-b6f066146912.png)

ここから先しばらくは、Storybookと他のプラグインとの兼ね合いの話をします。

## Vue I18nとのたたかい

Storybook で [Vue-I18n](https://kazupon.github.io/vue-i18n/)を使う場合はちょっと細工が必要です。

まずは vue-i18nをセットアップします。公式の[Installation](https://kazupon.github.io/vue-i18n/installation.html)参照。
Vue CLI 3.x向けの手順ですがたぶん問題ありません。

```bash
$ vue add i18n
```

とりあえず全部デフォルトで。基本のロケールが`en`になります。

```plaintext
? The locale of project localization. en
? The fallback locale of project localization. en
? The directory where store localization messages of project. It's stored under `src` directory. locales
? Enable locale messages in Single file components ? No
```

`src/main.js`から`src/i18n.js`がロードされるようになりました。
localeのファイルが`src/locales/en.json`にできています。

```json:src/locales/en.json
{
  "message": "hello i18n !!"
}
```

ちょっと`src/components/HelloWorld.vue`から読み込ませてみます。

```diff:src/components/HelloWorld.vue
diff --git a/src/components/HelloWorld.vue b/src/components/HelloWorld.vue
index 879051a..e77721a 100644
--- a/src/components/HelloWorld.vue
+++ b/src/components/HelloWorld.vue
@@ -1,6 +1,7 @@
 <template>
   <div class="hello">
     <h1>{{ msg }}</h1>
+    <p>{{ $t('message') }}</p>
     <p>
       For a guide and recipes on how to configure / customize this project,<br>
       check out the
```

`npm run serve` して http://localhost:8080/ を見てみます。
`hello i18n !!` が表示されてますね。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/543568/0041c2a9-939d-2607-a425-e41a85d5149f.png)

多言語の対応はここでは置いておいて、とりあえず`en`で話を進めます。

### Vue I18n × Storybook

StorybookでHelloWorldのStoryを見てみましょう。`npm run storybook:serve`で再ロードさせます。

残念なことにエラーが出ます。Vue I18nがセットするプロパティの`$t`がないからですね。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/543568/0f39b3b2-cbfa-d4e1-655d-6f12e59095c6.png)

Storybookの起動は `src/main.js` を経由していないため、別途コンポーネントにVueI18nを紐付けてやる必要があります。

やり方はいろいろあるんですが、とりあえずデコレータで解決しています。`Vue.use`はいらない。

```js:src/stories/defaultDecorator.js
import i18n from "@/i18n";

export default function defaultDecorator() {
  return {
    template: "<div><story /></div>",
    i18n
  };
}
```

```diff:src/stories/HelloWorld.stories.js
diff --git a/src/stories/HelloWorld.stories.js b/src/stories/HelloWorld.stories.js
index 93b4b7e..d8e8605 100644
--- a/src/stories/HelloWorld.stories.js
+++ b/src/stories/HelloWorld.stories.js
@@ -1,7 +1,9 @@
 import { storiesOf } from "@storybook/vue";
+import defaultDecorator from "@/stories/defaultDecorator";
 import HelloWorld from "@/components/HelloWorld.vue";

 storiesOf("HelloWorld", module)
+  .addDecorator(defaultDecorator)
   .add("test", () => {
   return {
     components: { HelloWorld },
```

エラーが解消して、`hello i18n !!`が表示されます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/543568/b8d88a75-dfed-4cad-83e7-0bdcaba83a74.png)


## vue-i18n-loaderとのたたかい

Vue I18nには、SFCの中に定義を書けるvue-i18n-loaderがあります。これを使う場合は更にひと手間が必要です。

とりあえず`vue invoke i18n`で、プラグインの設定をvue-i18n-loaderが使えるように更新します。

```bash
$ vue invoke i18n
```

```plaintext
? The locale of project localization. en
? The fallback locale of project localization. en
? The directory where store localization messages of project. It's stored under `src` directory. locales
? Enable locale messages in Single file components ? Yes // ここだけy！
```

`vue-i18n-loader`が追加され、`vue.config.js`の`enableInSFC`がtrueになり、`src/components/HelloI18n.vue`が作成されます。

`App.vue`から読めるようにしましょう。

```diff:src/App.vue
diff --git a/src/App.vue b/src/App.vue
index fcc5662..0660f5b 100644
--- a/src/App.vue
+++ b/src/App.vue
@@ -1,16 +1,19 @@
 <template>
   <div id="app">
     <img alt="Vue logo" src="./assets/logo.png">
+    <HelloI18n/>
     <HelloWorld msg="Welcome to Your Vue.js App"/>
   </div>
 </template>

 <script>
+import HelloI18n from './components/HelloI18n.vue'
 import HelloWorld from './components/HelloWorld.vue'

 export default {
   name: 'app',
   components: {
+    HelloI18n,
     HelloWorld
   }
 }
```

`npm run serve`を再実行すると、`Hello i18n in SFC!`が表示されます。（画像はテキスト選択しちゃった）

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/543568/82e498d7-add4-2f47-a3ed-33c7063d7f11.png)

### vue-i18n-loader × Storybook

`src/components/HelloI18n.vue`のStoryを作ります。

```js:src/stories/HelloI18n.stories.js
import { storiesOf } from "@storybook/vue";
import defaultDecorator from "@/stories/defaultDecorator";
import HelloI18n from "@/components/HelloI18n.vue";

storiesOf("HelloI18n", module)
  .addDecorator(defaultDecorator)
  .add("test", () => {
  return {
    components: { HelloI18n },
    template: `
    <hello-i18n />
    `
  };
});
```

この時点ではStorybookは正しく動作しません。keyである`hello`がそのまま表示されてしまいます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/543568/91d56c82-55cb-8709-2afd-19811a97f019.png)

Storybookとvue-i18n-loaderの間には複雑な問題があります。

[いろいろ](https://github.com/storybookjs/storybook/issues/1573)と見守ってはいるんですが、decoratorで`this.$root._i18n`をセットするのが現状可能な解決策の一つです。（via. [dlucian/vuejs-storybook-i18n #1](https://github.com/dlucian/vuejs-storybook-i18n/issues/1)）ただし強引な解決策なので、いつ動かなくなるかわかんないです。

```diff:src/stories/defaultDecorator.js
diff --git a/src/stories/defaultDecorator.js b/src/stories/defaultDecorator.js
index 4e5acdf..c50bf20 100644
--- a/src/stories/defaultDecorator.js
+++ b/src/stories/defaultDecorator.js
@@ -3,6 +3,9 @@ import i18n from "@/i18n";
 export default function defaultDecorator() {
   return {
     template: "<div><story /></div>",
-    i18n
+    i18n,
+    beforeCreate: function() {
+      this.$root._i18n = this.$i18n;
+    }
   };
 }
```

`Hello i18n in SFC!`が表示されるようになりました。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/543568/71dc11ba-7ff4-4503-cdd9-fa64cf24cbb3.png)

## Vue Routerとのたたかい

Routerを入れ忘れていたので入れます。

```bash
$ vue add router
```

`Use history mode for router? (Requires proper server setup for index fallback in production)` には無言でEnter（`Yes`）を押していきます。

Vue Routerをインストールすると、`src/App.vue`から`src/views/About.vue`と`src/views/Home.vue`へのリンクが生成されます。というかファイルごと上書きされます。さようならHelloI18n。

`src/App.vue`のStoryを書いてみます。

```src/stories/App.stories.js
import { storiesOf } from "@storybook/vue";
import defaultDecorator from "@/stories/defaultDecorator";
import App from "@/App.vue";

storiesOf("App", module)
  .addDecorator(defaultDecorator)
  .add("test", () => {
  return {
    components: { App },
    template: `
    <app />
    `
  };
});
```

StorybookでこのStoryを表示するとエラーが起こるので、decoratorで解決します。
Storybookでページ遷移させる予定はないので、ルーティング情報は空にしておきます。すまんな。

```diff:src/stories/defaultDecorator.js
diff --git a/src/stories/defaultDecorator.js b/src/stories/defaultDecorator.js
index c50bf20..746aef7 100644
--- a/src/stories/defaultDecorator.js
+++ b/src/stories/defaultDecorator.js
@@ -1,8 +1,10 @@
+import Router from 'vue-router'
 import i18n from "@/i18n";

 export default function defaultDecorator() {
   return {
     template: "<div><story /></div>",
+    router: new Router({}),
     i18n,
     beforeCreate: function() {
       this.$root._i18n = this.$i18n;
```

`config/storybook/config.js`で`Vue.use`しておく必要もあります。

```diff:config/storybook/config.js
diff --git a/config/storybook/config.js b/config/storybook/config.js
index fe20bab..d8dcd31 100644
--- a/config/storybook/config.js
+++ b/config/storybook/config.js
@@ -1,5 +1,9 @@
 /* eslint-disable import/no-extraneous-dependencies */
 import { configure } from '@storybook/vue'
+import Vue from 'vue'
+import Router from 'vue-router'
+
+Vue.use(Router)

 const req = require.context('../../src/stories', true, /.stories.js$/)
```

ここまでやれば、Vue Routerを使うコンポーネントのStoryが表示できるようになります。
リンクを押しても何も起こりません。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/543568/7317756b-1a96-284c-ad3f-5f011e4e4caf.png)

[つづく](https://qiita.com/senku/private/20e21033edd512be1d4d)