Posted at

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

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