jest mock vuex useStore() with vue 3 composition api - javascript

I'm trying to unit test a component where you click a button which should then call store.dispatch('favoritesState/deleteFavorite').
This action then calls an api and does it's thing. I don't want to test the implementation of the vuex store, just that the vuex action is called when you click the button in the component.
The Component looks like this
<template>
<ion-item :id="favorite.key">
<ion-thumbnail class="clickable-item remove-favorite-item" #click="removeFavorite()" slot="end" id="favorite-star-thumbnail">
</ion-thumbnail>
</ion-item>
</template>
import {useStore} from "#/store";
export default defineComponent({
setup(props) {
const store = useStore();
function removeFavorite() {
store.dispatch("favoritesState/deleteFavorite", props.item.id);
}
return {
removeFavorite,
}
}
});
The jest test
import {store} from "#/store";
test(`${index}) Test remove favorite for : ${mockItemObj.kind}`, async () => {
const wrapper = mount(FavoriteItem, {
propsData: {
favorite: mockItemObj
},
global: {
plugins: [store]
}
});
const spyDispatch = jest.spyOn(store, 'dispatch').mockImplementation();
await wrapper.find('.remove-favorite-item').trigger('click');
expect(spyDispatch).toHaveBeenCalledTimes(1);
});
I have tried different solutions with the same outcome. Whenever the "trigger('click')" is run it throws this error:
Cannot read properties of undefined (reading 'dispatch') TypeError:
Cannot read properties of undefined (reading 'dispatch')
The project is written in vue3 with typescript using composition API and vuex4

I found a solution to my problem.
This is the solution I ended up with.
favorite.spec.ts
import {key} from '#/store';
let storeMock: any;
beforeEach(async () => {
storeMock = createStore({});
});
test(`Should remove favorite`, async () => {
const wrapper = mount(Component, {
propsData: {
item: mockItemObj
},
global: {
plugins: [[storeMock, key]],
}
});
const spyDispatch = jest.spyOn(storeMock, 'dispatch').mockImplementation();
await wrapper.find('.remove-favorite-item').trigger('click');
expect(spyDispatch).toHaveBeenCalledTimes(1);
expect(spyDispatch).toHaveBeenCalledWith("favoritesState/deleteFavorite", favoriteId);
});
This is the Component method:
setup(props) {
const store = useStore();
function removeFavorite() {
store.dispatch("favoritesState/deleteFavorite", favoriteId);
}
return {
removeFavorite
}
}

Related

Vue Pinia function is undefined in onMounted when unit test is ran

I have a component and a Pinia store which contains a state and some actions. The code works perfectly fine in browser and in E2E (cypress) tests, but fail on unit tests. I'm using vue-testing-utils and vitest.
The store function can be called fine from the unit test when the button is clicked, but if the function is in the mounted or main script, it fails the test
src/components/UsersComponent.vue
<script setup>
import { onMounted } from 'vue'
import { useUsersStore } from '#/stores/users.store'
const usersStore = useUsersStore()
// usersStore.resetStatus() // <- This fails in the unit test
onMounted(() => {
usersStore.resetStatus() // <- This fails in the unit test
})
function changeStatus() {
usersStore.changeStatus() // <- This passes in the unit test
}
</script>
<template>
<div>
<p>Status: {{ usersStore.status }}</p>
<button #click="changeStatus()">Change Status</button>
</div>
</template>
src/stores/users.store.js
import { defineStore } from 'pinia'
import { usersAPI } from '#/gateways'
export const useUsersStore = defineStore({
id: 'users',
persist: true,
state: () => ({
status: 'ready',
}),
getters: {},
actions: {
resetStatus() {
this.status = 'ready'
},
changeStatus() {
this.status = 'loading'
},
},
})
src/components/tests/UsersComponent.spec.js
import { describe, it, expect, vi, beforeEach } from 'vitest'
import { mount } from '#vue/test-utils'
import { createTestingPinia } from '#pinia/testing'
import UsersComponent from '#/components/UsersComponent.vue'
import { useUsersStore } from '#/stores/users.store'
const wrapper = mount(UsersComponent, {
global: {
plugins: [createTestingPinia({ createSpy: vi.fn() })],
},
})
const usersStore = useUsersStore()
describe('UsersComponent', () => {
it('store function is called', async () => {
// arrange
const spy = vi.spyOn(usersStore, 'resetStatus')
const button = wrapper.find('button')
// act
await button.trigger('click')
// assert
expect(spy).toHaveBeenCalled()
})
})
The unit tests return 2 different error. The first is a console log when the function tries to run in onMounted() and the second is what vitest returns.
stderr | unknown test
[Vue warn]: Unhandled error during execution of mounted hook
at <UsersComponent ref="VTU_COMPONENT" >
at <VTUROOT>
FAIL src/components/__tests__/UsersComponent.spec.js [ src/components/__tests__/UsersComponent.spec.js ]
TypeError: usersStore.resetStatus is not a function
❯ src/components/UsersComponent.vue:16:14
16|
17| <template>
18| <div>
| ^
19| <p>Status: {{ usersStore.status }}</p>
20| <button #click="changeStatus()">Change Status</button>
I know this example is a little basic and doesn't really serve a purpose, but I'm wondering how I can have store functions inside the onMounted() (or similar places) without it breaking all my unit tests.
Maybe this can be useful to you:
describe('UsersComponent', () => {
it('changeStatus function is called', async () => {
const wrapper = mount(UsersComponent, {
mounted: vi.fn(), // With this you mock the onMounted
global: {
plugins: [createTestingPinia({
initialState: { // Initialize the state
users: { status: 'ready' },
}
})]
}
})
// Spy the method you call...
const spy = vi.spyOn(wrapper.vm, 'changeStatus');
wrapper.vm.changeStatus()
expect(spy).toHaveBeenCalled()
expect(spy).toHaveBeenCalledTimes(1)
})
})

vue - how to pass callback to vuex action

I am figuring how to pass callback to vuex action
I tried below code but not working. The code run before I fire it
src/store/modules/web3.module.js
import Web3 from "web3";
const state = {};
const getters = {};
const mutations = {};
const actions = {
async test(context, confirmCallback, rejectCallback) {
confirmCallback();
rejectCallback();
}
};
export default {
state,
getters,
actions,
mutations
};
App.vue
<template>
<div id="app"></div>
</template>
<script>
import { mapActions } from "vuex";
export default {
name: "App",
methods: {
...mapActions(["test"]),
onModalOpen() {
console.log("open");
},
onModalClose() {
console.log("Close");
},
},
async created() {
let result = await this.test({
confirmCallback: this.onModalOpen(),
rejectCallback: this.onModalClose(),
});
},
};
</script>
The issue takes place in 2 places:
the payload syntax in your store is wrong
you are firing the functions with the () at the end when passing it through an object:
Solve the payload issue
An action has 2 parameters, first comes the context which is an object that contains the state, mutations etc. then comes the payload which is an object aswell
const actions = {
async test(context, {confirmCallback, rejectCallback}) {
confirmCallback();
rejectCallback();
}
}
Solve the decleration issue
To solve the decleration issue simply remove the () at the end like shown below:
async created() {
let result = await this.test({
confirmCallback: this.onModalOpen,
rejectCallback: this.onModalClose,
});
},

Make shared property reactive in Vue Composition API composable by declaring variable outside of exported function

I am using the composition api plugin for vue2 (https://github.com/vuejs/composition-api) to reuse composables in my app.
I have two components that reuse my modalTrigger.js composable, where I'd like to declare some sort of shared state (instead of using a bloated vuex state management).
So in my components I do something like:
import modalTrigger from '../../../../composables/modalTrigger';
export default {
name: 'SearchButton',
setup(props, context) {
const { getModalOpenState, setModalOpenState } = modalTrigger();
return {
getModalOpenState,
setModalOpenState,
};
},
};
And in my modalTrigger I have code like:
import { computed, ref, onMounted } from '#vue/composition-api';
let modalOpen = false; // needs to be outside to be accessed from multiple components
export default function () {
modalOpen = ref(false);
const getModalOpenState = computed(() => modalOpen.value);
const setModalOpenState = (state) => {
console.log('changing state from: ', modalOpen.value, ' to: ', state);
modalOpen.value = state;
};
onMounted(() => {
console.log('init trigger');
});
return {
getModalOpenState,
setModalOpenState,
};
}
This works, but only because I declare the modalOpen variable outside of the function.
If I use this:
export default function () {
const modalOpen = ref(false); // <------
const getModalOpenState = computed(() => modalOpen.value);
...
It is not reactive because the modalTrigger is instantiated twice, both with it's own reactive property.
I don't know if that is really the way to go, it seems, that I am doing something wrong.
I also tried declaring the ref outside:
const modalOpen = ref(false);
export default function () {
const getModalOpenState = computed(() => modalOpen.value);
But this would throw an error:
Uncaught Error: [vue-composition-api] must call Vue.use(plugin) before using any function.
So what would be the correct way to achieve this?
I somehow expected Vue to be aware of the existing modalTrigger instance and handling duplicate variable creation itself...
Well, anyway, thanks a lot in advance for any hints and tipps.
Cheers
Edit:
The complete header.vue file:
<template>
<header ref="rootElement" :class="rootClasses">
<button #click="setModalOpenState(true)">SET TRUE</button>
<slot />
</header>
</template>
<script>
import { onMounted, computed } from '#vue/composition-api';
import subNavigation from '../../../../composables/subNavigation';
import mobileNavigation from '../../../../composables/mobileNavigation';
import search from '../../../../composables/searchButton';
import { stickyNavigation } from '../../../../composables/stickyNav';
import metaNavigation from '../../../../composables/metaNavigation';
import modalTrigger from '../../../../composables/modalTrigger';
export default {
name: 'Header',
setup(props, context) {
const { rootElement, rootClasses } = stickyNavigation(props, context);
mobileNavigation();
subNavigation();
search();
metaNavigation();
const { getModalOpenState, setModalOpenState } = modalTrigger();
onMounted(() => {
console.log('Header: getModalOpenState: ', getModalOpenState.value);
setModalOpenState(true);
console.log('Header: getModalOpenStat: ', getModalOpenState.value);
});
return {
rootClasses,
rootElement,
getModalOpenState,
setModalOpenState,
};
},
};
</script>
The composition API is setup somewhere else where there are Vue components mounted a bit differently than you normally would.
So I can't really share the whole code,but it has this inside:
import Vue from 'vue';
import CompositionApi from '#vue/composition-api';
Vue.use(CompositionApi)
The composition API and every other composable works just fine...

How to stub a module function with Cypress?

I want to create a test with Cypress that has a React component that uses an auth library (#okta/okta-react) with a HOC (withOktaAuth).
My component looks like this:
// Welcome.js
import { withOktaAuth } from '#okta/okta-react'
const Welcome = ({authState}) => {
return <div>{authState.isAuthenticated ? 'stubbed' : 'not working'}</div>
}
export default withOktaAuth(Welcome)
I tried to make a test like so:
// test.js
import * as OktaReact from '#okta/okta-react'
const withOktaAuthStub = Component => {
Component.defaultProps = {
...Component.defaultProps,
authState: {
isAuthenticated: true,
isPending: false
},
authService: {
accessToken: '123'
}
}
return Component
}
describe('Test auth', () => {
before(() => {
cy.stub(OktaReact, 'withOktaAuth').callsFake(withOktaAuthStub)
})
it('Stubs auth', () => {
cy.visit('/welcome')
cy.contains('stubbed')
})
})
When I run the test, the component still does not use the stubbed function. Any help is very much appreciated!
It's been 2 years that this questions was submitted, but for those who still encounters that error, Cypress provides a guide about Okta e2e testing: https://docs.cypress.io/guides/end-to-end-testing/okta-authentication#Adapting-the-back-end

Mocking Vue class component methods with inject loader

Let's say I have a very basic vue-class-component as shown below:
<template>
<div>Nothing of interest here</div>
</template>
<script>
import Vue from 'vue';
import Component from 'vue-class-component';
import http from './../modules/http';
#Component
export default class Example extends Vue {
user = null;
errors = null;
/**
* #returns {string}
*/
getUserId() {
return this.$route.params.userId;
}
fetchUser() {
this.user = null;
this.errors = null;
http.get('/v1/auth/user/' + this.getUserId())
.then(response => this.user = response.data)
.catch(e => this.errors = e);
}
}
</script>
I want to test the fetchUser() method so I just mock the './../modules/http' dependency and make http.get return a Promise. The problem is that in my assertion I want to check if the URL is being built properly and in order to do so the user ID has to come from an hard-coded variable in the test.
I tried something like this but it doesn't work:
import ExampleInjector from '!!vue-loader?inject!./../../../src/components/Example.vue';
const mockedComponent = ExampleInjector({
'./../modules/http': {
get: () => new Promise(/* some logic */)
},
methods: getUserId: () => 'my_mocked_user_id'
});
Unfortunately it doesn't work and I could not find anything this specific in the Vue docs so the question is, how am I supposed to mock both external dependencies and a class component method?
NOTE: I do not want to mock this.$route.params.userId as the userId could potentially come from somewhere else as well (plus this is just an example). I just want to mock the getUserId method.
Since I specifically asked about how I could mock Vue class component methods with the inject loader here's the complete solution for the question at hand:
import ExampleInjector from '!!vue-loader?inject!./../../../src/components/Example.vue';
const getComponentWithMockedUserId = (mockedUserId, mockedComponent = null, methods = {}) => {
if (!mockedComponent) {
mockedComponent = ExampleInjector();
}
return Vue.component('test', {
extends: mockedComponent,
methods: {
getUserId: () => mockedUserId,
...methods
}
});
};
And then in my test case:
const component = getComponentWithMockedUserId('my_mocked_user_id');
const vm = new Vue(component);
I found this to be very helpful when you need to create a partial mock AND inject some dependencies too.
The easiest way to do this is to extend the component and override the method:
const Foo = Vue.extend({
template: `<div>{{iAm}}</div>`,
created() {
this.whoAmI();
},
methods: {
whoAmI() {
this.iAm = 'I am foo';
}
},
data() {
return {
iAm: ''
}
}
})
Vue.component('bar', {
extends: Foo,
methods: {
whoAmI() {
this.iAm = 'I am bar';
}
}
})
new Vue({
el: '#app'
})
In this example I'm using the extends property to tell Vue that Bar extends Foo and then I'm overriding the whoAmI method, you can see this is action here: https://jsfiddle.net/pxr34tuz/
I use something similar in one of my open source projects, which you can check out here. All I'm doing in that example is switching off the required property for the props to stop Vue throwing console errors at me.

Categories

Resources