Form Handling
Forms in Vue can be as simple as plain HTML forms to complicated nested trees of custom Vue component form elements. We will gradually go through the ways of interacting with form elements, setting values and triggering events.
The methods we will be using the most are setValue()
and trigger()
.
Interacting with form elements
Let's take a look at a very basic form:
<template>
<div>
<input type="email" v-model="email" />
<button @click="submit">Submit</button>
</div>
</template>
<script>
export default {
data() {
return {
email: ''
}
},
methods: {
submit() {
this.$emit('submit', this.email)
}
}
}
</script>
Setting element values
The most common way to bind an input to data in Vue is by using v-model
. As you probably know by now, it takes care of what events each form element emits, and the props it accepts, making it easy for us to work with form elements.
To change the value of an input in VTU, you can use the setValue()
method. It accepts a parameter, most often a String
or a Boolean
, and returns a Promise
, which resolves after Vue has updated the DOM.
test('sets the value', async () => {
const wrapper = mount(Component)
const input = wrapper.find('input')
await input.setValue('my@mail.com')
expect(input.element.value).toBe('my@mail.com')
})
As you can see, setValue
sets the value
property on the input element to what we pass to it.
We are using await
to make sure that Vue has completed updating and the change has been reflected in the DOM, before we make any assertions.
Triggering events
Triggering events is the second most important action when working with forms and action elements. Let's take a look at our button
, from the previous example.
<button @click="submit">Submit</button>
To trigger a click event, we can use the trigger
method.
test('trigger', async () => {
const wrapper = mount(Component)
// trigger the element
await wrapper.find('button').trigger('click')
// assert some action has been performed, like an emitted event.
expect(wrapper.emitted()).toHaveProperty('submit')
})
If you haven't seen
emitted()
before, don't worry. It's used to assert the emitted events of a Component. You can learn more in Event Handling.
We trigger the click
event listener, so that the Component executes the submit
method. As we did with setValue
, we use await
to make sure the action is being reflected by Vue.
We can then assert that some action has happened. In this case, that we emitted the right event.
Let's combine these two to test whether our simple form is emitting the user inputs.
test('emits the input to its parent', async () => {
const wrapper = mount(Component)
// set the value
await wrapper.find('input').setValue('my@mail.com')
// trigger the element
await wrapper.find('button').trigger('click')
// assert the `submit` event is emitted,
expect(wrapper.emitted('submit')[0][0]).toBe('my@mail.com')
})
Advanced workflows
Now that we know the basics, let's dive into more complex examples.
Working with various form elements
We saw setValue
works with input elements, but is much more versatile, as it can set the value on various types of input elements.
Let's take a look at a more complicated form, which has more types of inputs.
<template>
<form @submit.prevent="submit">
<input type="email" v-model="form.email" />
<textarea v-model="form.description" />
<select v-model="form.city">
<option value="new-york">New York</option>
<option value="moscow">Moscow</option>
</select>
<input type="checkbox" v-model="form.subscribe" />
<input type="radio" value="weekly" v-model="form.interval" />
<input type="radio" value="monthly" v-model="form.interval" />
<button type="submit">Submit</button>
</form>
</template>
<script>
export default {
data() {
return {
form: {
email: '',
description: '',
city: '',
subscribe: false,
interval: ''
}
}
},
methods: {
async submit() {
this.$emit('submit', this.form)
}
}
}
</script>
Our extended Vue component is a bit longer, has a few more input types and now has the submit
handler moved to a <form/>
element.
The same way we set the value on the input
, we can set it on all the other inputs in the form.
import { mount } from '@vue/test-utils'
import FormComponent from './FormComponent.vue'
test('submits a form', async () => {
const wrapper = mount(FormComponent)
await wrapper.find('input[type=email]').setValue('name@mail.com')
await wrapper.find('textarea').setValue('Lorem ipsum dolor sit amet')
await wrapper.find('select').setValue('moscow')
await wrapper.find('input[type=checkbox]').setValue()
await wrapper.find('input[type=radio][value=monthly]').setValue()
})
As you can see, setValue
is a very versatile method. It can work with all types of form elements.
We are using await
everywhere, to make sure that each change has been applied before we trigger the next. This is recommended to make sure you do assertions when the DOM has updated.
TIP
If you don't pass a parameter to setValue
for OPTION
, CHECKBOX
or RADIO
inputs, they will set as checked
.
We have set values in our form, now it's time to submit the form and do some assertions.
Triggering complex event listeners
Event listeners are not always simple click
events. Vue allows you to listen to all kinds of DOM events, add special modifiers like .prevent
and more. Let's take a look how we can test those.
In our form above, we moved the event from the button
to the form
element. This is a good practice to follow, as this allows you to submit a form by hitting the enter
key, which is a more native approach.
To trigger the submit
handler, we use the trigger
method again.
test('submits the form', async () => {
const wrapper = mount(FormComponent)
const email = 'name@mail.com'
const description = 'Lorem ipsum dolor sit amet'
const city = 'moscow'
await wrapper.find('input[type=email]').setValue(email)
await wrapper.find('textarea').setValue(description)
await wrapper.find('select').setValue(city)
await wrapper.find('input[type=checkbox]').setValue()
await wrapper.find('input[type=radio][value=monthly]').setValue()
await wrapper.find('form').trigger('submit.prevent')
expect(wrapper.emitted('submit')[0][0]).toStrictEqual({
email,
description,
city,
subscribe: true,
interval: 'monthly'
})
})
To test the event modifier, we directly copy-pasted our event string submit.prevent
into trigger
. trigger
can read the passed event and all its modifiers, and selectively apply what is necessary.
TIP
Native event modifiers such as .prevent
and .stop
are Vue-specific and as such we don't need to test them, Vue internals do that already.
We then make a simple assertion, whether the form emitted the correct event and payload.
Native form submission
Triggering a submit
event on a <form>
element mimics browser behavior during form submission. If we wanted to trigger form submission more naturally, we could trigger a click
event on the submit button instead. Since form elements not connected to the document
cannot be submitted, as per the HTML specification, we need to use attachTo
to connect the wrapper's element.
Multiple modifiers on the same event
Let's assume you have a very detailed and complex form, with special interaction handling. How can we go about testing that?
<input @keydown.meta.c.exact.prevent="captureCopy" v-model="input" />
Assume we have an input that handles when the user clicks cmd
+ c
, and we want to intercept and stop him from copying. Testing this is as easy as copy & pasting the event from the Component to the trigger()
method.
test('handles complex events', async () => {
const wrapper = mount(Component)
await wrapper.find(input).trigger('keydown.meta.c.exact.prevent')
// run your assertions
})
Vue Test Utils reads the event and applies the appropriate properties to the event object. In this case it will match something like this:
{
// ... other properties
"key": "c",
"metaKey": true
}
Adding extra data to an event
Let's say your code needs something from inside the event
object. You can test such scenarios by passing extra data as a second parameter.
<template>
<form>
<input type="text" v-model="value" @blur="handleBlur" />
<button>Submit</button>
</form>
</template>
<script>
export default {
data() {
return {
value: ''
}
},
methods: {
handleBlur(event) {
if (event.relatedTarget.tagName === 'BUTTON') {
this.$emit('focus-lost')
}
}
}
}
</script>
import Form from './Form.vue'
test('emits an event only if you lose focus to a button', () => {
const wrapper = mount(Form)
const componentToGetFocus = wrapper.find('button')
wrapper.find('input').trigger('blur', {
relatedTarget: componentToGetFocus.element
})
expect(wrapper.emitted('focus-lost')).toBeTruthy()
})
Here we assume our code checks inside the event
object, whether the relatedTarget
is a button or not. We can simply pass a reference to such an element, mimicking what would happen if the user clicks on a button
after typing something in the input
.
Interacting with Vue Component inputs
Inputs are not only plain elements. We often use Vue components that behave like inputs. They can add markup, styling and lots of functionalities in an easy to use format.
Testing forms that use such inputs can be daunting at first, but with a few simple rules, it quickly becomes a walk in the park.
Following is a Component that wraps a label
and an input
element:
<template>
<label>
{{ label }}
<input
type="text"
:value="modelValue"
@input="$emit('update:modelValue', $event.target.value)"
/>
</label>
</template>
<script>
export default {
name: 'CustomInput',
props: ['modelValue', 'label']
}
</script>
This Vue component also emits back whatever you type. To use it you do:
<custom-input v-model="input" label="Text Input" class="text-input" />
As above, most of these Vue-powered inputs have a real button
or input
in them. You can just as easily find that element and act on it:
test('fills in the form', async () => {
const wrapper = mount(CustomInput)
await wrapper.find('.text-input input').setValue('text')
// continue with assertions or actions like submit the form, assert the DOM…
})
Testing complex Input components
What happens if your Input component is not that simple? You might be using a UI library, like Vuetify. If you rely on digging inside the markup to find the right element, your tests may break if the external library decides to change their internals.
In such cases you can set the value directly, using the component instance and setValue
.
Assume we have a form that uses the Vuetify textarea:
<template>
<form @submit.prevent="handleSubmit">
<v-textarea v-model="description" ref="description" />
<button type="submit">Send</button>
</form>
</template>
<script>
export default {
name: 'CustomTextarea',
data() {
return {
description: ''
}
},
methods: {
handleSubmit() {
this.$emit('submitted', this.description)
}
}
}
</script>
We can use findComponent
to find the component instance, and then set its value.
test('emits textarea value on submit', async () => {
const wrapper = mount(CustomTextarea)
const description = 'Some very long text...'
await wrapper.findComponent({ ref: 'description' }).setValue(description)
wrapper.find('form').trigger('submit')
expect(wrapper.emitted('submitted')[0][0]).toEqual(description)
})
Conclusion
- Use
setValue
to set the value on both DOM inputs and Vue components. - Use
trigger
to trigger DOM events, both with and without modifiers. - Add extra event data to
trigger
using the second parameter. - Assert that the DOM changed and the right events got emitted. Try not to assert data on the Component instance.