Search code examples
vue.jsvuejs2vue-test-utils

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://v2.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.


Solution

  • 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/vue@2.5.15/dist/vue.js"></script>
    <script src="https://cdn.jsdelivr.net/npm/vue-template-compiler@2.5.15/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.