概要
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