Vue-test-utils: @click event cannot be tested with a find-trigger function in Jest

Created on 17 Aug 2018  路  31Comments  路  Source: vuejs/vue-test-utils

Version

1.0.0-beta.19

Reproduction link

https://codesandbox.io/s/2vlyrzkm1p

Steps to reproduce

The next test in jest does not work with the code in the example:

it('calls the toggleVisibility method when clicking', () => {
    wrapper = shallowMount(App)
    wrapper.vm.toggleVisibility = jest.fn()

    wrapper
      .find('.unread-messages')
      .trigger('click')

    expect(wrapper.vm.toggleVisibility).toHaveBeenCalledTimes(1)
  })

Only if click is changed click.self does the test work, but in that case no component inside can be clicked (and that's what I want to achieve)

What is expected?

The test pass whether click or click.self is wrote.

What is actually happening?

The test fails.


A console.log of the wrapper shows that it has the class in the wrapper, so it should be found and triggered.

need repro

Most helpful comment

@TheoSl93 I pulled your repo and got it working. You can use setMethods, then your test passes.

  it('calls the toggleVisibility method when the icon is clicked', () => {
    wrapper.setMethods({ toggleVisibility:jest.fn() })

    wrapper
      .find('.unread-messages')
      .trigger('click')

    expect(wrapper.vm.toggleVisibility).toHaveBeenCalledTimes(1)
  })

All 31 comments

First, have you tried this with beta.24?

If the problem persists, can you provide a runnable reproduction?

Hi,
I've tried with beta.24 but the problem persists. I'll make a repository that you can fork.
Thanks!

Hey, we just updated to beta.24 and have the same problem. We had to refactor some of our tests using trigger('click') to make them work again.

Here is a repo with a minimal runnable reproduction: https://github.com/TheoSl93/vueUtils-repro-929

Thanks!!

@TheoSl93 I pulled your repo and got it working. You can use setMethods, then your test passes.

  it('calls the toggleVisibility method when the icon is clicked', () => {
    wrapper.setMethods({ toggleVisibility:jest.fn() })

    wrapper
      .find('.unread-messages')
      .trigger('click')

    expect(wrapper.vm.toggleVisibility).toHaveBeenCalledTimes(1)
  })

It works! Thanks a lot for the tip. What's the difference between the two methods, by the way?

setMethods updates the instance, which re render vnodes (and ultimately Elements) to use the new method in their on handlers.

I'm going to close this issue, since we aren't able to change this behavior in Vue Test Utils.

The recommended way to set a method is to use setMethods

Problem has been resolved. I'm using beta.28. I just want to know why we should using setMethods update the instance?

In the code example above, why don't wrapper.find('.unread-messages').trigger('click') share the same instance?

This is not a issue, its just a question @eddyerburgh

https://github.com/vuejs/vue-test-utils/issues/983#issuecomment-425468635

Why we need to re render the component after stubing a components methods?

Stub replace the Object reference and Vue is not reactive to the change of Object, which is similar to the princeple for normal Object defined as Vue data.

Is my assumption right?

Hi everyone,

I'm using beta.29 and this problem still seems to persist.

I have the following template

<template>
    <b-input-group class="sm-2 mb-2 mt-2">
      <b-form-input
        :value="this.searchConfig.Keyword"
        @input="this.updateJobsSearchConfig"
        class="mr-2 rounded-0"
        placeholder="Enter Search term..."
        id="input-keyword"
      />
      <b-button
        @click="searchJobs"
        class="rounded-0"
        variant="primary"
        id="search-button"
      >
        Search
      </b-button>
      <b-button
        @click="resetFilter"
        class="rounded-0 ml-2"
        variant="primary"
        id="reset-button"
      >
        Reset
      </b-button>
    </b-input-group>
</template>

My tests are:

it('should call searchJobs method on search button click event', async () => {
    wrapper.find('#search-button').trigger('click')
    expect(await searchJobs).toHaveBeenCalled()
  })

 //it('should call resetFilter method on reset button click event', () => {
    //wrapper.find('#reset-button').trigger('click')
    //expect(resetFilter).toHaveBeenCalled()
 // })

  it('should call resetFilter method on reset button click event', () => {
    wrapper.setMethods({ resetFilter: jest.fn() })
    wrapper.find('#reset-button').trigger('click')
    expect(wrapper.vm.resetFilter).toHaveBeenCalled()
  })

The first test passes successfully and it has searchJobs mocked inside shallow mounted wrapper like so

wrapper = shallowMount(JobsSearch, {
      methods: {
        updateJobsSearchConfig,
        searchJobs,
        resetFilter,
        emitEvents
      },
      localVue,
      store })

As you can see I, I tried mocking resetFilter and running the same test as with searchJobs but calling the correct function which didn't work. Then I tried using setMethods as @lmiller1990
lmiller1990 suggested but Im still getting 'Expected mock function to have been called, but it was not called.' on the second test.

Any help will be greatly appreciated.

Hey @BorisAng , can you post the entire test (with how you do jest.fn) etc? Or - ideally a repo that I can pull and repro the error would be ideal. The code looks okay, but it's possible something else is incorrect (or there is a bug).

@lmiller1990 here is the test file:

import { shallowMount, createLocalVue } from '@vue/test-utils'
import Vuex from 'vuex'
// BootstrapVue is necessary to recognize the custom HTML elements
import BootstrapVue from 'bootstrap-vue'
import JobsSearch from '@/components/jobs/JobsSearch.vue'

const localVue = createLocalVue()
localVue.use(Vuex)
localVue.use(BootstrapVue)

describe('JobsSearch.vue', () => {
  let actions
  let state
  let store
  let wrapper
  let updateJobsSearchConfig
  let searchJobs
  let resetFilter
  let emitEvents

  beforeEach(() => {
    state = {
      jobs: {
        paged: {
          size: 100,
          page: 1
        },
        search: {
          Keyword: '',
          status: [],
          ucode: []
        }
      }
    }

    actions = {
      updateJobsPagedConfig: jest.fn()
    }

    store = new Vuex.Store({
      actions,
      state
    })

    updateJobsSearchConfig = jest.fn()
    searchJobs = jest.fn()
    resetFilter = jest.fn()
    emitEvents = jest.fn()

    wrapper = shallowMount(JobsSearch, {
      methods: {
        updateJobsSearchConfig,
        searchJobs,
        resetFilter,
        emitEvents
      },
      localVue,
      store })
  })

  afterEach(() => {
    wrapper.destroy()
  })

// Tests go here, I have provided them above
})

The JobsSearch component also uses Vuex and has some actions, so I have mocked these. As you can see, I am mocking searchJobs and resetFilter in exactly the same way and then running pretty much the same tests just using the two functions respectively.

I have a similar problem. I don't understand why my cancelAlert() method is not called in tests... The strangest thing is that the code inside the function is executed...
1
2
3
4
Please, advice. @eddyerburgh

@TheoSl93 I pulled your repo and got it working. You can use setMethods, then your test passes.

  it('calls the toggleVisibility method when the icon is clicked', () => {
    wrapper.setMethods({ toggleVisibility:jest.fn() })

    wrapper
      .find('.unread-messages')
      .trigger('click')

    expect(wrapper.vm.toggleVisibility).toHaveBeenCalledTimes(1)
  })

Why do I have to set a method which already exists in a wrapper instance? @lmiller1990 @eddyerburgh

@nickelaos What happens if you do wrapper.vm.$options.methods.cancelAlert? Not exactly sure why, but that might work. Just doing a console.log on wrapper.vm.cancelAlert vs wrapper.vm.$options.methods.cancelAlert:

[Function: bound greet] and [Function: greet] respectively. Can you try that? That might help you too, @nickelaos .

@lmiller1990 Sorry, I didn't get your point. I can call wrapper.vm.cancelAlert(), and the test passes in this case. But I don't want to call it directly. I want to check if it is called after the button is clicked. It is not. That is the problem.

@nickelaos maybe I misunderstood. You said that you do jest.spyOn(wrapper.vm, 'cancelAlert'), then call shouldHaveBeenCalled() and it is false. I was suggesting you try spyOn(wrapper.vm.$options.methods, 'cancelAlert') - although this might be incorrect. I believe Vue saves all methods in an $options key under the hood, and if you want to do spyOn, you need to spyOn that method, not vm.cancelAlert (which likely calls $options.methods.cancelAlert in the end).

@lmiller1990 Didn't help:
1
2

Hm, not too sure then... sorry. If you can make a minimal repo, I can try debugging a little more.

Same here. Bound method is not called when triggering the click on the button. Data attributes are not updated, etc...
I don't understand as it feels rather straight forward. Is there anything I miss?

Upgraded from 3.12.0 to 4.0.5, but does nothing.
I run inside docker, with node:lts-alpine.

If you shallowMount and if you're button comes from a framework, then it is stubbed and the click action is not accessible.

Luckily I already had a test in which a click event was triggered, and I was able to understand the _very subtle_ difference between tests passing and tests failing: it's the _parenthesis_ in the function call.

So basically the very same test:

test('Click calls the right function', () => {
    // wrapper is declared before this test and initialized inside the beforeEach
    wrapper.vm.testFunction = jest.fn();
    const $btnDiscard = wrapper.find('.btn-discard');
    $btnDiscard.trigger('click');
    expect(wrapper.vm.testFunction).toHaveBeenCalled();
});

was failing with this template:

<button class="btn blue-empty-btn btn-discard" @click="testFunction">
  {{ sysDizVal('remove') }}
</button>

while it passed with this _subtle_ change:

<button class="btn blue-empty-btn btn-discard" @click="testFunction()">
  {{ sysDizVal('remove') }}
</button>

I've seen that in the original post the parenthesis were missing, so I assume this answers the question.

I hope this behavior will be fixed in future releases because the syntax without parenthesis is somehow encouraged in all the docs I have seen.

Hope this helps.

When I do that and remove the (), the mock is not even applied; my original method is triggered. 馃

I am not sure overriding a method by doing wrapper.vm.testMethod is advisable. Modifying internals, unsuprisingly, leads to unexpected side effects.

Overriding a method dynamically like this is not documented here or in Vue's core docs from what I can see. Vue has some magic to call functions even if you invoke them without () as a @click listeners (passing $event as the first arg), and I suspect by reassigning vm.testMethod to jest.fn that magic goes away.

What I think we need is a better way of testing this; the current behavior is not really a bug per-se, but a result of how Vue works. If you want to assert something happens when a method is called, ideally you would assert the effect of that method. Eg, if it is an API call with axios, you can do jest.mock and assert that module was called.

According to the docs, the official way to mock a method is:

wrapper.setMethods({ clickMethod: clickMethodStub })

But in the same docs they say that it is deprecated.

Anyway, if I test he function mocking it like this:

wrapper.vm.testFunction = jest.fn();

or like this:

wrapper.setMethods({ testFunction: jest.fn() });`

the issue is still the same: my test succeeds if the method is called in the template with the parenthesis, otherwise it fails.

Official syntax (deprecated), with parenthesis in the template
Schermata 2020-07-02 alle 12 05 52

Official syntax (deprecated), without parenthesis in the template
Schermata 2020-07-02 alle 12 10 35

Just replace it before mounting. This is not an advisable way to test anyway, like Lachlan said.

@dobromir-hristov How do you replace a method before mounting? Can you show me an example?

merge(yourComponent, { methods: { myMethod: () => jest.fn() } }

Note: merge is likely from the lodash library. It is not part of VTU.

Ok, that's why it gave an error:

ReferenceError: merge is not defined

Ok thanks to this answer on Stack Overflow I've found how to mock the method before mounting:

mock + mount

const testFunctionSpy = jest.spyOn(Component.methods, 'testFunction');
const wrapper = shallowMount(Component);

In this way I can test that the function is called even without the parentheses.

spec

expect(testFunctionSpy).toHaveBeenCalled();

template

<button class="btn blue-empty-btn btn-discard" @click="testFunction">
  {{ sysDizVal('remove') }}
</button>
Was this page helpful?
0 / 5 - 0 ratings