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.
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.
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.
You can do it:
<currency-input>
<currency-input>
with a value that it transforms (13.467
is transformed by <currency-input>
to 13.46
)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);
});
});
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.
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>
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With