vuejs / vue-test-utils

Component Test Utils for Vue 2
https://vue-test-utils.vuejs.org
MIT License
3.57k stars 668 forks source link

Can't trigger input event #266

Closed sdellis closed 6 years ago

sdellis commented 6 years ago

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.
lmiller1990 commented 6 years ago

Are you trying to trigger a mock action, or just the function inside of the methods object? You aren't using the Vuex action in your component at the moment.

Inside of resizeThumbs you would need to do:

 methods: {
    resizeThumbs: function (event) {
      this.$store.dispatch('resizeThumbs')
      this.thumbPixelWidth = event.target.value
    }
  }

Also in the component you do not need to pass $event, it is the first argument by default. It should be @input="resizeThumbs".

More info can be found in the docs:: https://vue-test-utils.vuejs.org/en/guides/using-with-vuex.html

This does not appear to be a problem with vue-test-utils, I think stackoverflow might be a better bet for these kind of questions. If you are new to to Vue testing or want some more examples, here are two blogs (myself and the maintainer, Eddy) who write a lot about testing with vue-test-utils, for more examples.

https://eddyerburgh.me/ https://medium.com/@lachlanmiller_52885

eddyerburgh commented 6 years ago

Hi @sdellis. This is a problem with your code, rather than vue-test-utils. The input even is getting triggered, but the action isn't called because you haven't added any code to dispatch the action.

This issue tracker is reserved for bug fixes and feature requests. For questions like these, please ask on StackOverflow or the Vue discord channel.

@lmiller1990 FYI, it's Edd, not Eddy 😉

sdellis commented 6 years ago

I apologize for erroneously filing this as a bug report. I'll be more careful next time. @lmiller1990 I was trying to trigger the function within the methods object. I think I got confused as example was simplified from a more complicated component in which the resizeThumbs method doesn't need to touch the store.

eddyerburgh commented 6 years ago

If you think this is a bug, could you please create a minimal reproduction that demonstrates the bug? If it's not a bug I'll leave this closed.Thanks 🙂

sdellis commented 6 years ago

@eddyerburgh If I'm passing the event properly, I think this may be a bug. Here's a minimal reproduction with all the unnecessary vuex stuff taken out. My component:

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

<script>
export default {
  name: 'test',
  data: function () {
    return {
      thumbPixelWidth: 200
    }
  },
  computed: {
    thumbnails: function () {
      return [{"id":"1","url":"1.png"},{"id":"2","url":"2.png"}]
    }
  },
  methods: {
    resizeThumbs: function (event) {
      this.thumbPixelWidth = event.target.value
    }
  }
}
</script>

My test:

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

describe('Test.vue', () => {
  it('changes thumbnail size when range input changes', () => {
    const wrapper = mount(Test)
    wrapper.find('#resize_thumbs_input').trigger('input', {target: {value: 100}})
    const resizedThumb = wrapper.find('.thumbnail')
    expect(resizedThumb.hasStyle('max-width', '100px')).toBe(true)
  })
})

I get the following error when running the test:

FAIL  valhalla/app/javascript/test/unit/specs/Test.spec.js
 ● Test.vue › changes thumbnail size when range input changes

   TypeError: Cannot set property target of [object Object] which has only a getter

According to the docs I think I'm passing event.target.value properly.

eddyerburgh commented 6 years ago

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.

sdellis commented 6 years ago

@eddyerburgh Thank you for your help, for improving the error message, and adding this to the docs! One small note with your example above is that I needed to remove , {target: {value: 100}} from the trigger call to get it to work for me.

mjvezzani commented 6 years ago

I have a related issue to @sdellis, except that using a native html 5 input tag, I'm using a b-form-input component that is provided by bootstrap vue. input.trigger() seems to trigger an event, but input.element.value doesn't seem to actually set the value of the b-form-input field. Any thoughts on why this would be different for b-form-input and how vue-test-utils deals with this kind of custom input component?

lmiller1990 commented 6 years ago

Can you provide a link to the component library and snippet? Most likely the component is built (by the library author) like so

<b-form-input>
  <div class="bootstrap_styling_etc">
    <input value="aaa">
  </div>
</b-form-input>

When you call input.element.value you may be setting it on the <b-form-input> instead of the actual <input> wrapper by the library author. If you provide some code and context I can offer other suggestions.

mjvezzani commented 6 years ago

@lmiller1990 In the case of Bootstrap Vue, b-form-input seems to hide the actual input field within the component, so what you end up with is:

<b-form-input v-model="text1"
              type="text"
              placeholder="Enter your name">
</b-form-input>

You can read more about it here.

In my test, when I create my wrapper object of the component I am trying to test:

const wrapper = shallowMount(MyComponent, { propsData: { options } })

I then get the input field I am trying to manipulate:

<!-- The template markup -->
<div class="row">
  <div class="col-lg-12" id="contract-id-input">
    <b-form-input
      placeholder="7-digit code"
      :value="productData.contractId"
      @input="updateProduct($event, 'contractId')"
      aria-describedby="contractIdFeedback"
      autocomplete="off"
      maxlength="7"
      :state="this.productValidationStates.contractId"
      @keydown.native="isNumericAllowed($event)"
    >
    </b-form-input>
    <b-form-invalid-feedback id="contractIdFeedback">
      7 digits required
    </b-form-invalid-feedback>
  </div>
</div>

// The code in my test to get the DOM element

const myInput = wrapper.find('#contract-id-input > b-form-input')

Which, as far as I can tell, successfully gets that b-form-input node within the component's html template. When I console.log myInput.attributes() in my test, I can see all the attributes that were attached to the b-form-input in my template markup.

One thing that I do find very strange is the inconsistency of console.log values when running the unit tests verses running the code in a browser. A specific case is when I'm trying to console.log the value of this.productData.contractId, which is data defined in the component. In the test, I get the value of Event { isTrusted: [Getter] }, but in Chrome I get an empty string. Each time I add a value to that b-form-input field, then the value gets logged out to the console in the browser.

If any of this information triggers a thought as to what the problem might be, I'd really appreciate the help. Thanks in advance!

mjvezzani commented 6 years ago

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.

mjvezzani commented 6 years ago

Might as well provide the source code that I am working with to help provide full context:

The component:

<template>
    <div>
        <div class="cartulary" @click="click">
            <div :class="'classification ' + active">
                <fuse-icon @click="updateProduct($event, 'delete')" iconClass="dismiss" customCss="float-right"></fuse-icon>
                <div class="row align-items-center">
                    <div class="col-lg-3">
                        <div class="product-name">
                            {{ productData.name }}
                        </div>
                    </div>
                    <div class="col-lg-9">
                        <div class="row">
                            <!-- Contract ID -->
                            <div class="col-lg-4">
                                <div class="row">
                                    <div class="col-lg-5" id="contract-id-label">
                                        <span class="detail-label">
                                           Contract ID *
                                        </span>
                                    </div>
                                </div>
                                <div class="row">
                                    <div class="col-lg-12" id="contract-id-input">
                                        <b-form-input
                                            placeholder="7-digit code"
                                            :value="productData.contractId"
                                            @input="updateProduct($event, 'contractId')"
                                            aria-describedby="contractIdFeedback"
                                            autocomplete="off"
                                            maxlength="7"
                                            :state="this.productValidationStates.contractId"
                                            @keydown.native="isNumericAllowed($event)"
                                        >
                                        </b-form-input>
                                        <b-form-invalid-feedback id="contractIdFeedback">
                                           7 digits required
                                        </b-form-invalid-feedback>
                                    </div>
                                </div>
                            </div>
                        </div>
                    </div>
                </div>
            </div>
        </div>
    </div>
</template>
<script>
    import validationHelpers from '../../../helpers/validationHelpers'
    export default {
        name: 'EnercareLineItem',
        props: [
            'options',
            'active'
        ],
        data() {
            return {
                datePickerOptions: {
                    type: 'datetime',
                    format: 'MM/dd/yyyy hh:mm A',
                    placeholder: 'Select date and time'
                },
                productData : this.options,
                productValidationStates: {
                    contractId: '',
                    caseId: ''
                },
                isValid: false
            }
        },
        methods: {
            click() {
                this.$emit('click')
            },
            /**
             * Only allow the user to enter numbers, otherwise prevent the
             * input from registering
             */
            isNumericAllowed(event) {
                if (!validationHelpers.allowNumeric(event)) {
                    event.preventDefault()
                }
            },
            /**
             * updateProducts() sends the product data up to the parent.
             */
            updateProduct(value, type) {
                if (type === 'delete') {
                    this.$emit('delete')
                    return
                }
                this.productData[type] =  value
                console.log('#################################')
                console.log('Here in updateProduct()')
                console.log('#################################')
                this.setValidationStates()
                this.$emit('update', {
                    encrProductData: this.productData,
                    isValid: !Object.values(this.productValidationStates).includes('invalid')
                })
            },
            setValidationStates() {
                console.log('#################################')
                console.log('Here in setValidationStates()')
                console.log(this.productData.contractId)
                console.log('#################################')
                this.productValidationStates.contractId = this.productData.contractId.length !== 7 ?  'invalid' : 'valid'
                switch (this.productData.caseId.length) {
                    case 0:
                        this.productValidationStates.caseId = 'reset'
                        break
                    case 8:
                        this.productValidationStates.caseId = 'valid'
                        break
                    default:
                        this.productValidationStates.caseId = 'invalid'
                        break
                }
            }
        },
        created() {
            this.setValidationStates()
        },
        updated() {
            console.log('I just updated', this.options)
        }
    }
</script>

The unit test:

import Vuex from 'vuex'
import BootstrapVue from 'bootstrap-vue'
import { shallowMount, createLocalVue } from '@vue/test-utils'
import EnercareLineItem from '../../resources/assets/js/components/formfields/enercare/EnercareLineItem'
import BaseField from '../../resources/assets/js/components/BaseField'
​
const Vue = createLocalVue()
Vue.use(Vuex)
Vue.use(BootstrapVue)
​
describe('EnercareLineItem', () => {
  describe('Contract Id input', () => {
    it('is valid at a length of 7', () => {
      let options = {
        index: 0,
        name: 'Hot Water Heater Tune Up',
        contractId: '',
        appointmentTime: '2018-01-01T12:00:00Z',
        caseId: ''
      }
​
      const wrapper = shallowMount(EnercareLineItem, {
        propsData: { options }
      })
​
      const contractIdInput = wrapper.find('#contract-id-input > b-form-input')
      expect(contractIdInput.attributes().state).toBe('invalid')
      contractIdInput.element.value = '1234567'
      contractIdInput.trigger('input')
      expect(contractIdInput.attributes().state).toBe('valid')
    })
  })
})
lmiller1990 commented 6 years ago

Hi @mjvezzani

There is a bit of code missing (like what kind of props are received, and what validationHelpers is). This made it a bit hard to recreate your example, so I simplified the code to only have the b-form-input and attempted to test the thing you want to test.

Simplified component:

<template>
  <div>
    <div class="col-lg-12" id="contract-id-input">
      <b-form-input
        id="b-form-input"
        v-model="productData.contractId"
        maxlength="7"
        :state="productValidator"
      >
      </b-form-input>
    </div>
  </div>
</template>

<script>
export default {
  name: 'EnercareLineItem',

  data() {
    return {
      productData: { contractId: '' }
    }
  },

  computed: {
    productValidator() {
      switch (this.productData.contractId.length) {
        case 0:
          return 'reset'
        case 8:
          return 'valid'
        default:
          return 'invalid'
      }
    }
  }
}
</script>

Passing Tests:

import BootstrapVue from 'bootstrap-vue'
import { shallowMount, mount, createLocalVue } from '@vue/test-utils'
import EnercareLineItem from '@/components/HelloWorld'
const Vue = createLocalVue()
Vue.use(BootstrapVue)

describe('EnercareLineItem', () => {
  describe('Contract Id input', () => {
    it('is invalid at a length of 7', () => {
      const wrapper = shallowMount(EnercareLineItem, {
        localVue: Vue,
        data() {
          return {
            productData: { contractId: "1234567" }
          }
        }
      })

      const contractIdInput = wrapper.find('#b-form-input')

      expect(contractIdInput.attributes().state).toBe('invalid')
    })

    it('is valid at a length of 8', () => {
      const wrapper = shallowMount(EnercareLineItem, {
        localVue: Vue,
        data() {
          return {
            productData: { contractId: '12345678' }
          }
        }
      })

      const contractIdInput = wrapper.find('#b-form-input')

      expect(contractIdInput.attributes().state).toBe('valid')
    })

    it('changes state', () => {
      const wrapper = shallowMount(EnercareLineItem, {
        localVue: Vue,
        data() {
          return {
            productData: { contractId: '1234567' }
          }
        }
      })

      const contractIdInput = wrapper.find('#b-form-input')
      expect(contractIdInput.attributes().state).toBe('invalid')

      wrapper.setData({ productData: { contractId: '12345678' } })
      expect(contractIdInput.attributes().state).toBe('valid')
    })
  })
})

I am not sure why your test was not working, I could not get it running locally without the rest of your project. I am not sure if this is exactly what you are looking for, but it shows how you can check the state is valid or not.

Hopefully this example might give you some ideas?


One unfortunate thing is wrapper.find('#contract-id-input > b-form-input') does not work. I tried hard to solve this in https://github.com/vuejs/vue-test-utils/pull/897, but there are some impossible edge cases to allow selecting by custom component tags. The select API looks like this:

PS: don't forget to pass localVue: Vue to shallowMount. Otherwise you won't be using the localVue instance

mjvezzani commented 6 years ago

Okay, so I wired up my test to work in a similar way that you suggested, @lmiller1990, and I have passing tests. So now I have a question about unit testing philosophy. My understanding is that one should unit test public facing inputs and public facing outputs. I was under the impression that setting up a test in which contractId is not initially populated, and then mimicking user input into the b-form-input field, and then asserting that validity on that field is set is how I wanted to test.

The examples that you provide me exercise an assertion on a wrapper component that already has the internal state set. In other words, in the Assemble, Act, Assert model of unit testing, the example tests Assemble and then Assert, bypassing the need to Act on anything. I'd be curious to hear more about why Assemble and Assert is preferable to Assemble, Act, and Assert. Thanks!

lmiller1990 commented 6 years ago

@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?

mjvezzani commented 6 years ago

@eddyerburgh I would also be curious to hear your thoughts on this.

As an aside to all the previous comments, I purchased the Testing Vue.js Applications (V6) recently, and attempted to use the code in chapter 5 that demonstrates how to test a component when a user inputs data into an input field.

wrapper.find('#my-element').value = '1234567'
wrapper.find('#my-element').trigger('change')

Doesn't seem to work for me. Currently the DOM element that I have my #my-element id on is a b-form-input provided from Bootstrap Vue. Does this pattern for mimicking user behavior in a unit test also work for a b-form-input? If so, that would go a long way in helping me know that this behavior is a result of something that we've done something wrong, and not that unit testing a b-form-input by changing its value with .value = and .trigger('change') inherently doesn't work. Thanks!

lmiller1990 commented 6 years ago

I think that should be trigger('input') if he is updating a <input>. Change is for <select>. (haven't got the book on hand right now, so can't confirm).

mjvezzani commented 6 years ago

Ya. That is how I originally had my code, and it wasn't working, so I attempted to use .trigger('change'), but had the same result.

As another aside, I've been a long time proponent of TDD coming from a background of server side programming languages, but my experience with TDD as it relates to JavaScript frameworks has been less than mediocre. Blog articles that I read (and books for that matter) do a good job of laying out how to TDD an application that is fairly simple, but thus far I've only worked on (perhaps needlessly) complicated projects.

In the current project I am working on, we have built a custom form builder, which has required that we create several base Vue components that get extended by several other components, and our Vuex store has a fair amount of complexity in it as well. I'm finding it more and more difficult to find answers on how to wrap these components with unit tests because I'm finding it impossible to get the component under test to mount or shallowMount so I can actually test it. I fully recognize that this is likely because the application has been written in such a way that it is difficult to test, but what is the path forward? So far I haven't found any good resources on how to begin refactoring a Vue.js (or any JavaScript framework project) so that it is testable.

@lmiller1990 @eddyerburgh any encouraging words for me to maintain my belief/desire to continue practicing TDD?

lmiller1990 commented 6 years ago

@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:

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.

maksnester commented 5 years ago

@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')
juniorgarcia commented 5 years ago

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.

viT-1 commented 4 years ago

@Alendorff My thoughts about all this discussion is: I want to expect 'input' emitted but instaead of this I should emit 'input', but it's not I wanted initially

This test for vue-multiselect is not working =(

it('node_modules vue-multiselect is emitted input by changing prop value', () => {
        expect.assertions(1);

        const wrapper = mount(VueMultiselect, {
            localVue,
            propsData: {
                options: ['foo', 'bar', 'baz'],
            },
        });

        // setValue unsupported, cause root-element
        // wrapper.setValue('bar');
        wrapper.setProps({ value: 'bar' });

        expect(wrapper.emitted('input')).toHaveLength(1);
    });
lucapu88 commented 4 years ago

I am having more or less the same problem that @lmiller1990 solved, I post the link of my question about stack overflow so I avoid rewriting a lot here: https://stackoverflow.com/questions/64572492/unit-test-how-can-i-correctly-trigger-a-trigger-event-on-an-input-which-calls-a

Thanks in advance for your answers.

lmiller1990 commented 4 years ago

Can you share more code? Where is checkSubmitStatus and what does tit look like?

lucapu88 commented 4 years ago

@lmiller1990

checkSubmitStatus is written what it does, it calls updateSubmitDisabled (which is itself in the mutations) to check if the vmodel is greater than 0.

lmiller1990 commented 4 years ago

I will reply in SO

lucapu88 commented 4 years ago

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(); });

robinr commented 4 years ago

Currently have similar issue with the input events been not triggering events, have tried both 'trigger' function and also the dispatchEvent doesn't really work.

GitHUB reference component (AddCustomer ==> InputWidget ==> BFormInput)

[Error log for discussion ] GitHUB reference test spec file

test("Given a  encoded base57-UUID for 'Add Customer->Alphanumeric ID' is input, When the base57-UUID is matching all validation criteria, Then this results in valid condition and feedback text is <empty>", async () => {
const spied_text_validator = jest.spyOn(
      TextValidation.prototype,
      "validate_uuidBase_fiftyseven_encode"
    );

    const input_all_ids = wrapper.findAll("#input-widget");
    const input_alphanumeric_ids = input_all_ids.at(0);
    input_alphanumeric_ids.element.setData(String,uuid_base57);
    input_alphanumeric_ids.element.dispatchEvent(new Event('input')); //  is not a function error received.

    await Vue.nextTick();

    const parent_id_events = input_alphanumeric_ids.emitted("update:value");  // the event sent from the child component
    expect(parent_id_events).toHaveLength(1);    <--- // **error as the response we obtain is undefined**

}
robinr commented 4 years ago

Please let me know how to properly dispatchEvent as its not working the relationship (Parent) AddCustomer ==> (Child) InputWidget ==> (Grand Child) BFormInput

The input event when triggered on instance of the (Child) instance isn't responding with ('update:value')

lmiller1990 commented 4 years ago

So many things are confusing here.

input_alphanumeric_ids.element.setData(String,uuid_base57);

element is an HTML element right? Those don't have setData functions, that's part of Vue Test Utils.

If you would like to update an input with v-model on it, you can use await wrapper.find('#foo').setValue('some value'). Hopefully this will get you going in the right direction - you should not need dispatchEvent or element very often.

robinr commented 4 years ago

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