Reset Vue for every jest test? - vue.js

I am using Vue JS with #vue/test-utils and jest. For my tests I am calling:
let localVue = createLocalVue();
vueMount(MyComponent, { localVue: localVue, options });
The problem is, I am referencing libraries which does stuff like this:
import Vue from 'vue'
import Msal from 'vue-msal'
//...
Vue.use(Msal, {...});
The Vue.use() registers some global stuff on the prototype, etc. For testing purposes, I need this to start fresh each test. The only thing I could think of is to use mockImplementation() with jest on the Vue object. But I am not quite sure how I could accomplish that, if at all possible.
Is there any way to do this? Thanks!

It took me a while, but here is how I solved this...
let setupComplete = false;
let setupFailure = false;
let testContext = {};
function resetTestContext() {
Object.keys(testContext).forEach(function(key) { delete testContext[key]; });
}
function createTestContext(configureTestContext) {
beforeEach(() => {
jest.isolateModules(() => {
setupFailure = true;
jest.unmock('vue');
resetTestContext();
testContext.vueTestUtils = require('#vue/test-utils');
testContext.vue = testContext.vueTestUtils.createLocalVue();
jest.doMock('vue', () => testContext.vue);
testContext.vuetify = require('vuetify');
testContext.vue.use(testContext.vuetify);
testContext.vuetifyInstance = new testContext.vuetify();
if (configureTestContext) {
configureTestContext(testContext);
}
setupComplete = true;
setupFailure = false;
});
});
afterEach(() => {
setupComplete = false;
resetTestContext();
jest.resetModules();
setupFailure = false;
});
return testContext;
},
What made this possible was the jest.isolateModules() method. With this approach, Vue and it's prototype, and also Vuetify, are completely recreated and brand new with each test case.
For it to work, the test spec and the library containing the utility above may not 'import' Vue or any module which depends on Vue. Instead, it needs to be required in the configureTestContext() function or in the test case itself.
My test specs look like this:
import createTestContext from '#/scripts/createTestContext'
describe('sample', () => {
const testContext = createTestContext(function configureTestContext(testContext)
{
testContext.vueDependency = require('#/scripts/vueDependency').default;
});
test('demo', () => {
// I added a helper to more easily do this in the test context...
const sample = testContext.vueTestUtils.mount(require('#/components/Sample').default, {
localVue: testContext.vue,
vuetify: testContext.vuetifyInstance
});
expect(testContext.vueDependency.doSomething(sample)).toBe(true);
expect(sample.isVueInstance()).toBeTruthy();
});
});

import { shallowMount, createLocalVue } from '#vue/test-utils';
import Vuex from 'vuex';
const localVue = createLocalVue();
const { user } = require('./customer.mock');
const originUser = { ...user };
const resetUserData = wrapper => {
wrapper.setData( { userData: originUser } );
};
const TestComponent = localVue.component( 'TestComponent', {
name : 'TestComponent',
data : () => {
return { userData: user };
},
render( createElement ) {
return createElement( 'h3', 'hoy hoy' );
},
} );
describe( 'computed fields', () => {
afterEach( () => {
resetUserData( wrapper );
} );
it( 'isPrivatePerson should return false', () => {
wrapper.setData( { userData: { Contacts: [{ grpid: 'bad field' }] } } );
expect( !wrapper.vm.isPrivatePerson ).toBeTruthy();
} );
it( 'isPrivatePerson should return true', () => {
expect( wrapper.vm.isPrivatePerson ).toBeTruthy();
} );
});

Related

Vue unit test is holds old component code in wrapper

I am doing unit test of vue component methods through vue-test-utils and facing an weird issue. wrapper.vm.somemthod() is executing old code that was written earlier inside the method. It's printing old console.log statements. If I put new console.log statement, it's not printing that at all. Am I missing something?
import { mount, shallowMount } from '#vue/test-utils'
import TestComponent from '#/components/TestComponent.vue'
const mockMixin = {
methods: {
InnerMethod() {
return 2;
},
}
}
describe('Test Screen', () => {
let wrapper;
let mock;
beforeAll(() => {
mock = new MockAdapter(api);
})
beforeEach(() => {
jest.resetModules();
wrapper = mount(TestComponent, {
i18n,
vuetify,
mixins: [mockMixin],
data() {
},
mocks: {
$t: (msg) => msg,
$config: (value) => value,
$store: (value) => value,
$route: (value) => value,
}
});
})
afterEach(() => {
mock.reset();
wrapper.destroy();
wrapper = null;
});
describe('Component Test', () => {
it('getdata', async () => {
expect(wrapper.vm.somedata.length).toBe(0);
const spy = jest.spyOn(wrapper.vm, 'InnerMethod');
wrapper.vm.someMethod();
expect(spy).toBeCalledTimes(1);
expect(wrapper.vm.somedata.length).toBe(2);
});
});
});

Use props in composables vue3

I am upgrading an app from vue 2 to vue 3 and I am having some issues with composables. I'd like to use props in the composable but it doesn't seem to be working. The code sample is pulled from a working component and works fine when I leave it in the component.
I assume defineProps isn't supported by composables, but then I am unclear how to handle it. When I pass the src in the parameters it loses its reactivity.
// loadImage.js
import { defineProps, onMounted, ref, watch } from 'vue'
// by convention, composable function names start with "use"
export function useLoadImage() {
let loadingImage = ref(true)
let showImage = ref(false)
const props = defineProps({
src: String,
})
const delayShowImage = () => {
setTimeout(() => {
showImage.value = true
}, 100)
}
const loadImage = (src) => {
let img = new Image()
img.onload = (e) => {
loading.value = false
img.onload = undefined
img.src = undefined
img = undefined
delayShowImage()
}
img.src = src
}
onMounted(() => {
if (props.src) {
loadImage(props.src)
}
})
watch(
() => props.src,
(val) => {
if (val) {
loadingImage.value = true
loadImage(val)
}
},
)
// expose managed state as return value
return { loadingImage, showImage }
}
Edit
This method worked for me, but the two methods mentioned in the comments below did not.
I have a new question here.
// loadImage.js
import { onMounted, ref, watch } from 'vue'
// by convention, composable function names start with "use"
export function useLoadImage(props) {
let loadingImage = ref(true)
let showImage = ref(false)
const delayShowImage = () => {
setTimeout(() => {
showImage.value = true
}, 100)
}
const loadImage = (src) => {
let img = new Image()
img.onload = (e) => {
loading.value = false
img.onload = undefined
img.src = undefined
img = undefined
delayShowImage()
}
img.src = src
}
onMounted(() => {
if (props.src) {
loadImage(props.src)
}
})
watch(
() => props.src,
(val) => {
if (val) {
loadingImage.value = true
loadImage(val)
}
},
)
// expose managed state as return value
return { loadingImage, showImage }
}
<script setup>
import { defineProps, toRef } from 'vue'
import { useLoadImage } from '../../composables/loadImage'
const props = defineProps({
src: String
})
const { loading, show } = useLoadImage(props)
</script>
According to official docs :
defineProps and defineEmits are compiler macros only usable inside <script setup>
You should pass the props as parameter without destructing them to not loose the reactivity :
export function useLoadImage(props) {
....
}
you can use useRef to pass specific props without losing reactivity
const imgRef = toRef(props, "img");
const { loding, show } = useLoadImage(imgRef);

Vuex + Jest + Composition API: How to check if an action has been called

I am working on a project built on Vue3 and composition API and writing test cases.
The component I want to test is like below.
Home.vue
<template>
<div>
<Child #onChangeValue="onChangeValue" />
</div>
</template>
<script lang="ts>
...
const onChangeValue = (value: string) => {
store.dispatch("changeValueAction", {
value: value,
});
};
</scirpt>
Now I want to test if changeValueAction has been called.
Home.spec.ts
...
import { key, store } from '#/store';
describe("Test Home component", () => {
const wrapper = mount(Home, {
global: {
plugins: [[store, key]],
},
});
it("Test onChangeValue", () => {
const child = wrapper.findComponent(Child);
child.vm.$emit("onChangeValue", "Hello, world");
// I want to check changeValueAction has been called.
expect(wrapper.vm.store.state.moduleA.value).toBe("Hello, world");
});
});
I can confirm the state has actually been updated successfully in the test case above but I am wondering how I can mock action and check if it has been called.
How can I do it?
I have sort of a similar setup.
I don't want to test the actual store just that the method within the component is calling dispatch with a certain value.
This is what I've done.
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
}
}
Hope this will help you further :)

Why can't I use dragula in Vue3 setup but mounted?

When I use dragula in vue3 setup. It isn't working. Like this:
setup() {
const dragFrom = ref(null);
const dragTo = ref(null);
onMounted(() => {
dragula([dragFrom, dragTo], {
copy: (el) => {
console.log(el);
return true;
},
accepts: () => {
return true;
},
});
});
return { dragFrom, dragTo };
}
But this way can be successful:
mounted() {
const dragFrom = this.$refs.dragFrom;
const dragTo = this.$refs.dragTo;
dragula([dragFrom, dragTo], {
copy: function (el, source) {
console.log(el);
return true;
},
accepts: function (el, target) {
return true;
},
});
}
Both methods are based on vue3.What's wrong?
Your issue comes from the fact that you are not accessing the value of the ref, i.e. dragFrom.value and dragTo.value when passing them into the dragula() function. Remember that when you create a reactive and mutable ref object, you will need to access its inner value using the .value property.
This should therefore work:
setup() {
const dragFrom = ref(null);
const dragTo = ref(null);
onMounted(() => {
// Ensure you access the VALUE of the ref!
dragula([dragFrom.value, dragTo.value], {
copy: (el) => {
console.log(el);
return true;
},
accepts: () => {
return true;
},
});
});
return { dragFrom, dragTo };
}
See proof-of-concept on this demo CodeSandbox I've created: https://uwgri.csb.app/

How can I test my custom react hook that uses fetch?

I have created a custom react hook which uses fetch from whatwg-fetch. I have tests for the components that make use of the hook and can mock the whole hook, but now am trying to write tests for the hook itself and my goal is to mock the fetch response. This is my hook.
import { useState, useEffect } from "react";
import "whatwg-fetch";
export default function useFetch(url) {
const [data, setData] = useState(undefined);
const [response, setResponse] = useState(undefined)
const [isLoading, setLoading] = useState(true);
const [error, setError] = useState(undefined);
useEffect(() => {
try {
const fetchData = async () => {
const result = await fetch(url);
setResponse(result);
const responseText = await result.text();
setData(responseText);
setLoading(false);
};
fetchData();
} catch (error) {
setError(error);
}
}, [url]);
return { data, response, isLoading, error };
}
export { useFetch }
Currently, this is how my test looks like. Feels like I cannot mock the fetch to return the desired value.
I have tried writing tests by looking at several tutorials with no luck. I have tried the following tutorials:
Test Custom Hooks Using React Hooks Testing Library
Testing custom react hooks with jest
A Complete Guide to Testing React Hooks
UPDATE: Changed tests, my first test passes (resolve) my second one does not. Based on the third tutorial.
NEW TESTS
import React from "react";
import { render, unmountComponentAtNode } from "react-dom";
import { act } from "react-dom/test-utils";
import useFetch from "./useFetch";
function sleep(ms) {
return new Promise((resolve) => setTimeout(resolve, ms));
}
let container = null;
describe("useFetch tests", () => {
beforeEach(() => {
// setup a DOM element as a render target
container = document.createElement("div");
document.body.appendChild(container);
});
afterEach(() => {
// cleanup on exiting
unmountComponentAtNode(container);
container.remove();
container = null;
});
it("useFetch returns data on success", async () => {
function fetchMock(url) {
return new Promise((resolve) => setTimeout(() => {
resolve({
status: 200,
ok: true,
text: () => Promise.resolve({
data: "data from api"
})
});
}, 300));
}
jest.spyOn(global, "fetch").mockImplementation(fetchMock);
act(() => {
render(<TestComponent url="url1" />, container);
});
expect(container.textContent).toBe("loading");
await sleep(500);
expect(container.textContent).toBe("data from api");
});
it("useFetch return error on fail", async () => {
// const a = 200 + Math.random() * 300;
// console.log(a)
// let promise = new Promise(function (resolve, reject) {
// // after 1 second signal that the job is finished with an error
// setTimeout(() => reject("error"), a);
// });
// function fetchMock(url) {
// return promise;
// }
function fetchMock(url) {
return new Promise((resolve) => setTimeout(() => {
resolve({
status: 404,
ok: true,
text: () => Promise.resolve({
data: "data from api"
})
});
}, 200 + Math.random() * 300));
}
jest.spyOn(global, "fetch").mockImplementation(fetchMock);
act(() => {
render(<TestComponent url="url1" />, container);
});
expect(container.textContent).toBe("loading");
await sleep(500);
expect(container.textContent).toBe("error");
});
});
function TestComponent({ url }) {
const {
data, response, isLoading, error
} = useFetch(url);
if (isLoading) {
return <div>loading</div>;
}
if (data) {
return <div>{data.data}</div>
}
if (error) {
return <div>error</div>
}
return <div></div>;
}
OLD TESTS
import { useFetch } from "../../../src/utils/custom-hooks/useFetch";
describe("useFetch tests", () => {
beforeEach(() => {
jest.spyOn(window, "fetch");
});
it("200", () => {
window.fetch.mockResolvedValueOnce({
ok: true,
status: 200,
})
const { result, rerender } = renderHook(
(url) => useFetch(url),
{
url: "url1"
}
);
expect(result.current.response).toBe(undefined);
rerender("url2");
expect(result.current.status).toBe(200);
});
});