5

I'm struggling to understand some basic concepts of unit testing in Vue.js using Karma, Mocha and Chai.

This is my component:

VueExample.vue

<template>
    <div>
        <p>{{ name }}</p>
        <input v-model="name">
    </div>
</template>

<script>
    export default {
        name: 'VueExample',
        data () {
            return {
                name: 'Bruce Lee'
            };
        }
    }
</script>

This is how I'm trying to test it:

VueExample.spec.js

import Vue from 'vue';
import VueExample from "../../../components/VueExample";

describe('VueExample.vue', () => {
    let vm;

    beforeEach(() => {
        const Constructor = Vue.extend(VueExample);
        vm = new Constructor().$mount();
    });

    it('should change the name', done => {
        const input = vm.$el.querySelector('input');
        input.value = 'Chuck Norris';
        expect(vm.$el.querySelector('p').textContent).to.equal('Bruce Lee');
        Vue.nextTick(() =>{
            expect(vm.$el.querySelector('p').textContent).to.equal('Chuck Norris');
            console.log(vm.$el.querySelector('p').textContent);
            done();
        });
    });
});

I use Karma to run the tests and Chai as assertion library. Everything is properly configured in karma.conf.js. When I run this test, it fails. The text inside the tag <p> doesn't change. The console.log command outputs Bruce Lee.

The tests are run with Firefox.

cezar
  • 11,616
  • 6
  • 48
  • 84
  • 1
    `v-model` [relies on the `input` event](https://vuejs.org/v2/guide/components.html#Using-v-model-on-Components), which does not occur simply by editing the input's value in JavaScript (this is true [outside of Vue](https://codepen.io/tony19/pen/XWJLVvE)). Dispatching the `input` event manually in your test should work. In your example, do `input.dispatchEvent(new Event('input'))` after setting `input.value`. – tony19 Jan 31 '20 at 10:48
  • @tony19 Thank you very much for the comment. I'd highly appreciate if you can resubmit it as answer. – cezar Jan 31 '20 at 11:08
  • No problem. I've submitted it as an answer. – tony19 Feb 01 '20 at 01:27

3 Answers3

3

v-model relies on the input event, which does not occur simply by editing the input's value in JavaScript. This is true outside of Vue, as seen below:

function logInput(e) {
  console.log('input', e.target.value)
}

function update() {
  const input = document.querySelector('#input1')
  input.value = "foo"
}
<input oninput="logInput(event)" id="input1">
<button onclick="update()">Update textbox</button>
<div>Clicking button changes text but does not emit <code>input</code> event. See console.</div>

Dispatching the input event manually in your test should work. In your example, do input.dispatchEvent(new Event('input')) after setting input.value:

const input = vm.$el.querySelector('input');
input.value = 'Chuck Norris';
input.dispatchEvent(new Event('input')); // input event required to update v-model
tony19
  • 125,647
  • 18
  • 229
  • 307
  • Thank you! Succinct and very clear answer. The link to the official documentation helped a lot too. It helps to really understand what's going on in the code example. – cezar Feb 05 '20 at 11:09
1

Change the value of input don't trigger Vue to update the model (Because input's properties aren't reactive).'

You can try it in your browser. Run in the console document.getElementsByTagName('input')[0].value = 'Chuck Norris' And nothing happens, the text of the p element is still "Bruce Lee".

The way to trigger Vue is by input event. so you should dispatch an input event. this can be someting like this:

let event = document.createEvent('HTMLEvents')
event.initEvent('input', true, true)
vm.$el.querySelector('input').dispatchEvent(event)
Shalom Peles
  • 2,285
  • 8
  • 21
  • I'm still in the dark. I get the idea about the [event](https://developer.mozilla.org/en-US/docs/Web/API/Event), but where and how do I change the model value? – cezar Jan 29 '20 at 16:30
  • 1
    Please try vue-test-utils. It will be much simplier to test such scenarios. – Anatoly Jan 31 '20 at 18:35
  • cezar, it's as @tony19 shows - set the value then dispatch the event. If you use vue-test-utils, the equivalent is `.trigger()` but I'm not sure if it fits with Karma and Mocha so I'd stick with `input.dispatchEvent()` which is native and compatible with any testing framework. – Richard Matsen Feb 05 '20 at 03:16
  • @Shalom Peles Your answer points right where the problem is and which approach should be taken. However the code example isn't so good and uses the deprecated method `initEvent`. Therefore I just upvoted this one, and gave the bounty to the other answer, which is more elaborate. – cezar Feb 05 '20 at 11:12
  • @Anatoly Thanks, very probably I'll take advantage of vue-test-utils in the future. For now I needed to learn about this issue using Karma and Mocha. – cezar Feb 05 '20 at 11:14
1

In one of the comments @Anatoly suggested to use the Vue Test Utils. I was playing around and came up with a solution I'd like to share:

yarn add -D @vue/test-utils

or:

npm install --save-dev @vue/test-utils

Then the test file looks like this:

import Vue from 'vue';
import { shallowMount } from '@vue/test-utils';
import VueExample from "../../../components/VueExample";

describe('VueExample.vue', () => {
  let wrapper;

  beforeEach(() => {
    wrapper = shallowMount(VueExample);
  });

  it('should change the name', done => {
    const textInput = wrapper.find('input');
    textInput.setValue('Chuck Norris');
    textInput.trigger('input');
    expect(wrapper.find('p').text()).to.equal('Bruce Lee');
    Vue.nextTick(() => {
      expect(wrapper.find('p').text()).to.equal('Chuck Norris');
      done();
    });
  });
});

There is only one test in this example, but I still use beforeEach to make it easier expandable with further tests. Here we mount the Vue component VueExample. In the test we find the <input> tag, set its value to Chuck Norris and trigger the input event. We can see that the text node of the <p> tag is unchanged and still says Bruce Lee. This is due to asynchronous nature of the DOM updates in Vue.

Using nextTick() we can check that the chane has taken effect and the text node of the <p> tag equals to the previous set value, which is Chuck Norris.

cezar
  • 11,616
  • 6
  • 48
  • 84