Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How can I test a custom input Vue component

In the Vue.js documentation, there is an example of a custom input component. I'm trying to figure out how I can write a unit test for a component like that. Usage of the component would look like this

<currency-input v-model="price"></currency-input>

The full implementation can be found at https://vuejs.org/v2/guide/components.html#Form-Input-Components-using-Custom-Events

The documentation says

So for a component to work with v-model, it should (these can be configured in 2.2.0+):

  • accept a value prop
  • emit an input event with the new value

How do I write a unit test that ensures that I've written this component such that it will work with v-model? Ideally, I don't want to specifically test for those two conditions, I want to test the behavior that when the value changes within the component, it also changes in the model.

like image 702
Jason Desrosiers Avatar asked Mar 11 '18 21:03

Jason Desrosiers


People also ask

How do I get input value from Vue?

To get an input value in Vue, we can use the v-model directive to set up a two-way binding between the value and a variable. Every time the user changes the text in the input field, the text variable will be updated automatically. We can then use this variable to perform an action or display information.

What is snapshot testing in Vue?

The second means that snapshot testing is a way of making sure an entire component runs the way you intend it to. The way snapshot testing works is that the very first time you run jest , snapshots are generated of the DOM.


2 Answers

You can do it:

  • Using Vue Test Utils, and
  • Mounting a parent element that uses <currency-input>
  • Fake an input event to the inner text field of <currency-input> with a value that it transforms (13.467 is transformed by <currency-input> to 13.46)
  • Verify if, in the parent, the price property (bound to v-model) has changed.

Example code (using Mocha):

import { mount } from '@vue/test-utils'
import CurrencyInput from '@/components/CurrencyInput.vue'

describe('CurrencyInput.vue', () => {
  it("changing the element's value, updates the v-model", () => {
    var parent = mount({
      data: { price: null },
      template: '<div> <currency-input v-model="price"></currency-input> </div>',
      components: { 'currency-input': CurrencyInput }
    })

    var currencyInputInnerTextField = parent.find('input');
    currencyInputInnerTextField.element.value = 13.467;
    currencyInputInnerTextField.trigger('input');

    expect(parent.vm.price).toBe(13.46);
  });
});

In-browser runnable demo using Jasmine:

var CurrencyInput = Vue.component('currency-input', {
  template: '\
    <span>\
      $\
      <input\
        ref="input"\
        v-bind:value="value"\
        v-on:input="updateValue($event.target.value)">\
    </span>\
  ',
  props: ['value'],
  methods: {
    // Instead of updating the value directly, this
    // method is used to format and place constraints
    // on the input's value
    updateValue: function(value) {
      var formattedValue = value
        // Remove whitespace on either side
        .trim()
        // Shorten to 2 decimal places
        .slice(0, value.indexOf('.') === -1 ? value.length : value.indexOf('.') + 3)
      // If the value was not already normalized,
      // manually override it to conform
      if (formattedValue !== value) {
        this.$refs.input.value = formattedValue
      }
      // Emit the number value through the input event
      this.$emit('input', Number(formattedValue))
    }
  }
});



// specs code ///////////////////////////////////////////////////////////
var mount = vueTestUtils.mount;
describe('CurrencyInput', () => {
  it("changing the element's value, updates the v-model", () => {
    var parent = mount({
      data() { return { price: null } },
      template: '<div> <currency-input v-model="price"></currency-input> </div>',
      components: { 'currency-input': CurrencyInput }
    });
    
    var currencyInputInnerTextField = parent.find('input');
    currencyInputInnerTextField.element.value = 13.467;
    currencyInputInnerTextField.trigger('input');

    expect(parent.vm.price).toBe(13.46);
  });
});

// load jasmine htmlReporter
(function() {
  var env = jasmine.getEnv()
  env.addReporter(new jasmine.HtmlReporter())
  env.execute()
}())
<link rel="stylesheet" type="text/css" href="https://cdn.jsdelivr.net/jasmine/1.3.1/jasmine.css">
<script src="https://cdn.jsdelivr.net/jasmine/1.3.1/jasmine.js"></script>
<script src="https://cdn.jsdelivr.net/jasmine/1.3.1/jasmine-html.js"></script>
<script src="https://npmcdn.com/[email protected]/dist/vue.js"></script>
<script src="https://cdn.jsdelivr.net/npm/[email protected]/browser.js"></script>
<script src="https://rawgit.com/vuejs/vue-test-utils/2b078c68293a41d68a0a98393f497d0b0031f41a/dist/vue-test-utils.iife.js"></script>

Note: The code above works fine (as you can see), but there can be improvements to tests involving v-model soon. Follow this issue for up-to-date info.

like image 66
acdcjunior Avatar answered Oct 23 '22 06:10

acdcjunior


I would also mount a parent element that uses the component. Below a newer example with Jest and Vue Test Utils. Check the Vue documentation for more information.

import { mount } from "@vue/test-utils";
import Input from "Input.vue";

describe('Input.vue', () => {
    test('changing the input element value updates the v-model', async () => {
        const wrapper = mount({
            data() {
                return { name: '' };
            },
            template: '<Input v-model="name" />',
            components: { Input },
        });

        const name = 'Brendan Eich';
        await wrapper.find('input').setValue(name);

        expect(wrapper.vm.$data.name).toBe(name);
    });

    test('changing the v-model updates the input element value', async () => {
        const wrapper = mount({
            data() {
                return { name: '' };
            },
            template: '<Input v-model="name" />',
            components: { Input },
        });

        const name = 'Bjarne Stroustrup';
        await wrapper.setData({ name });

        const inputElement = wrapper.find('input').element;
        expect(inputElement.value).toBe(name);
    });
});

Input.vue component:

<template>
    <input :value="$attrs.value" @input="$emit('input', $event.target.value)" />
</template>
like image 32
Sand4rt Avatar answered Oct 23 '22 07:10

Sand4rt