Vue.js+TypeScriptでElement-ui利用時に単体テストが完了しなくて悩んだ話

## 概要

Vue.js+TypeScriptでElement-ui利用なプロジェクトで単体テストを書いてたらテストが完了せずにハマったので覚書です。

Elementについては下記をご参考ください。

Element
http://element.eleme.io/#/en-US

Vue.jsのコンポーネント詰め合わせ「Element」がスゴかった
https://s8a.jp/vue-js-library-element


すべてを調べてないので、あれですが、ElementのLoadingコンポーネントをServiceとして利用する場合、```close``` メソッド内で、```setTimeout``` を利用しているので```nextTick``` を忘れないようにしましょう(結論)
http://element.eleme.io/#/en-US/component/loading


Githubに検証で利用したプロジェクトをUPしています。よければご参考ください。
https://github.com/kai-kou/vue-js-typescript-element-ui-unit-test


## 準備

Vue-Cliで環境を作ります。
ここではDockerを利用して環境構築していますが、ローカルで構築してもらってもOKです。

```sh
> mkdir 任意のディレクトリ
> cd 任意のディレクトリ
> vi Dockerfile
> vi docker-compose.yml
```

```txt:Dockerfile
FROM node:10.8.0-stretch

RUN npm install --global @vue/cli

WORKDIR /projects
```

```yaml:docker-compose.yml
version: '3'

services:
  app:
    build: .
    ports:
      - "8080:8080"
    volumes:
      - ".:/projects"
    tty: true
```

```sh
> docker-compose up -d
> docker-compose exec app bash
```

単体テストも書くので、Unit Testを忘れずに。

```sh:コンテナ内
> vue create app

Vue CLI v3.0.1
? Please pick a preset: Manually select features
? Check the features needed for your project: Babel, TS, Linter, Unit
? Use class-style component syntax? Yes
? Use Babel alongside TypeScript for auto-detected polyfills? Yes
? Pick a linter / formatter config: TSLint
? Pick additional lint features: Lint on save
? Pick a unit testing solution: Mocha
? Where do you prefer placing config for Babel, PostCSS, ESLint, etc.? In dedicated config files
? Save this as a preset for future projects? No
? Pick the package manager to use when installing dependencies: (Use arrow keys)
❯ Use Yarn
  Use NPM
```

プロジェクトが作成できたら、Elementをインストールします。

```sh:コンテナ内
> cd app
> yarn add element-ui
```

これで環境が整いました。


## 検証

プロジェクト作成すると最初からあるHelloWorldコンポーネントでElementのLoadingコンポーネントを利用します。

画像をクリックするとLoadingが表示されるようにします。
ただ、検証のため、ブラウザではほぼ視認できないかと思います^^

```vue:src/components/HelloWorld.vue
(略)
@Component
export default class HelloWorld extends Vue {
  @Prop() private msg!: string;

  private mounted() {
    const loadingInstance = Loading.service({});
    loadingInstance.close();
  }
}
(略)
```

それでは、単体テストを実行してみます。

```sh:コンテナ内
> yarn test:unit

 MOCHA  Testing...

  HelloWorld.vue
    ✓ renders props.msg when passed (179ms)
    1) renders props.msg when passed

  1 passing (2s)
  1 failing

  1) HelloWorld.vue
       renders props.msg when passed:
     Uncaught TypeError: Cannot read property 'split' of undefined
      at getTransitionInfo (dist/webpack:/node_modules/vue/dist/vue.runtime.esm.js:7003:1)
      at whenTransitionEnds (dist/webpack:/node_modules/vue/dist/vue.runtime.esm.js:6973:1)
      at Timeout._onTimeout (dist/webpack:/node_modules/vue/dist/vue.runtime.esm.js:7201:1)

 MOCHA  Tests completed with 1 failure(s)
```

はい。

テストはパスしているものの、エラーが発生しています。エラーについても、
```
renders props.msg when passed:
     Uncaught TypeError: Cannot read property 'split' of undefined
``` 

とあり、なんのことかよくわかりません。


## 原因

とりあえず、ElementのLoadingコンポーネントの実装を眺めてみます。

```js:element-ui/lib/loading.js(抜粋)
LoadingConstructor.prototype.close = function () {
  var _this = this;

  if (this.fullscreen) {
    fullscreenLoading = undefined;
  }
  (0, _afterLeave2.default)(this, function (_) {
    var target = _this.fullscreen || _this.body ? document.body : _this.target;
    (0, _dom.removeClass)(target, 'el-loading-parent--relative');
    (0, _dom.removeClass)(target, 'el-loading-parent--hidden');
    if (_this.$el && _this.$el.parentNode) {
      _this.$el.parentNode.removeChild(_this.$el);
    }
    _this.$destroy();
  }, 300);
  this.visible = false;
};
```

```(0, _afterLeave2.default)``` なるものが、なにやら怪しかったので、実装を見てみます。

```js:element-ui/src/utils/after-leave.js(抜粋)
export default function(instance, callback, speed = 300, once = false) {
  if (!instance || !callback) throw new Error('instance & callback is required');
  let called = false;
  const afterLeaveCallback = function() {
    if (called) return;
    called = true;
    if (callback) {
      callback.apply(null, arguments);
    }
  };
  if (once) {
    instance.$once('after-leave', afterLeaveCallback);
  } else {
    instance.$on('after-leave', afterLeaveCallback);
  }
  setTimeout(() => {
    afterLeaveCallback();
  }, speed + 100);
};

```

oh。```setTimeout``` ですね。

ここで、Elementのドキュメントのことを思い出しました。

http://element.eleme.io/#/en-US/component/loading

``` ts
let loadingInstance = Loading.service(options);
this.$nextTick(() => { // Loading should be closed asynchronously
  loadingInstance.close();
});

```

はい。

```this.$nextTick``` を忘れていました(白目

```Loading should be closed asynchronously(ロードは非同期で終了する必要があります)``` とありますし。


では、HelloWorldコンポーネントを修正します。


```vue:src/components/HelloWorld.vue
(略)
@Component
export default class HelloWorld extends Vue {
  @Prop() private msg!: string;

  private mounted() {
    const loadingInstance = Loading.service({});
    this.$nextTick(() => {
        loadingInstance.close();
    });
  }
}
(略)
```

テストを実行します。

```sh:コンテナ内
> yarn test:unit

 MOCHA  Testing...

  HelloWorld.vue
    ✓ renders props.msg when passed (535ms)

  1 passing (633ms)

 MOCHA  Tests completed successfully
```

はい。
無事にエラーが発生しなくなりました。

ハマっていた当初はVuexやaxiosも利用していたので、原因はどこだーとあれこれ探って見つけられず仕舞いでしたが、こうやってシンプルな実装で検証すると見つかるものですね。教訓。


## 参考

Element
http://element.eleme.io/#/en-US

Vue.jsのコンポーネント詰め合わせ「Element」がスゴかった
https://s8a.jp/vue-js-library-element

Element - Loading
http://element.eleme.io/#/en-US/component/loading


Vue.js+TypeScriptで開発するときの参考記事まとめ
https://qiita.com/kai_kou/items/19b494a41023d84bacc7