vuejs / vue-test-utils

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

@click event cannot be tested with a find-trigger function in Jest #929

Closed TheoSl93 closed 6 years ago

TheoSl93 commented 6 years ago

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.

eddyerburgh commented 6 years ago

First, have you tried this with beta.24?

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

TheoSl93 commented 6 years ago

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

miriamgreis commented 6 years ago

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.

TheoSl93 commented 6 years ago

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

Thanks!!

lmiller1990 commented 6 years ago

@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)
  })
TheoSl93 commented 6 years ago

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

eddyerburgh commented 6 years ago

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

AlexZhong22c commented 5 years ago

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

AlexZhong22c commented 5 years ago

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?

BorisAng commented 5 years ago

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.

lmiller1990 commented 5 years ago

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).

BorisAng commented 5 years ago

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

nickelaos commented 4 years ago

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

nickelaos commented 4 years ago

@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

lmiller1990 commented 4 years ago

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

nickelaos commented 4 years ago

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

lmiller1990 commented 4 years ago

@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).

nickelaos commented 4 years ago

@lmiller1990 Didn't help: 1 2

lmiller1990 commented 4 years ago

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

ped59430 commented 4 years ago

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?

ped59430 commented 4 years ago

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

ped59430 commented 4 years ago

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

gtempesta commented 4 years ago

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.

lmiller1990 commented 4 years ago

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.

gtempesta commented 4 years ago

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

dobromir-hristov commented 4 years ago

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

gtempesta commented 4 years ago

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

dobromir-hristov commented 4 years ago
merge(yourComponent, { methods: { myMethod: () => jest.fn() } }
lmiller1990 commented 4 years ago

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

gtempesta commented 4 years ago

Ok, that's why it gave an error:

ReferenceError: merge is not defined

gtempesta commented 4 years ago

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>
ovuefe commented 3 years ago

Good day, I am have issue testing click on my application, I have tried all the methods mention here; none is working . Please can anyone provide me an example to follow, I will really appreciate Thanks.

gtempesta commented 3 years ago

@ovuefe Did you try following this Stack Overflow question with the answer?

ovuefe commented 3 years ago

Yes, this is my test it('delete assignment', async ()=>{ const btnDelete= jest.spyOn(wrapper.vm, 'handleDeleteAssignment'); wrapper.find('#deleteAssignment').trigger('click') expect(btnDelete).toHaveBeenCalled()

})

This is the error message Error: expect(jest.fn()).toHaveBeenCalled()

Expected number of calls: >= 1 Received number of calls: 0

gtempesta commented 3 years ago

You have to instantiate the spy before the wrapper, and so the method you spy should be taken directly from the component and not from the wrapper, like this:

btnDeleteSpy = jest.spyOn(Modal.methods, 'handleDeleteAssignment');

Here is an example taken from a real test, with some variables renamed after your example and some details omitted:

import { shallowMount } from '@vue/test-utils';
import Modal from '@/components/modals/Modal.vue';

describe('Modal.vue', () => {
    let wrapper;
    let btnDeleteSpy;
    beforeEach(() => {
        // create the spy before instantiating the wrapper
        btnDeleteSpy = jest.spyOn(Modal.methods, 'handleDeleteAssignment');
       // instantiate the wrapper
        wrapper = shallowMount(Modal);
    });
    afterEach(() => {
        // this is to count correctly how many times a function has been called
        jest.clearAllMocks();
    });

    test('delete assignment', () => {
        const $btnDiscard = wrapper.find('#deleteAssignment');
        $btnDiscard.trigger('click');
        expect(btnDeleteSpy).toHaveBeenCalled();
    });

});
ovuefe commented 3 years ago

This is my entire code

handleDeleteAssignment = jest.spyOn(Assignment.methods,'handleDeleteAssignment')
        wrapper = shallowMount(Assignment,{localVue,store,
            data(){
            return{
                loading:false,
                fileTextSvg:'',
                all_assignment:['name']

            }
            },
        mocks:{
            $route,
        },

        })
    })
    afterEach(()=>{
        jest.clearAllMocks()
    })

it('delete assignment', async ()=>{
        const $btnDiscard = wrapper.find('#deleteAssignment');
        $btnDiscard.trigger('click');
        expect(handleDeleteAssignment).toHaveBeenCalled();

    })

The same result: --->

Error: expect(jest.fn()).toHaveBeenCalled() Expected number of calls: >= 1 Received number of calls: 0
lmiller1990 commented 3 years ago

Please share your entire component, I'll make a recommendation on how to test it. There is probably a better way than using spy.

ovuefe commented 3 years ago

Good morning @lmiller1990 and the entire team This is the entire test component

import Vue from "vue"
import {createLocalVue, mount, shallowMount} from "@vue/test-utils";
import AssignmentAssessment
    from "@/trainingProvider/components/classes/class/components/assessment/AssignmentAssessment";
import Assignment from "@/trainingProvider/components/classes/class/components/assignment/Assignment";
import Vuetify from "vuetify";
import Vuex from "vuex";
import VueRouter from 'vue-router';
import {ValidationObserver,ValidationProvider} from 'vee-validate'
import flushPromises from 'flush-promises'
import {greaterThan} from "simple-vue-validator/src/templates";
import {methods} from "vue-json-to-csv/src/utils/helpers";

const localVue = createLocalVue();
localVue.use(Vuex);
Vue.use(Vuetify)
// Vue.use(Vuetify);
// Vue.use(VueRouter)
// const router = new VueRouter();

describe("Assignment assessment test",()=>{
    let wrapper;

    let store;
    let $route;
    let classAssessment;
    let handleDeleteAssignment;

    beforeEach(()=>{
        classAssessment ={
            namespaced : true,
            getters:{
                all_assignment : jest.fn()
            },
            state:{
                all_assignment : ['name']
            },
            actions:{
                getAllAssignment : jest.fn(() => Promise.resolve()),
                handleStopAssignment : jest.fn(()=> Promise.resolve()),
            },

        }
        $route={
            params:{
                id: '198',
                    name : 'sweet boy'
            }
        }

        store = new Vuex.Store({
            modules:{
                classAssessment
            }
        })
        handleDeleteAssignment = jest.spyOn(Assignment.methods,'handleDeleteAssignment')
        wrapper = shallowMount(Assignment,{localVue,store,
            data(){
            return{
                loading:false,
                fileTextSvg:'',
                all_assignment:['name']

            }
            },
        mocks:{
            $route,
        },

        })
    })
    afterEach(()=>{
        jest.clearAllMocks()
    })
    it('should mount assignment component', function () {
        expect(wrapper.exists).toBeTruthy()
    });
    it('due date should be one day ahead of today', function () {
        let today = new Date();
        let dueDate = new Date();
        dueDate.setDate(today.getDate()+1)
        wrapper.vm.$data.date=dueDate.getDate().toLocaleString()
        console.log(wrapper.vm.$data.date)
        expect(Number.parseInt(wrapper.vm.$data.date)).toBeGreaterThan(today.getDate())

    });
    it('all assignment to be called once ', async function () {
        expect(classAssessment.actions.getAllAssignment.mock.calls).toHaveLength(1)
    });
    it('delete assignment', async ()=>{
        const $btnDiscard = wrapper.find('#deleteAssignment');
        $btnDiscard.trigger('click');
        expect(handleDeleteAssignment).toHaveBeenCalled();

    })
    it('should click delete assignment button',()=>{
        expect(wrapper.find('#deleteAssignment').trigger('click')).toBeTruthy()
    })
})
lmiller1990 commented 3 years ago

I cannot run this code as is - it has a bunch of unknown imports.

I'll need something I can run locally, unforutantely I don't have time to figure out the config etc to run this example.

lobo-tuerto commented 2 years ago

You probably need to add parentheses to the event handler on the child component. For example when testing from a parent component that a child emits an event and calls the appropriate function...

This works:

<MainTopbar @account="handleTopbarAccountClick()" />

This does not:

<MainTopbar @account="handleTopbarAccountClick" />