Image

unit-testing-vue-3

8 min read
Last update: December 19, 2021

Testing Emitted Events

In the previous lesson, we looked at testing a component that took in some props and generated a number based upon a button click. This required us to simulate (trigger) the button click within our test. That “click” falls under the category of a native DOM event, but often in Vue components, we need our components to emit their own custom event, and in this lesson we’ll look at testing such events.


What’s a custom event?

If custom events are new to you, you can check out the Communicating Events lesson in our Intro to Vue.js course. In short: sometimes a child component needs to let another component in the app know that something happened within it. It can broadcast that something happened by emitting a custom event (such as formSubmitted) to let its parent know that event happened. The parent can wait, listening for that event to happen, then respond accordingly when it does.


The Starting Code

Fortunately, Vue Test Utils provides us with an emitted API, which we can use to test these kinds of custom emitted events within our components. We’ll explore how to use that API in a moment, but first let’s look at the component that we’ll be testing.

📃 LoginForm.vue

<template>
    <form @submit.prevent="onSubmit">
        <input type="text" v-model="name" />
        <button type="submit">Submit</button>
    </form>
</template>

<script>
    export default {
        data() {
            return {
                name: ''
            }
        },
        methods: {
            onSubmit() {
                this.$emit('formSubmitted', {
                    name: this.name
                })
            }
        }
    }
</script>

As you can see, we have a very simple login form. Pay attention to how we have the onSubmit() method, which is used to $emit a custom event by the name of formSubmitted, which sends a payload containing the name data, which is bound to our input element. We want to test that when the form iis submitted, it indeed emits an event, containing the user name payload.


Scaffolding the test file

When writing tests, it’s helpful if we write them in a way that mimics how an actual end user will interact with the component. So how will a user use this component? Well, they will find the text input field, then they’ll add their name, then submit the form. So we’ll replicate that process as closely as possible in our test, like so:

📃 LoginForm.spec.js

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

describe('LoginForm', () => {
it('emits an event with a user data payload', () => {
const wrapper = mount(LoginForm)
// 1. Find text input
// 2. Set value for text input
// 3. Simulate form submission
// 4. Assert event has been emitted
// 5. Assert payload is correct
})
})

Above, you can see that we’ve imported the LoginFrom component along with mount from Vue Test Utils, and we need to accomplish the following steps:

  1. Find text input
  2. Set value for text input
  3. Simulate form submission
  4. Assert event has been emitted
  5. Assert payload is correct

Let’s implement these steps.


Setting the text input value

First, just like our end user would, we’ll find the text input and set its value.

📃 LoginForm.spec.js

describe('LoginForm', () => {
it('emits an event with user data payload', () => {
const wrapper = mount(LoginForm)
const input = wrapper.find('input[type="text"]') // Find text input

input.setValue('Adam Jahr') // Set value for text input

// 3. Simulate form submission
// 4. Assert event has been emitted
// 5. Assert payload is correct

})
})

A note about targeting inputs

This works great for our specific needs, but it’s worth mentioning here that in production tests, you might considering using test-specific attribute on your elements, like so:

<input data-testid="name-input" type="text" v-model="name" />

Then, in your test file, you’d find the input using that attribute.

const input = wrapper.find('[data-testid="name-input"]')

This is beneficial for a few reasons. First, if we had multiple inputs we could target them specifically with these ids, and perhaps more importantly this decouples the DOM from your test. For example, if you eventually replaced the native input with an input from a component library, the test would still conform to the same interface and wouldn’t need to change. It also solves the issue of a designer changing the class or id name of the element, causing your test to fail. Test-specific attributes are one way to future-proof your tests.


Simulating the form submission

Once our end user has filled out our form, the next step they would take would be to submit the form. In the previous lesson, we looked at how the trigger method can be used to simulate an event on a DOM element.

While you might be tempted to use trigger on our form’s button to simulate our form submission, there’s a potential problem with that. What if we eventually removed the button from this component and instead relied on the input’s keyup.enter event to submit the form? We would have to refactor our test. In other words: in that case our test would have been too tightly coupled with the implementation details of our component’s form. So the more future-proofed solution would be to force a submit even on the form itself, without relying on our button as the middle man.

📃 LoginForm.spec.js

describe('LoginForm', () => {
it('emits an event with user data payload', () => {
const wrapper = mount(LoginForm)
const input = wrapper.find('input[type="text"]') // Find text input

input.setValue('Adam Jahr') // Set value for text input
wrapper.trigger('submit') // Simulate form submission

// 4. Assert event has been emitted
// 5. Assert payload is correct

})
})

Now by using wrapper.trigger('submit'), we’ve implemented a more scalable, decoupled solution to simulate a user submitting our form.


Testing our expectations

Now that our input field’s value has been set and our form has been submitted, we can move on to testing that what we expect to happen is actually happening:

  • The event has been emitted
  • The payload is correct

To test the event was emitted, we’ll write:

📃 LoginForm.spec.js

describe('LoginForm', () => {
it('emits an event with user data payload', () => {
const wrapper = mount(LoginForm)
const input = wrapper.find('input[type="text"]') // Find text input

input.setValue('Adam Jahr') // Set value for text input
wrapper.trigger('submit') // Simulate form submission

// Assert event has been emitted
const formSubmittedCalls = wrapper.emitted('formSubmitted')
expect(formSubmittedCalls).toHaveLength(1)
})
})

Here we are using Vue Test Utils’ emitted API to store any calls of the formSubmitted event in a const, and asserting that we expect that array to have a length of 1. In other words: we are checking to see if the event was indeed emitted.


Now we just need to confirm that the event was emitted with the proper payload (our component’s name data value). We’ll use the emitted API again for this.

If we were to console.log wrapper.emitted('formSubmitted'), we’d see this:

[[], [{ 'name': 'Adam Jahr' }]]

So in order to target the payload itself, the syntax would look like:

wrapper.emitted('formSubmitted')[0][0])

We’ll then match that against our expected payload, which for organization purposes we’ll store in const expectedPayload = { name: 'Adam Jahr' }

Now we can check to see that our expectedPayload object matches the payload emitted along with the formSubmitted event.

const expectedPayload = { name: 'Adam Jahr' }
expect(wrapper.emitted('formSubmitted')[0][0]).toMatchObject(expectedPayload)

Sidenote: We could alternatively hard-code the expected payload into the matcher: .toEqual({ name: 'Adam Jahr' }). But storing it in a const keeps us a bit more clear about what is what.

Our full testing file now looks like this:

📃 LoginForm.spec.js

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

describe('LoginForm', () => {
it('emits an event with user data payload', () => {
const wrapper = mount(LoginForm)
const input = wrapper.find('input[type="text"]') // Find text input

input.setValue('Adam Jahr') // Set value for text input
wrapper.trigger('submit') // Simulate form submission

// Assert event has been emitted
const formSubmittedCalls = wrapper.emitted('formSubmitted')
expect(formSubmittedCalls).toHaveLength(1)

// Assert payload is correct
const expectedPayload = { name: 'Adam Jahr' }
expect(wrapper.emitted('formSubmitted')[0][0]).toMatchObject(expectedPayload)
})
})

If we npm run test:unit we’ll see that our new test is passing. Great work!



Let’s ReVue

We’ve learned how to write a test while replicating how a user would interact with a component, in order to test that our custom events are emitted with the correct payload.