Qiita Teams that are logged in
You are not logged in to any team

Log in to Qiita Team
Community
OrganizationEventAdvent CalendarQiitadon (β)
Service
Qiita JobsQiita ZineQiita Blog
2
Help us understand the problem. What are the problem?

More than 3 years have passed since last update.

@kai_kou

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

> 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のドキュメントのことを思い出しました。

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://qiita.com/kai_kou/items/19b494a41023d84bacc7

Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
2
Help us understand the problem. What are the problem?