vue-test-utils: Can't trigger input event

I’m able to trigger a click event with vue-test-utils, but can’t seem to trigger an input event.

Here’s my component:

<template>
  <div class="content">
      <input @input="resizeThumbs($event)" id="resize_thumbs_input" type="range" min="40" max="400" value="200">
      <div v-model="thumbnails" class="img_gallery">
          <div v-bind:style="{'max-width': thumbPixelWidth + 'px' }" class="thumbnail"
               v-for="thumbnail in thumbnails" :key="thumbnail.id">
            <img :src="thumbnail.url" class="thumb">
          </div>
      </div>
  </div>
</template>

<script>
export default {
  name: 'thumbnails',
  data: function () {
    return {
      thumbPixelWidth: 200
    }
  },
  computed: {
    thumbnails: {
      get () {
        return this.$store.state.images
      }
    }
  },
  methods: {
    resizeThumbs: function (event) {
      this.thumbPixelWidth = event.target.value
    }
  }
}
</script>

Here’s my test:

import Vue from 'vue'
import Vuex from 'vuex'
import { mount, createLocalVue } from 'vue-test-utils'
import Thumbnails from '@/components/Thumbnails'
import Fixtures from '@/test/fixtures/image-collection'
const localVue = createLocalVue()
localVue.use(Vuex)

describe('Thumbnails.vue', () => {
  let wrapper
  let options
  let actions
  let state
  let store

  beforeEach(() => {
    actions = {
      resizeThumbs: jest.fn(),
    }
    state = {
      images: Fixtures.imageCollection,
    }
    store = new Vuex.Store({
      state,
      actions
    })
    options = {
      computed: {
        thumbnails: {
          get () {
            return state.images
          }
        }
      }
    }
  })

  it('changes thumbnail size when range input changes', () => {
    const wrapper = mount(Thumbnails, { options, store, localVue })
    wrapper.findAll('#resize_thumbs_input').at(0).trigger('input')
    expect(actions.resizeThumbs).toHaveBeenCalled()
  })

})

The test fails with the following output:

● Test.vue › changes thumbnail size when range input changes

    expect(jest.fn()).toHaveBeenCalled()
    
    Expected mock function to have been called.

About this issue

  • Original URL
  • State: closed
  • Created 7 years ago
  • Comments: 31 (12 by maintainers)

Commits related to this issue

Most upvoted comments

trigger creates a new DOM Event, and tries to overwrite the Event properties with the properties of options. Unfortunately, target is a read only property, which leads to the confusing error.

To trigger an input with the value of 100, you need to set the value of the element you’re triggering the event on to 100:

const input = wrapper.find('#resize_thumbs_input')
input.element.value = 100
input.trigger('input', {target: {value: 100}})

I’m going to add a note to the docs that target is unwritable, and throw a custom error inside trigger, so that users get an improved error message.

Let me know if setting the value before triggering the event solves your problem.

@lmiller1990 Thanks for sharing it, but I still have some questions about testing with shallowMount only, maybe you can clarify it for me.

How to use shallowMount if component I want to test doesn’t have pure inputs/buttons, but uses some wrapper components, e.g <input-wrapper v-model="someValue" /> . In that case I just have to use mount to reach the input.

I’d be happy to work with component-wrapper itself instead of input. If I use shallowMount, I still can find input-wrapper by name or importing it and passing into find like this:

import InputWrapper from '@/some/path/to/InputWrapper.vue'
const inputWrapper = wrapper.find(InputWrapper)

But I can’t neither trigger input on that component nor call setValue - both don’t work. Should they?

// doesn't work (also type of second arg `object` according to typings I have now)
input.trigger('input', { some: 'more complex value than just string' } as any)

// produces error: wrapper.setValue() cannot be called on this element
input.setValue('some value')

upd: this how it works for me now:

    const input = wrapper.find(InputWrapper)
    input.vm.$emit('input', 'some value')

@mjvezzani I hear you loud and clear. I come from Ruby on Rails, where testing is built in and works great out of the box (mainly from many years of hard work by the community. I’m sure it was not good back in the early days, either).

This is my opinion from the last year or two of developing with Vue - take it was a grain of salt, everyone has their own style and preferences.

Regarding Vuex:

My experience regarding Vuex has been to test the two “sides” of it, if you will.

  1. the store in isolation (mutations/actions/getters). This is easy, since they are just regular JS functions.
  2. the “integration” with components (for example, when you test a component that commits a mutation, you mock commit with jest.fn, and assert expect(commit).toHaveBeenCalledWith(mutationHandler, payload).

Of course this means that you could break a getter, and the Vue component that relies on that getter will not fail. That’s unit tests for you, they only test things in isolation.

Not to plug my own guide, but I wrote about it in detail here (with the help of 15 contributors).

For the case of a complex form using a Vuex store and several components that compose together, I think you should test each part individually (store, mutations, validations). As for making sure it works together, it should be tested using an e2e test, using something like Selenium (meh, but what I use at work) or cypress.io (awesome).

About shallowMount and testing around component frameworks

My personal experience reflects what you are encountering, regarding testing components composed of many other components. It’s awkward and doesn’t feel very beneficial. If possible, make your components smaller and more focused, so they are easier to test.

Some practices I have adopted:

  • use shallowMount almost exclusively. It’s a unit test, by using mount it doesn’t seem like a unit test anymore. I haven’t found many use cases for mount.
  • only test components by settings their state using the mounting options (I never use setMethods, setData etc). Like I demonstrated above in my snippet.
  • use trigger to test clicking buttons, then assert events are emitted, or some method is called.
  • don’t test component frameworks at all. They work, that’s why we use them.

Case Study to illustrate these thoughts

Another case that comes up a lot is "how do I test my components that use vuetify/vuebootstrap/whatever. I don’t know what other people are doing, but in my experience if I have a component like this:

<template>
  <v-form v-model="valid">
    <v-text-field
      v-model="name"
      :rules="nameRules"
      :counter="10"
      label="Name"
      required
    ></v-text-field>
  </v-form>
</template>

<script>
Vue.use(Vuetify)

export default {
  data() {
    name: '',
    nameRules: [this.short, this.long]
  },
  
  methods: {
    short(val) { return val.length < 3 },
    long(val) { return val.length > 10 } 
  }
}
</script>

In this case, I know that v-form works, since Vuetify already has tests around that. That means I should not be testing it. In this case, the code I test is the validations. I would just test it like this (docs here for v-form):

const factory = (name) => shallowMount(Component)

it('prohibits short names', () => {
  const wrapper = factory()
  expect(wrapper.vm.short('aa')).toBe(false)
})

it('prohibits long names', () => {
  const wrapper = factory()
  expect(wrapper.vm.short('12345678910')).toBe(false)
})

it('accepts the name', () => {
  const name = 'some_name'
  const wrapper = factory()
  expect(wrapper.vm.short(some_name)).toBe(true)
  expect(wrapper.vm.long(some_name)).toBe(true)
})

Let’s say name actually comes from this.$store.state.name. Test doesn’t change - the component does not need to know where the name comes from, only if it is valid.

Writing E2E tests is time consuming but worth it

So, say we have a very complex Vuetify + Vuex form. What do I do?? Unit test the mutations and actions, and validations. Since my backend is Rails, I write a bunch of E2E tests using the testing tools there, filling out the form, submitting, and making sure that the form submission did not occur, or the correct error message is rendered, or database was correctly updated.

This take a lot longer to write, and run, that a unit test, but it is the only way I have found I can have confidence everything works together.


That was a pretty long answer, but hopefully it gives you some food for thought and keeps your motivation for Vue testing and TDD in general high! Although you can force it, I don’t think vue-test-utils alone is enough to fully “test” your Vue app, and is best when used to test small units of work.

I dug into the bootstrap-vue project on Github and found the tests for the b-form-input component. In those tests, they use vue-test-utils and they use the trigger() method in their tests. The expectations in those tests are different from what I was anticipating:

it('apply transform function', async () => {
    const wrapper = mount(Input, {
      propsData: {
        formatter (value) {
          return value.toLowerCase()
        }
      }
    })
    const input = wrapper.find('input')
    input.element.value = 'TEST'
    input.trigger('input')

    expect(wrapper.emitted().input[0]).toEqual(['test'])
  })

In this test, the expectation is on what value was emitted by the input field when an input event occurs. Naively, I assumed that there was a data property in the b-form-input component definition, and that the test would assert that a value was set somewhere instead of just emitting a value. This information doesn’t really get me any closer to understanding how to get my test case working, but does shed some further context on what I am working with in b-form-input.

Thanks, for a quick response… I will try your suggestion of await wrapper.find('#input-widget').setValue('data'), and tryout the testing today.

I will reply in SO

I finally settled with:

it(“should call the updateSubmitDisabled mutation”, () => { const wrapper = mount(UserForm, { localVue, store }); const input = wrapper.get(‘input[name=“name”]’); input.element.dispatchEvent(new Event(‘input’)); expect(mutations.updateSubmitDisabled).toHaveBeenCalled(); });

I think I’m passing through a similar issue by now. In my case I’m using Choices as a third party component and Jest as test runner.

Here is a JSFiddle as an example.

Here is my test:

import CustomSelect from '@/CustomSelect';
import { mount } from '@vue/test-utils';

let wrapper,
  options = [
    {
      key: 'Foo',
      value: 'foo',
    },
    {
      key: 'Bar',
      value: 'bar',
    },
    {
      key: 'Baz',
      value: 'baz',
    },
  ];

beforeEach(() => {
  wrapper = mount(CustomSelect, {
    propsData: {
      options,
    },
  });
});

it('Emits an `input` event when selection changes', () => {
  wrapper.vm.choicesInstance.setChoiceByValue([options[1].value]);
  expect(wrapper.emitted().input).not.toBeFalsy();
});

This only test case above does not work…

For some reason, when I change the value through the choicesInstance it does not emits the input event only when unit testing. If I do the exact change programmatically using the browser console or interacting with Choices it works. Vue extension detects the input event correctly, but somehow this is not happening while testing with Jest.

@mjvezzani great questions and observations. I like these kind of discussions in issues, as opposed to simply “how do I do X”, we should be asking “how should I do X”?

I don’t think there is any one “correct” way to test. What I’ve been doing lately is identifying all the states a component can be in (for example a form can be valid and invalid). Then I set up the component (assemble), act if necessary (click submit, for example) and assert (the form was not submitted).

In the example I provided, I used setData. What if I forgot to write v-model in the markup? The test would still pass based on how I wrote it. It would be giving a false positive. So I do agree with you, it would be ideal to simulate the user input in this case.

Either way I would still write an e2e test using something like cypress.io, and fully exercise that entire page to have full confidence. But this is something Vue test utils should (can?) do.

@eddyerburgh I’m interested in your opinion about this topic, and is there even a way to set a input inside a shallowMount component using vue-test-utils?