Unable to test a lodash debounced vue method - vue.js

I've been trying to test a confirmation button using jest and vue-test-utils. I'm using a debounced method to handle accidental double-clicking.
I've tried using jest.useFakeTimers() and jest.runOnlyPendingTimers(), but I'm still not seeing my button text changing in my tests. I've created an isolated test file below - any advice on what I'm doing wrong here would be greatly appreciated!
import { shallowMount, createLocalVue } from '#vue/test-utils'
import _ from 'lodash'
jest.useFakeTimers()
const myComponent = {
data(){
return {
confirming: false
}
},
computed: {
state(){
return this.confirming ? 'are you sure?' : 'default'
}
},
template: `<div #click="changeState">{{ state }}</div>`,
methods: {
changeState(){
this.doConfirm()
},
doConfirm: _.debounce(function(){
if(!this.confirming){
this.confirming = true
}else{
this.confirming = false
}
}, 500)
}
}
describe('Testing debounce methods', () => {
let $wrapper
beforeEach(() => {
$wrapper = shallowMount(myComponent)
})
test('Check default state', () => {
expect($wrapper.text()).toContain('default')
})
test('Check state changes with click', () => {
$wrapper.trigger('click')
jest.runOnlyPendingTimers()
expect($wrapper.text()).toContain('are you sure?')
})
})

Related

Jest: How I should change the mock data of Vuex in each test?

I've been working in a test where I need the data from Vuex. However, I'm having some problems, I need to change that data in each test in order to test the functionality of the component.
Here is my component:
<template>
<div id="cb-items-displayer" #click="textClick">
<span>(</span>
<p>{{ text }}</p>
<span>)</span>
</div>
</template>
<script lang="ts" setup>
import { capitalize } from '#/utils/capitalize'
import { ItemsDisplayer } from '#/models/ItemsDisplayer'
import { computed, PropType } from 'vue'
import { useStore } from 'vuex'
const store = useStore()
const props = defineProps({
type: {
type: String,
default: '',
},
menuType: {
type: String,
default: '',
},
items: {
type: Array as PropType<ItemsDisplayer[]>,
default: () => [],
}
})
const emit = defineEmits<{
(event: 'textClicked'): void
}>()
const text = computed(() => {
const param = props.menuType === 'radio' ? 'One' : 'Many'
console.log( "TYPEEE ", props.type, " ", param )
const itemsIds = store.getters['filters/get' + capitalize(props.type) + param]
console.log("ITEMSSS", JSON.stringify(itemsIds))
return getTextToShow(itemsIds)
})
const getTextToShow = (itemsIds: string) => {
//TODO - improve it
if (itemsIds === 'all') {
return 'all'
} else if (itemsIds.length === 0) {
return '-'
} else if (itemsIds.length === 1) {
return getName(itemsIds[0], props.items)
} else {
return itemsIds.length
}
}
const textClick = () => {
emit('textClicked')
}
const getName = (id: string, items: ItemsDisplayer[]) => {
const found: ItemsDisplayer = items.find((x) => x.id! === id) as ItemsDisplayer
console.log("GETNAME ", found.name)
return found?.name
}
</script>
And this is the test:
import { render, screen, click, waitFor } from '#tests/app-test-utils'
import ItemsDisplayer from './ItemsDisplayer.vue'
import { capitalize } from '#/utils/capitalize'
let mockStoreCommit: jest.Mock
jest.mock('vuex', () => ({
...jest.requireActual('vuex'),
useStore: () => ({
getters: {
[`filters/get${capitalize('categories')}Many`]: [],
},
commit: mockStoreCommit,
}),
}))
describe('ItemsDisplayer', () => {
beforeEach(() => {
mockStoreCommit = jest.fn()
render(
ItemsDisplayer,
{},
{
props: {
type: 'categories',
menuType: 'checkbox',
items: [
{
box_templates:"",
id:"1",
name:"Culture"
},
{
box_templates:"",
id:"2",
name:"Economy"
},
{
box_templates:"",
id:"3",
name:"Education"
}
]},
}
)
})
it('renders the component', async() => {
await screen.getByText('-')
})
it('renders the component with one item', async() => {
//DON'T WORK HERE THERE SHOULD BE A CHANGE OF DATA IN THE MOCKED STORE IN ORDER TO WORK
await screen.getByText('Culture')
})
})
My problem is that I need to change the value of [filters/get${capitalize('categories')}Many] to ["1"] in the second test.
I tried several things in order to change the mocked data but they don't work. How can I change the mocked store data in each test?
Thanks!
You can achieve this by lazy loading your vue component:
Add jest.resetModules(); in the beforeEach to reset all of the imported modules before each test so they can be re-evaluated and re-mocked:
beforeEach(() => {
jest.resetModules();
In each unit test, you will first need to import the vue component using the require syntax as follows:
const ItemsDisplayer = require('./ItemsDisplayer.vue').default;
Then add the mock directly after the import with the [`filters/get${capitalize('categories')}Many`] value being set to whatever you want it to be:
jest.mock('vuex', () => ({
...jest.requireActual('vuex'),
useStore: () => ({
getters: {
[`filters/get${capitalize('categories')}Many`]: ["1"],
},
commit: mockStoreCommit,
}),
}));
I have noticed that you do your rendering in the beforeEach. Unfortunately because you import and mock your modules during the test, the rendering will need to be done after these have taken place - hence you will need to either move that logic into your unit test or extract it into another function which can be called from within the unit test.
Each unit test should look something like this:
it('renders the component', async() => {
const ItemsDisplayer = require('./ItemsDisplayer.vue').default;
jest.mock('vuex', () => ({
...jest.requireActual('vuex'),
useStore: () => ({
getters: {
[`filters/get${capitalize('categories')}Many`]: ["1"],
},
commit: mockStoreCommit,
}),
}));
// beforeEach logic here or a call to a function that contains it
await screen.getByText('-')
})

Vue.js/nuxt.js - test a dynamically added method inside a component

I'm trying to make a test for dynamically created methods in one of my components the code goes like this.
<template>
<div id="app">
<div #click="executeDynamic('myCustomFunction')">Click me!</div>
</div>
</template>
<script>
export default {
name: "App",
data () {
return {
// These contain all dynamic user functions
userFuncs: {}
}
},
created () {
window.setTimeout(() => {
this.$set(this.userFuncs, 'myCustomFunction', () => {
console.log('whoohoo, it was added dynamically')
})
}, 2000)
},
methods: {
executeDynamic (name) {
if (this.userFuncs[name]) {
this.userFuncs[name]()
} else {
console.warn(`${name} was not yet defined!`)
}
}
}
};
</script>
test file
import WorkDateTime from "#/components/WorkDateTime.vue"
import Vue from "vue"
describe("WorkDateTime.vue", () => {
it("allowedDatesFrom: today -> NG", () => {
const that = {
$set: Vue.set
}
expect(WorkDateTime.data.userFuncs['myCustomFunction']).toBeTruthy()
})
}
code pen
https://codesandbox.io/s/vue-template-forked-ec7tg?file=/src/App.vue:0-662
Try something like that:
import { shallowMount } from '#vue/test-utils';
import WorkDateTime from '#/components/WorkDateTime.vue';
describe('WorkDateTime.vue', () => {
it('userFuncs empty', () => {
let wrapper = shallowMount(WorkDateTime);
expect(wrapper.vm.userFuncs).toStrictEqual({});
});
it('userFuncs filled', async () => {
let wrapper = shallowMount(WorkDateTime);
let wait3Seconds = () => new Promise(resolve => setTimeout(() => resolve(), 3000));
await wait3Seconds();
expect(wrapper.vm.userFuncs['myCustomFunction']).toBeInstanceOf(Function);
});
});

How to mock VueAxios in jest

I want to test my Api functions which are on separate file outside vue component. Inside this methods i call api by Vue.axios, and i can't find the way to mock and test it like in this post:
How do I test axios in jest
example method:
cancelAuction: function (auction_id) {
if (validateApiInt(auction_id)) {
return Vue.axios.delete(`/auctions/${auction_id}`);
}
return {};
},
example usage:
const response = await AuctionApi.cancelAuction(id);
Ok that was pretty obvious. I had to mock whole Vue like below:
jest.mock('vue', () => ({
axios: {
get: jest.fn()
},
}));
Just start learning Jest + #vue/test-utils. Here is a simple example for those people want to mock "vue-axios".
// #/components/Helloword.vue
<template>
<div>
<h1>Email: <span>{{ email }}</span></h1>
<button #click="fetchData">Get Random Email</button>
</div>
</template>
<script>
export default {
name: 'HelloWorld',
data() {
return {
email: '',
};
},
methods: {
async fetchData() {
const res = (await this.axios.get('https://randomuser.me/api/')).data
.results[0].email;
this.email = res;
},
},
};
</script>
--
// test/unit/example.spec.js
import { mount } from '#vue/test-utils';
import HelloWorld from '#/components/HelloWorld.vue';
import axios from 'axios';
jest.mock('axios', () => ({
get: () =>
Promise.resolve({
data: {
results: [{ email: 'mockAxios#email.com' }],
},
}),
}));
describe('HelloWorld.vue', () => {
it('click and fetch data...', async (done) => {
const wrapper = mount(HelloWorld, {
mocks: {
axios,
},
});
await wrapper.find('button').trigger('click');
wrapper.vm.$nextTick(() => {
expect(wrapper.find('h1').text()).toContain('#');
done();
});
});
});

Testing a vue app with nuxt, vuex and jest gettig not supported error

I want to Unit test my component which mutates an object in a store module when a button is clicked.
I followed the article by brandon aaskov on how to unit test nuxt plus vuex, but I'm not able to reference the nuxt store Object.
I always get an output like this:
Error: Not supported
at Object.<anonymous> (...\tests\unit\plannerObjectSelector.spec.js:67:5)
> (the line NuxtStore = await import(storePath);)
at Object.asyncJestLifecycle (...\node_modules\jest-jasmine2\build\jasmineAsyncInstall.js:53:37)
at ...\node_modules\jest-jasmine2\build\queueRunner.js:43:12
at new Promise (<anonymous>)
at mapper (...\node_modules\jest-jasmine2\build\queueRunner.js:26:19)
at ...\node_modules\jest-jasmine2\build\queueRunner.js:73:41
This is the Component i want to test:
<template>
<v-container>
<v-row v-for="object in plannerObjects" :key="object.id">
<v-btn
v-if="object.id == activeObjectId"
color="primary"
class="button"
block
large
tile
#click="objectSelected(object)"
>
{{ object.name }}
</v-btn>
<v-btn
v-else
color="primary"
block
large
text
#click="objectSelected(object)"
:ref="'unSelectedBtn-' + object.id"
>
{{ object.name }}
</v-btn>
</v-row>
</v-container>
</template>
<script>
export default {
name: "spaceSelector",
props: {
plannerObjects: {
type: Array,
required: true
}
},
data: () => ({
activeObjectId: -1
}),
methods: {
objectSelected(object) {
console.log("Sroe object", object);
this.$store.commit("planner/setActivePlannerObject", object);
this.activeObjectId = object.id;
console.log(this.$store.getters["planner/activePlannerObject"]);
}
}
};
</script>
<style scoped>
.button {
width: 190px;
}
</style>
```
this is my jest.config.js file:
module.exports = {
preset: "#vue/cli-plugin-unit-jest/presets/no-babel",
setupFiles: ["<rootDir>/tests/unit/index.js"],
globalSetup: "<rootDir>/jest.setup.js"
};```
jest.setup.js:
import { Nuxt, Builder } from "nuxt";
import nuxtConfig from "./nuxt.config";
// these boolean switches turn off the build for all but the store
const resetConfig = {
loading: false,
loadingIndicator: false,
fetch: {
client: false,
server: false
},
features: {
store: true,
layouts: false,
meta: false,
middleware: false,
transitions: false,
deprecations: false,
validate: false,
asyncData: false,
fetch: false,
clientOnline: false,
clientPrefetch: false,
clientUseUrl: false,
componentAliases: false,
componentClientOnly: false
},
build: {
indicator: false,
terser: false
}
};
// we take our nuxt config, lay the resets on top of it,
// and lastly we apply the non-boolean overrides
const config = Object.assign({}, nuxtConfig, resetConfig, {
mode: "spa",
srcDir: nuxtConfig.srcDir,
ignore: ["**/components/**/*", "**/layouts/**/*", "**/pages/**/*"]
});
const buildNuxt = async () => {
const nuxt = new Nuxt(config);
await new Builder(nuxt).build();
return nuxt;
};
module.exports = async () => {
const nuxt = await buildNuxt();
// we surface this path as an env var now
// so we can import the store dynamically later on
process.env.buildDir = nuxt.options.buildDir;
};
unit/index.js
import Vue from "vue";
import Vuetify from "vuetify";
Vue.config.productionTip = false;
Vue.use(Vuetify);
and finally my test class:
import { shallowMount } from "#vue/test-utils";
import plannerObjectSelector from "../../components/core/bars/planner/plannerObjectSelector";
import { __createMocks as createStoreMocks } from "../../store";
import _ from "lodash";
import { createLocalVue } from "#vue/test-utils";
import Vuex from "vuex";
import Vuetify from "vuetify";
var plannerObjects = [
{
id:0
}
];
const factory = () => {
return shallowMount(plannerObjectSelector, {
propsData: {
plannerObjects: plannerObjects
}
});
};
describe("plannerObjectSelector.vue", () => {
const localVue = createLocalVue();
localVue.use(Vuex);
localVue.use(Vuetify);
// to use Store
let NuxtStore;
let store;
beforeAll(async () => {
// note the store will mutate across tests
const storePath = `${process.env.buildDir}/store/index.js`;
NuxtStore = await import(storePath);
});
beforeEach(async () => {
store = await NuxtStore.createStore();
});
it("renders", () => {
const wrapper = factory();
expect(wrapper.exists()).toBe(true);
});
it("buttonClickedStoresObjectInStore", () => {
const wrapper = factory();
var btnref = "unSelectedBtn-0";
const btn = wrapper.find({ ref: btnref });
btn.trigger("click");
// look whats in our Store
let plannerObject = store.getters["planner/activePlannerObject"];
console.log(plannerObject);
expect(plannerObject).toBe(plannerObjects[0]);
});
test("mounts properly", () => {
const wrapper = factory();
expect(wrapper.isVueInstance()).toBeTruthy();
});
test("renders properly", () => {
const wrapper = factory();
expect(wrapper.html()).toMatchSnapshot();
});
});
And this is my folder structure:
I would be thankful for any advice.
It turns out pretty simple, just test the function directly, even though I had banged my head for some hours by going through the documents just like you.
// ~/store/getters.ts
import { GetterTree } from 'vuex'
import { RootState } from '~/store/state'
export default {
[Getters.KEY.IS_SIGN_IN]: (state: RootState): boolean => {
return Boolean(state.currentUser)
}
} as GetterTree<RootState, RootState>
// ~/store/__tests__/getters.test.js (do not use .ts)
import state from '~/store/state'
import getters, { Getters } from '~/store/getters'
describe('getters', () => {
it(Getters.KEY.IS_SIGN_IN, () => {
// currentUser: undefined
expect(getters[Getters.KEY.IS_SIGN_IN](state)).toBe(false)
// SignIn: true
state.currentUser = { ...$mockData.currentUser }
expect(getters[Getters.KEY.IS_SIGN_IN](state)).toBe(true)
})

Vue Test Utils mount in multiple tests

I am testing my Vue App using Vue Test Utils and Jest. Below is my dashboard component.
<template>
<div class="dashboard-v2">
<div class="component-container">
<component :loading="loading" :key="identifier" :is="currentTab" />
</div>
<SnackBar
v-on:snackBarHide="displaySnackBar = false"
:text="snackBarText"
:show="displaySnackBar"
:type="snackBarType"
/>
</div>
</template>
<script>
import { mapState } from "vuex";
import "#/shared/chart-kick";
import EventBus from "#/shared/event-bus";
import Tabs from "./helpers/Tabs";
import Summary from "./Summary/Index";
import { filters } from "../helpers/filters-details";
import SnackBar from "#/shared/components/SnackBar.vue";
export default {
components: {
Tabs,
Summary,
SnackBar
},
data() {
return {
identifier: +new Date(),
loading: false,
filtersLoading: false,
displaySnackBar: false,
snackBarText: "",
snackBarType: ""
};
},
mounted() {
if (!this.projects.length) this.fetchFilterData();
EventBus.$on("CLEAR_ALL", () => {
this.identifier = +new Date();
this.$store.commit(`dashboardV2/UPDATE_FILTER_STATE`, {});
});
EventBus.$on("filterChange", () => {
this.getExecData();
});
},
computed: {
...mapState("dashboardV2", [
"projects",
"currentTab",
"selectedFilters",
"timeFilter"
])
},
methods: {
fetchFilterData() {
this.filtersLoading = true;
this.$store
.dispatch("dashboardV2/GET_EXEC_FILTER_DATA")
.catch(() => {
this.displaySnackBar = true;
this.snackBarText = "There was some problem while fetching data";
this.snackBarType = "failure";
})
.finally(() => {
this.filtersLoading = false;
});
this.getExecData();
},
getExecData() {
this.loading = true;
let params = {
time_bucket: this.timeFilter,
time_zone_offset: new Date().getTimezoneOffset()
};
filters.map(e => {
params[e.query] = this.selectedFilters[e.value]
? this.selectedFilters[e.value].id
: null;
});
this.$store
.dispatch("dashboardV2/GET_EXEC_DATA", params)
.catch(() => {
this.displaySnackBar = true;
this.snackBarText = "There was some problem while fetching data";
this.snackBarType = "failure";
})
.finally(() => (this.loading = false));
}
}
};
</script>
<style lang="scss" scoped>
#import "#/styles/dashboard.scss";
</style>
Then this is my test file
import Main from "../Main.vue";
import mergeWith from "lodash.mergewith";
import { customizer, createWrapper } from "#/shared/test-helper";
import Vuex from "vuex";
import EventBus from "#/shared/event-bus";
let GET_EXEC_DATA = jest.fn(() => Promise.resolve());
let GET_EXEC_FILTER_DATA = jest.fn(() => Promise.resolve());
export const createStore = (overrides) => {
let storeOptions = {
modules: {
dashboardV2: {
namespaced: true,
state: {
projects: [],
currentTab: "",
selectedFilters: {},
timeFilter: "",
},
actions: {
GET_EXEC_DATA,
GET_EXEC_FILTER_DATA,
},
},
},
};
return new Vuex.Store(mergeWith(storeOptions, overrides, customizer));
};
describe("Loads Main Dashboard", () => {
it("should fetch chart data and filter data", () => {
createWrapper({}, Main, createStore());
expect.assertions(2);
expect(GET_EXEC_DATA).toBeCalled();
expect(GET_EXEC_FILTER_DATA).toBeCalled();
});
it("should call fetch chart data when filter changed", () => {
createWrapper({}, Main, createStore());
EventBus.$emit("filterChange");
expect.assertions(1);
expect(GET_EXEC_DATA).toBeCalledTimes(2);
});
});
My first test is running successfully but my second test is failing because GET_EXEC_DATA is being called 4 times instead of 2 times. Is it because it's being called once in the first test. Then, How do I avoid this?
Actually, I was able to solve this by clearing the mock functions
afterEach(() => {
jest.clearAllMocks();
});