概要

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です。

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

Dockerfile

FROM node:10.8.0-stretch

RUN npm install --global @vue/cli

WORKDIR /projects

docker-compose.yml

version: '3'

services:
  app:
    build: .
    ports:
      - "8080:8080"
    volumes:
      - ".:/projects"
    tty: true
> docker-compose up -d
> docker-compose exec app bash

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

コンテナ内

> 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をインストールします。

コンテナ内

> cd app
> yarn add element-ui

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

検証

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

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

src/components/HelloWorld.vue

(略)
@Component
export default class HelloWorld extends Vue {
  @Prop() private msg!: string;

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

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

コンテナ内

> 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コンポーネントの実装を眺めてみます。

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) なるものが、なにやら怪しかったので、実装を見てみます。

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

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

はい。

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

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

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

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();
    });
  }
}
(略)

テストを実行します。

コンテナ内

> 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://cloudpack.media/43084

元記事はこちら

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