How to unit test inner function(not return) of setup in Vue3? - vue.js

I want to test like this.
Case 1: Error
Cannot spy the inner property because it is not a function; undefined given instead.
Component.vue
export default {
setup() {
function outer() {
inner();
}
function inner() {
// do something for only outer function
}
return { outer };
}
};
Component.spec.js
it('what can I do?', () => {
wrapper.vm.inner = jest.fn(); // throw error
wrapper.vm.outer();
expect(wrapper.vm.inner).toBeCalled();
});
Case 2: Error
Component.vue
export default {
setup() {
function outer() {
inner();
}
function inner() {
// ...
}
return { outer, inner }; // add inner
}
};
Component.spec.js
it('what can I do?', () => {
wrapper.vm.inner = jest.fn();
wrapper.vm.outer();
expect(wrapper.vm.inner).toBeCalled();
});
expect(jest.fn()).toBeCalled()
Expected number of calls: >= 1
Received number of calls: 0
Case 3: Pass
it('what can I do?', () => {
wrapper.vm.outer = jest.fn(() => wrapper.vm.inner());
jest.spyOn(wrapper.vm, 'inner');
wrapper.vm.outer();
expect(wrapper.vm.inner).toBeCalled();
});
This case is not pretty...
Can't I get "inner()" without including it in "return"?
These methods pass when implemented in options api. But I want to use only setup().

Self Answer
I found a way. I made a class like this.
Util.js
class Util {
outer() {
this.inner();
}
inner() {
// do something...
}
}
Component.vue
setup() {
const util = reactive(new Util());
function call() {
util.outer();
}
return { util, call };
}
Component.spec.js
it('is this way correct?', () => {
jest.spyOn(wrapper.vm.util, 'outer');
jest.spyOn(wrapper.vm.util, 'inner');
await wrapper.vm.call();
expect(wrapper.vm.util.outer).toBeCalledTimes(1);
expect(wrapper.vm.util.inner).toBeCalledTimes(1);
});

Related

Vue, await for Watch

i have similar architecture in my app.
computed(){
someStoreValue = this.$store.someStoreValue;
}
watch() {
someStoreValue() = async function () {
//do some async action
}
},
methods: {
someAction() {
this.$store.someStoreValue = 'NEW VALUE'
//await for "watch"
//do stuff
}
}
I need to "someAction" await for "someStoreValue" watcher ends.
I need this kind of architecture someStoreValue can be changed in many places.
Sure, you can't make your watchers async, which is pretty senseless since the data you are after has already arrived.
someStoreValue(newValue, oldValue) {
// But you can still call other async functions.
// Async functions are just functions that returns a promise. Thus:
doSomeAsyncAction().then(this.someAction)
}
Still, why not just do your async stuff in someAction instead?
watch:{
someStoreValue(newValue, oldValue) {
this.someAction()
}
},
methods:{
async someAction(){
await doSomeAsyncStuff() // What prevents you from doing this?
//do stuff
}
}
You can use a flag and wait for it.
data() {
return {
flag: false
}
},
watch() {
someStoreValue() = async function () {
//do some async action
flag = true;
}
},
methods: {
async someAction() {
this.$store.someStoreValue = 'NEW VALUE'
await new Promise((resolve) => {
if (this.flag) {
resolve();
} else {
const unwatch = this.$watch('flag', (newVal) => {
if (newVal) {
unwatch();
resolve();
}
});
}
});
//do stuff
}
}
Maybe in this case the #ippi solution is better, but you can use this approach in other cases.

How to set mock nuxt asyncData in jest

I am using Nuxt.js and want to test my page which uses asyncData with Jest. I have a factory function to set up my wrapper, but it basically returns a shallowMount.
Expected
When clicking a button I want the function to behave differently depending on the query parameter. When running the test I want to mock this by setting it directly when creating the wrapper (Similar to setting propsData). E.g. const wrapper = factory({ propsData: { myQueryParam: 'some-value' } });
Result
However trying to set propsData still returns undefined: console.log(wrapper.vm.myQueryParam); // undefined while I would expect it to be 'some-value'
Question
Is there a different approach on how I can test this function that relies on query parameters?
Because asyncData is called before Vue is initialised, it means shallowMount doesn't work right out of the box.
Example:
page:
<template>
<div>Your template.</div>
</template>
<script>
export default {
data() {
return {}
},
async asyncData({
params,
error,
$axios
}) {
await $axios.get("something")
}
}
</script>
test:
import { shallowMount } from "#vue/test-utils";
describe('NewsletterConfirm', () => {
const axiosGetMock = jest.fn()
const axiosPostMock = jest.fn()
var getInitialised = async function (thumbprint) {
if (thumbprint == undefined) throw "thumbprint not provided"
let NewsletterConfirm = require('./_thumbprint').default
if (!NewsletterConfirm.asyncData) {
return shallowMount(NewsletterConfirm);
}
let originalData = {}
if (NewsletterConfirm.data != null) {
originalData = NewsletterConfirm.data()
}
const asyncData = await NewsletterConfirm.asyncData({
params: {
thumbprint
},
error: jest.fn(),
$axios: {
get: axiosGetMock,
post: axiosPostMock
}
})
NewsletterConfirm.data = function () {
return {
...originalData,
...asyncData
}
}
return shallowMount(NewsletterConfirm)
}
it('calls axios', async () => {
let result = await getInitialised("thumbprint")
expect(axiosGetMock).toHaveBeenCalledTimes(1)
});
});
Credits to VladDubrovskis for his comment: in this nuxt issue

Why my vue js data member is not getting updated?

Data part
data () {
return {
containsAd: true
}
},
Method that manipulates the data member containsAd
updated () {
let _this = this
window.googletag.pubads().addEventListener('slotRenderEnded', function (event) {
if (event.slot.getSlotElementId() === 'div-gpt-ad-nativead1') {
_this.containsAd = !event.isEmpty // this is false
console.log('Ad Exists? ', _this.containsAd)
}
})
},
Just to check if the value has changed or not.
check () {
let _this = this
setTimeout(function () {
console.log('Current Value', _this.containsAd)
}, 5000)
}
Resulting Output
I think doing the event listener in the mounted hook will sort your issue.
data() {
return {
containsAd: true
};
},
mounted() {
window.googletag.pubads().addEventListener('slotRenderEnded', event => {
if (event.slot.getSlotElementId() === 'div-gpt-ad-nativead1') {
this.containsAd = ! event.isEmpty // this is false
console.log('Ad Exists?', this.containsAd);
}
});
}
Also using es6 shorthand function will avoid you having to set _this.

redux-saga: eventChannel and listener which react callback return

In react-native backhandler listener react to callback function and act appropriately.
I need to read my store and depending on it, return true or false.
But I cant use select effect in normal function and I cant affect listener callback function from "watchBackButton" function.
export function* backButtonListen() {
return eventChannel(emitter => {
const backHandlerListener = BackHandler.addEventListener(
"hardwareBackPress",
() => {
emitter("back pressed");
}
);
return () => {
backHandlerListener.remove();
};
});
}
export function* watchBackButton() {
const chan = yield call(backButtonListen);
try {
while (true) {
let back = yield take(chan);
}
}
Since event channels are not bidirectional, I don't think there is a way to get some current state from saga to event channel using the select effect.
However, it is possible to access the store directly. There are multiple ways to get the store instance to the event channel. See my other answer here.
Using e.g. the context method you could do something like this:
// redux.js
...
const store = createStore(...);
sagaMiddleware.runSaga(rootSaga, {store});
// root-saga.js
export default function * rootSaga(context) {
yield setContext(context);
yield fork(watchBackButton);
}
// watch-back-button.js
export function* backButtonListen() {
const store = yield getContext('store');
return eventChannel(emitter => {
const backHandlerListener = BackHandler.addEventListener(
"hardwareBackPress",
() => {
emitter("back pressed");
return store.getState().foo === 'bar';
}
);
return () => {
backHandlerListener.remove();
};
});
}
export function* watchBackButton() {
const chan = yield call(backButtonListen);
try {
while (true) {
let back = yield take(chan);
}
}

How do I test a watcher on a tool that was imported in my component?

I use an instance of a class as a tool in one of my components. This component watches for changes in the class instance. However I fail at writing a test for that watcher.
I tried using jest.fn, spyOn and a setData, but none of these worked.
The class looks like this:
export default class myTool {
constructor () {
this._myData = null
}
get myData () {
return this._myData
}
set myData (updatedMyData) {
this._myData = updatedMyData
}
}
And the component:
import myTool from '#/utils/myTool'
export default {
...
data() {
return {
myTool: null
}
},
methods: {
handleMyDataUpdate(updatedMyData) {
// do something
}
},
mounted() {
this.$watch('myTool.myData', (updatedMyData) => {
this.handleMyDataUpdate(updatedMyData)
})
this.myTool = new myTool()
}
...
}
1st attempt with jest.fn:
test:
it('should call handleMyDataUpdate on myData update.', () => {
const wrapper = mountComponent()
const handleMyDataUpdate = jest.fn()
wrapper.setMethods({ handleMyDataUpdate })
wrapper.vm.myTool.myData = 5
expect(handleMyDataUpdate).toBeCalled()
})
2nd attempt with spyOn:
test:
it('should call handleMyDataUpdate on myData update.', () => {
const wrapper = mountComponent()
const spy = jest.spyOn(wrapper.vm, 'handleMyDataUpdate')
wrapper.vm.myTool.myData = 5
expect(spy).toBeCalled();
}
3rd attempt with setData:
test:
it('should call handleMyDataUpdate on myData update.', () => {
const wrapper = mountComponent()
const handleMyDataUpdate = jest.fn()
wrapper.setMethods({ handleMyDataUpdate })
wrapper.setData({
myTool: {
myData: 5
}
})
expect(handleMyDataUpdate).toBeCalled()
}
Result: the 3 things I tried always fail with the following reason: Expected mock function to have been called., whether I comment the line where myData is updated or not.
Other things that I tried:
I tried wrapping the expect line within a $nextTick, but it doesn't work either:
wrapper.vm.$nextTick(() => {
// expectation
done()
})
The following error outputs and the test is always considered as "passed", whereas it should be "failed":
console.error node_modules/vue/dist/vue.runtime.common.js:1739
{ Error: expect(jest.fn()).toBeCalled()
Looking at line 1739 of vue.runtime.common.js didn't help.
So how do I do to test my watcher?
The issue is your _myData in the myTool class is initially undefined, so it's not reactive. To resolve the issue, initialize _myData in myTool's constructor:
class myTool {
constructor() {
this._myData = null
}
// ...
}
Then, your "1st attempt" test should pass successfully.
demo