Cannot spyOn on a primitive value; undefined given . Vue JS, Jest, Utils - vue.js

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.

Related

Cypress spy not being called when VueJS component emits event

I'm trying to follow the guide here to test an emitted event.
Given the following Vue SFC:
<script setup>
</script>
<template>
<button data-testid="credits" #click="$emit('onCredits')">Click</button>
</template>
and the following Cypress test:
import { createTestingPinia } from '#pinia/testing';
import Button from './Button.vue';
describe('<Button />', () => {
it('renders', () => {
const pinia = createTestingPinia({
createSpy: cy.spy(),
});
cy.mount(Button, {
props: {
onCredits: cy.spy().as('onCreditsSpy'),
},
global: {
plugins: [pinia],
},
});
cy.get('[data-testid=credits]').click();
cy.get('#onCreditsSpy').should('have.been.called');
});
});
My test is failing with
expected onCreditsSpy to have been called at least once, but it was never called
It feels weird passing in the spy as a prop, have I misunderstood something?
I solved such a situation with the last example within Using Vue Test Utils.
In my case the PagerElement component uses the properties 'pages' for the total of pages to render and 'page' for the current page additional to the 'handleClick'-Event emitted once a page has been clicked:
cy.mount(PagerElement, {
props: {
pages: 5,
page: 0
}
}).get('#vue')
Within the test I click on the third link that then emmits the Event:
cy.get('.pages router-link:nth-of-type(3)').click()
cy.get('#vue').should(wrapper => {
expect(wrapper.emitted('handleClick')).to.have.length
expect(wrapper.emitted('handleClick')[0][0]).to.equal('3')
})
First expectation was for handleClick to be emitted at all, the second one then checks the Parameters emitted (In my case the Page of the element clicked)
In order to have the Wrapper-element returned a custom mount-command has to be added instead of the default in your component.ts/component.js:
Cypress.Commands.add('mount', (...args) => {
return mount(...args).then(({ wrapper }) => {
return cy.wrap(wrapper).as('vue')
})
})

Vue Testing (JEST): button.trigger('click') not working

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)
})

The object sent as prop is undefined while testing a Vue SFC with Jest

I want to test a Vue single file component which receives a prop as input. When I mock the prop, which is an object, I get an error that the object is undefined, The error comes from the HTML where the values of the object are used. If I make the prop to be a string for example (and I remove answer.value and :class="{'active': answer.selected}" from HTML), everything works fine.
Component:
<template>
<div class="answer-container" #click="setActiveAnswer()" :class="{'active': answer.selected}">
<div class="answer">
<p>{{answer.value}}</p>
</div>
</div>
</template>
<script>
export default {
name: 'Answer',
props: {
answer: Object,
},
methods: {
setActiveAnswer() {
this.$emit('selectedAnswer', this.answer);
}
}
}
</script>
Test file:
import { mount } from '#vue/test-utils'
import Answer from './../../src/components/Answer'
describe('Answer', () => {
it('should receive "answer" as prop', () => {
const answer = {
value: 'testAnswer',
selected: true
};
const wrapper = mount(Answer, {
propsData: {
answer: answer
}
});
expect(wrapper.props().answer.value).toBe('testAnswer');
})
})
The error I get is:
TypeError: Cannot read property 'selected' of undefined
Please advise what am I doing wrong. Thanks!
I managed to fix this by adding a v-if="answer" on <div class="answer-container" ..., which is quite strange (as this is not async data) since the code works fine when checking the application in the browser - the problem only appeared while unit testing the component. I suppose there is also a fix in a Jest/Unit testing way, something like declaring the prop after the component finished rendering/mounting...

Accessing props in constructor vuejs

I'm looking for accessing props from constructor in VueJS.
I tried the react way like you can see below but without any success at that time.
Parent :
<nb-add-card
test="test props">
</nb-add-card>
Child:
<template>
<div>
test : {{ test }}
</div>
</template>
<script lang="ts">
import Vue from 'vue';
import Component from 'vue-class-component';
#Component({
components: { },
props: {
test: {
type: String,
default: "",
required: true
},
},
})
export default class AddCard extends Vue {
constructor(props: any) {
super(props);
console.log('TEST :', props); // => Undefined
console.log('TEST1 :', this); // => Refer to AddCard
}
}
</script>
The log "TEST" still "undefined".
Being stuck on this for hours now, on react we just simply pass "props" in constructor args, but in vuejs, it's seems not working... :/
I also test to access the props value by writing the props name of < nb-add-card > but it's not working either...
constructor(test: any) {
super(test);
console.log('TEST :', test); // => Undefined
console.log('TEST1 :', this); // => Refer to AddCard
}
Anyone have an idea ? Thank's
The constructor is of no use with vue-class-component. What you're exporting is a constructor itself. Your 'test' var is immediately accesable within' the component. Use the 'created' or 'mounted' hook to log it.

Vuex - When to load/initialize store data from http server

I want to show some data in the menu-bar, that needs to be fetched remotely (http get call) to be correctly displayed. When my application loads, the store wasn't initialized yet. Where should I do that?
This is what I have right now. nodeInfo is an empty object, as long as no data is fetched.
navigation component
<template>
<nav class="navbar" role="navigation" aria-label="main navigation">
...
<div class="navbar-end">
<span class="navbar-item">
<div v-if="nodeInfo.latestSolidSubtangleMilestoneIndex">
{{nodeInfo.latestSolidSubtangleMilestoneIndex}} / {{nodeInfo.latestMilestoneIndex}}
</div>
<div v-else>
Node seems offline!
</div>
</span>
</div>
</div>
</nav>
</template>
<script>
import {mapGetters} from 'vuex';
export default {
name: 'Menu',
computed: {
...mapGetters(['nodeInfo']) // Only the getters, no actions called to initialize them.
}
};
</script>
<style scoped>
</style>
store:
import Vue from 'vue';
import Vuex from 'vuex';
Vue.use(Vuex);
import axios from 'axios';
const iri_ip = '192.168.1.199';
const iri_port = '14265';
const state = {
token: null,
loading: false,
nodeInfo: {}
};
const mutations = {
SET_NODE_INFO(state, info) {
state.nodeInfo = info;
}
};
const actions = {
fetchNodeInfo({commit}) {
axios(createIriRequest('getNodeInfo')).then(response => {
console.log(response.data);
commit('SET_NODE_INFO', response.data);
});
}
};
const getters = {
token: state => state.token,
loading: state => state.loading,
nodeInfo: state => state.nodeInfo
};
const loginModule = {
state,
mutations,
actions,
getters
};
function createIriRequest(command) {
return {
url: `http://${iri_ip}:${iri_port}`,
data: {'command': command},
method: 'post',
headers: {
'Content-Type': 'application/json',
'X-IOTA-API-Version': '1'
}
};
}
export default new Vuex.Store({
modules: {
loginModule
}
});
The naming doesn't make much sense at the moment. But would I need to call the "actions" from the create() method of the menu component? That would somehow be weird. It would be cool if my store could somehow make the initial http calls itself without needing to be triggered. I don't even know how to call an action just like that from the create() part.
Have a look at the vue.js lifecycle diagram here: https://v2.vuejs.org/v2/guide/instance.html#Lifecycle-Diagram and read on the the lifecycle hooks here: https://v2.vuejs.org/v2/guide/instance.html#Instance-Lifecycle-Hooks.
It will hep you considerably in understanding when and where to add the stores dispatch method. this.$store.dispatch('fetchNodeInfo')
In Short:
Created hook:
Instance has been created, all the data observation, computed properties, methods, watch/event callbacks have been set up but the $el property isn't available yet.
Mounted hook:
Vue instance has been mounted, where el is replaced by the newly created vm.$el. el being the instance creation via new Vue({...}).
For your reading pleasure:
Lifecycle hooks: http://devdocs.io/vue~2-api-options-lifecycle-hooks/
#Bert was right. I added the dispatch method to the created() method of my component.
export default {
name: 'Menu',
created() {
this.$store.dispatch('fetchNodeInfo');
},
...
}