Been reading up a lot of stackoverflow and github discussions, about vue jest having trouble with button.trigger('click'). I've been struggling over this issue for a few hours today, have to say I'm quite frustrated, and surprised how a simple function such as trigger('click') can cause so much problems.
In short, my code has a b-button, which #click fires off fetchData function, from vuex. This works perfectly well in browser, but in testing mode, the fetchData does not get executed.
Vue Component Code
<template>
<b-button id="loadButton" variant="outline-primary" #click="fetchData">Load Data</b-button>
</template>
<script>
import { mapActions } from 'vuex';
export default {
name: "LoadAndSave",
methods: { ...mapActions(['fetchData']) }
}
</script>
Testing Code
import { shallowMount, createLocalVue } from '#vue/test-utils'
import Vuex from 'vuex'
import { BootstrapVue } from 'bootstrap-vue'
import LoadAndSave from '../../resources/components/LoadAndSave'
const localVue = createLocalVue()
localVue.use(BootstrapVue)
localVue.use(Vuex)
describe('LoadAndSave.vue', () => {
let actions
let getters
let store
beforeEach(() => {
actions = {
fetchData: jest.fn()
}
store = new Vuex.Store({
actions
})
})
it('LoadAndSave: onClick executes fetchData', async () => {
const wrapper = shallowMount(LoadAndSave, { localVue, store })
const button = wrapper.find("#loadButton")
await button.trigger('click')
expect(actions.fetchData).toHaveBeenCalled()
})
})
Result of testing
expect(jest.fn()).toHaveBeenCalled()
Expected number of calls: >= 1
Received number of calls: 0
This is not my first day coding, I am no where near an expert in coding either, but just the idea of not being able to get a button click to trigger really sends chills down my spine, not to mention the frustration accompanied.
If anyone could give any suggestion that'd be appreciated, thank you.
Codey
I have been struggling with this as well, it seems sometimes the test component has a hard time finding the emitting/calls to functions that do not have parentheses.
it('Calls save() when pressing save button', async () => {
const savebutton = wrapper.find('#saveButton')
const spy = jest.spyOn(wrapper.vm, 'save')
savebutton.trigger('click')
await wrapper.vm.$nextTick()
expect(spy).toHaveBeenCalled()
jest.restoreAllMocks()
})
the above test will fail here:
<button class="btn btn-success" id="saveButton" #click="save">Save</button>
but not here:
<button class="btn btn-success" id="saveButton" #click="save()">Spara</button>
You can at-least check if this is your problem, make a reference to the store function in methods and call the function with parentheses on the element.
Another way of asserting the button-click has been fired is by looking at the emitted object.
it('Calls save() when pressing save button', () => {
const savebutton = wrapper.find('#saveButton')
savebutton.trigger('click')
expect(wrapper.emitted('save')).toHaveLength(1)
})
Related
Every time I fetch data, I want to change boolean value to render <Loading /> component.
I don't want my condition to be dependant on array length. So I decided to do it this way.
And <Loading /> component never reacts to state.isLoading change.
I tried to test whether this.isLoading changes at all using watch. But watch never logged anything.
I've never seen anybody using watch with primitives.
The problem is that I don't know if I can use watch with primitives and what I can use instead, like useEffect in React.
App.vue
<script setup>
import { RouterView } from 'vue-router'
import { watch, ref, onMounted, reactive } from 'vue';
import Navbar from './components/Navbar.vue'
import { useShopStore } from './stores/shopStore';
const shop = useShopStore()
const bool = ref(shop.isLoading)
console.log(bool)
watch(bool.value, (newBool) => {
console.log(newBool)
}, { deep: true })
</script>
Category.vue
<template>
<LoadingVue v-if="shop.isLoading" />
<div v-else class="category__menu">
<CardVue
v-for="item in shop.category"
:item="item"
:key="item.id"
/>
</div>
</template>
ShopStore.js
actions: {
async getProducts(path) {
if (typeof path !== 'string' || path === undefined) return
this.setLoading()
try {
const response = fetch(`https://fakestoreapi.com/products/category/${path}`)
.then(res => res.json())
.then(res => this.category = res)
} catch (error) {
console.log(error)
alert('Something went wrong')
}
this.setLoading()
},
setLoading() {
console.log('setLoading')
this.isLoading = !this.isLoading
}
}
You are creating a new ref over a reactive data. It's like copying by value, the original reactive data and the new ref wrapped over it are not connected. So when shop.isLoading changes, your bool ref doesn't, they are two different variables now.
I guess you are using pinia for the store. If so, the shop.isLoading is already reactive, you don't have to wrap it into a ref.
<Loading v-model="shop.isLoading" />
You can also use storeToRefs helper method from pinia to use destructuring over your store and get refs of your state:
const { isLoading } = storeToRefs(shop)
console.log(isLoading.value)
So.
The problem was that I used async but I didn't use await inside the function and that's why condition worked the way it worked. Or didn't work as I expected.
Now I fixed it and I want to publicly admit that I am a complete moron.
Thank you for your attention.
P.S.
Still didn't figure out how to use watch. The only way is to watch the whole state object. watch doesn't react to only state.bool value change.
I have pretty much the same example from the docs of watch and vue router for composition API. But console.log is never getting triggered even though the route.params.gameId is correctly displayed in template
<script setup>
import { ref, watch } from 'vue'
import { useRoute } from 'vue-router'
import { useStore } from 'vuex'
const store = useStore()
const route = useRoute()
watch(
() => route.params.gameId,
async newId => {
console.log("watch"+newId)
}
)
</script>
<template>
<div>
{{route.params.gameId}}
</div>
</template>
What am I doing wrong? I also tried making watch function non async but it didn't change anything and later on I will need it for api fetching so it should be async.
You should add immediate: true option to your watch :
watch(
() => route.params.gameId,
async newId => {
console.log("watch"+newId)
},
{
immediate: true
}
)
Because the watch doesn't run at the first rendering while the params is changes only one time at the page load, So the watch misses that change.
I try to use spyOn to spy the functions and it's implementation. However, i got this error. "Cannot spyOn on a primitive value; undefined given".
I already read the documentation of jest.spyOn in https://jestjs.io/docs/en/jest-object . But it keeps showing the same errror... is there anything that i should add and improve?
below is the code
<template>
<div>
<form #submit.prevent="onSubmit(inputValue)">
<input type="text" v-model="inputValue">
<span class="reversed">{{ reversedInput }}</span>
</form>
</div>
</template>
<script>
import axios from 'axios';
export default {
props: ['reversed'],
data: () => ({
inputValue: '',
results: [],
}),
methods: {
onSubmit(value) {
const getPromise = axios.get(
'https://jsonplaceholder.typicode.com/posts?q=' + value,
);
getPromise.then(results => {
this.results = results.data;
});
return getPromise;
},
},
};
</script>
while the test code is
import axios from 'axios'; // axios here is the mock from above!
import { shallowMount } from '#vue/test-utils';
import Form from '#/components/Form.vue';
describe('Form.test.js', () => {
const wrapper;
describe('Testing Submit events', () => {
wrapper = shallowMount(Form);
it('calls submit event', () => {
const onSubmit = jest.spyOn(Form.prototype, 'onSubmit') // mock function
// updating method with mock function
wrapper.setMethods({ onSubmit });
//find the button and trigger click event
wrapper.findAll('form').trigger('submit');
expect(onSubmit).toBeCalled();
})
});
})
Can you also vrief me what and how to use spyOn to test the method?
Thank you so much
Best regards
Lughni
Component definition suggests that Form is an object. Form.prototype === undefined because Form is not a function. Since Vue class components aren't in use, nothing suggests the opposite.
It can be spied as:
jest.spyOn(Form.methods, 'onSubmit')
This should be done prior to component instantiation. And spyOn with no implementation provided creates a spy, not a mock.
v-menu uses a transition internally to show the given Element when the activator is triggered.
We simply want to test if the menu element is shown after the activator hit it.
In our setup we have a component test using Vue Test Utils and do:
const wrapper = createWrapper(...)
expect(wrapper.contains('.my-class')).toBe(false) // the Element is not there in the beginning
clickTheTriggerElement(wrapper) // trigger v-menu to show
await wrapper.vm.$nextTick()
expect(wrapper.find('.my-class').isVisible()).toBe(true) // fails
This fails because the transition used by v-menu is not finished yet. (It would after a setTimeout(...), thou but this is less acceptable).
Vue Test Utils describes this as a common issue and provides a solution that seems to work for plain vue.js Transitions. (They use a mock for Transition to render immediately.) Unfortunately this doesn't seem to work with internal transitions of vuetify.
How would you handle that?
I wrote unit-test for v-list inside v-menu and I didn't mock transitions. This is my code:
//ACTION
const button = wrapper.find('.theme-picker');
await button.trigger('click');
const topic = wrapper.find('.v-list-item');
await topic.trigger('click');
const result = wrapper.vm.$router;
//ASSERT
expect(result).toContainEqual(expected);
If you have some async behaviour which you can't 'reach' you can use:
test('description', (done) => {
wrapper.vm.$nextTick(() => {
expect(result).toBe(expected);
done();
});
});
It work for Jest and Mocha. Test will wait until async code finish.
May be it will help someone.
I don't think it's the transitions that are causing the problem here. You should be able to disable transitions completely with the following code:
import { config } from '#vue/test-utils';
config.stubs.transition = false;
I tried this, and it did not help my situation. What I ended up doing was stub out VMenu entirely. I created a component stub somewhere only accessible to tests, e.g. tests/unit/__stubs__/VMenuStub.vue
<template>
<div class="v-menu-stub">
<slot
name="activator"
:on="{
click: toggleValue,
}"
></slot>
<slot v-if="value"></slot>
</div>
</template>
<script lang="ts">
/**
* VMenuStub - used to stub out v-menu
* as a workaround for issues dealing with v-menu directly in a unit test
* example usage: use the "stubs" option for mount
* componentWrapper = mount(component, {
* ... other options, like localVue, vuetify ...
* stubs: {
* 'v-menu': VMenuStub,
* },
* };
*/
import { defineComponent } from '#vue/composition-api';
export default defineComponent({
props: {
value: {
type: Boolean,
required: true,
},
},
setup(props, context) {
function toggleValue() {
context.emit('input', !props.value);
}
return {
toggleValue,
};
},
});
</script>
I can use this stub like so:
import VMenuStub from '../../__stubs__/VMenuStub';
it('test', () => {
componentWrapper = mount(component, {
... other options, like localVue, vuetify ...
stubs: {
'v-menu': VMenuStub,
},
};
});
once the stub is added, whichever button I have set up to trigger menu open/close will work as expected.
given my component to test has a menu with activator slot:
<v-menu
v-model="menu"
>
<template #activator="{ on, attrs }">
<v-btn v-bind="attrs" v-on="on">
ClickMe
</v-btn>
</template>
I can open the menu by triggering click via trigger('click') or vm.$emit('click')
it('test', () => {
componentWrapper = mount(component, {
... other options, like localVue, vuetify ...
stubs: {
'v-menu': VMenuStub,
},
};
const menuButton = componentWrapper.findAll('.v-btn').filter(i => i.text().includes('ClickMe')).at(0);
// either of these will work
await menuButton.trigger('click');
// OR
await menuButton.vm.$emit('click');
// and I can verify
expect(componentWrapper.vm.menu).toEqual(true);
});
I'm using bootstrap as my design framework and have been using bootstrap-vue. Now I would like to implement some tests to go along with my components. I'm writing a very simple test to make sure a modal is opened. What do I use in vue-test-utils to open the bootstrap-vue modal?
I'm using the basics that come with Laravel, bootstrap-vue, vue-test-utils, mocha, and mocha-webpack.
I'm trying to open the modal with wrapper.find('#modal-1').trigger('click'). I've tried using a directive <b-button v-b-modal.modal-1> I've tried using an event <b-button #click="$bvModal.show('modal-1')">. And lastly, I tried a regular button <button #click="showModal = true"> with this on the b-modal <b-modal v-model="showModal">. I've also tried adding a $nextTick in between the trigger and the assertion.
import { createLocalVue, mount } from '#vue/test-utils';
import expect from 'expect';
import BootstrapVue from 'bootstrap-vue';
import MyComponent from '#/components/MyComponent.vue';
const localVue = createLocalVue();
localVue.use(BootstrapVue);
describe ('MyComponent', () => {
let wrapper;
beforeEach(() => {
wrapper = mount(QuotesExceptions, {
localVue
});
});
it ('opens a modal', () => {
expect(wrapper.contains('#modal-1')).toBe(false);
wrapper.find('#btnShow').trigger('click');
expect(wrapper.contains('#modal-1')).toBe(true);
});
});
I'm expecting the modal to be in the wrapper with expect(wrapper.contains('#modal-1')).toBe(true) and this is where the assertion is failing.
Use the attachToDocument: true mount option, as modal needs to be in the document in order to open.
You can see how BootstrapVue tests their modals at https://github.com/bootstrap-vue/bootstrap-vue/blob/dev/src/components/modal/modal.spec.js
I was looking at the bootstrap-vue test on github as Troy suggested (https://github.com/bootstrap-vue/bootstrap-vue/blob/dev/src/components/modal/modal.spec.js)
There you can see that they are using the prop static: true. Adding this to my code solved my problem.
component.vue
<b-modal
v-model="showModal"
id="myModal"
data-qa="importModal"
:static="true"
>
</b-modal>
component.spec.js
it ('opens a modal', (done) => {
const button = wrapper.find('[data-qa="button"]');
const modal = wrapper.find('#myModal');
expect(button.exists()).toBe(true);
expect(button.is('button')).toBe(true);
expect(modal.exists()).toBe(true);
expect(modal.is('div')).toBe(true);
expect(modal.isVisible()).toBe(false);
button.trigger('click');
Vue.nextTick(() => {
expect(modal.isVisible()).toBe(true);
done();
});
});
I had to select the modal by id because the inner part is getting display: none. When I put a data-qa on the modal it sticks on the outer element which is not hidden itself. Another solution for that would be to select it the following way:
const modal = wrapper.find('[data-qa="modal"] .modal');
But I still get the following warning in my console:
[BootstrapVue warn]: observeDom: Requires MutationObserver support.
it ('opens a modal', (done) => {
const button = wrapper.find('[data-qa="button"]');
expect(!!document.querySelector('myModal')).toBe(false)
button.trigger('click');
expect(!!document.querySelector('myModal')).toBe(true)
});
I'm using jest to test if a modal appears when a button is clicked adn ended up with related problem. Will leave my answer here for furture readers.
I was trying to do something like:
let openModalBtn = wrapper.find('button[id="openModal"]');
expect(openModalBtn.exists()).toBe(true); // OK
expect(openModalBtn.is("button")).toBe(true); // OK
await deleteBtn.trigger("click"); // BANG
Leading to this:
[Vue warn]: Error in v-on handler (Promise/async): "TypeError: this.$refs.confirmDialogue[0].show is not a function"
The thing was that I was using shallowMount:
// not good
describe("some test", () => {
let wrapper;
beforeEach(() => {
wrapper = shallowMount(MyComponent, {...});
});
And at some point of MyComponent, it needs to do something like:
...
methods: {
openModal() {
await this.$refs.confirmDialogue[0].show();
...
},
Therefore this.$ref was comming as undefined.
Using mount instead:
// good
describe("some test", () => {
let wrapper;
beforeEach(() => {
wrapper = mount(MyComponent, {...});
});
allowed me to click that button and find the stub within the wrapper.html().
So make sure to mount your components if you gonna need to access stubs from it.
In my case this work perfectly,
Here I have a b-modal in template with a id="modal-1" and when the button is clicked bootstrap-vue modal is opened using showModal() method.
Try this:
<template>
<b-button v-on:click="showModal()">
<b-modal id="modal-1"></b-modal>
</template>
<script>
methods: {
showModal() {
this.$root.$emit("bv::show::modal", 'modal-1', "#btnShow");
},
}
</script>