Changing Jest mock value between tests - javascript

I have a utility file that uses the following implementation with a vuex store:
// example.js
import store from '#/store';
[...]
export default function exampleUtil(value) {
const user = store.state.user.current;
[...]
}
In my test, I found that I can successfully mock the value of user in the following two ways:
Manual mock
// store/__mocks__/index.js
export default {
state: {
user: {
current: {
roles: [],
isAdmin: false,
},
},
},
};
or
Mock function
// example.spec.js
jest.mock('#/store', () => ({
state: {
user: {
current: {
roles: [],
isAdmin: false,
},
},
},
}));
The issue that I'm running into is that I want to be able to change the value of current between tests, such as changing isAdmin to true or updating the array for roles.
What is the best way to do this using Jest mocks?

It turns out that the value of a mock can be changed inside a test directly after importing the mocked file.
Using the mock function example from above:
// example.spec.js
import store from '#/store'; // <-- add this
jest.mock('#/store', () => ({
state: {
user: {
current: {
roles: [],
isAdmin: false,
},
},
},
}));
it('should do things', () => {
store.state.user.current.roles = ['example', 'another']; // <-- change mock value here
[...]
});

Related

Understanding how to use preloadedState in redux-toolkit

Im trying to save some things in locale storage and reHydrate the state on refresh.
And it works, but my initalState from my slice file is overwritten.
const reHydrateStore = () => {
if (localStorage.getItem('culture') !== null) {
return { login: { culture: JSON.parse(localStorage.getItem('culture')) } };
}
};
export default reHydrateStore;
Then in configureStore:
preloadedState: reHydrateStore(),
my initalState object looks like this when I dont use reHydrateStore:
const initialState = {
isLoading: false,
culture: '',
error: null,
};
And when I use reHydrateStore:
const initialState = {
culture: 'en-US',
};
Is there a way to configure the preloadedState to just replace the given prop?
I know I can set up all initalState props in the reHydrateStore method but that seems like awfaul way of doing it. Writing the same code twice feels very unnescesery imo.
Edit:
I moved the logic to the slice to be able to access the initalState.
But #Rashomon had a better approach imo and will try with that instead.
But here was my first solution:
const reHydrateLoginSlice = () => {
if (localStorage.getItem("culture")) {
return {
login: { ...initialState, culture: JSON.parse(localStorage.getItem("culture")) },
};
}
};
export { reHydrateLoginSlice };
Maybe you can define your initialState as this to avoid rewriting the rest of keys:
const initialState = {
isLoading: false,
culture: '', // default value, will be overwrited in following line if defined in the storage
...(reHydrateStore() || {}), // if returns undefined, merge an empty object and use previous culture value
error: null,
};
If you only need culture to be rehydrated, I would rehydrate only the value instead of an object to make it simpler:
const initialCultureValue = ''
const initialState = {
isLoading: false,
culture: reHydrateCulture() || initialCultureValue,
error: null,
};

Mock return value for useRef passed as an argument

I have the following function which takes in React.useRef() as an argument.
Having issues in testing it where I am trying to get the useRef to hold values and be able to removeChild().
When I run the test, I am getting following error.
TypeError: useRef.current.removeChild is not a function
This is cos I am passing an object instead of a useRef hook function during test.
How can I pass in a mocked useRef and be able to pass value and perform the removeChild operation during test?
This is the implementation.
export const postData = (
useRef, // this useRef is coming from the calling component which is actually React.useRef()
token,
id
) => {
useRef.current.children[0].value = token;
useRef.current.children[1].value = id;
[...useRef.current.children].forEach((child) => {
if (!child.value) {
useRef.current.removeChild(child);
}
});
};
This is the test which fails with above error.
it('should work', () => {
const formRef = {
current: {
action: '',
submit: jest.fn(),
children: [
{ value: '', },
{ value: '', },
],
},
};
postData(formRef, 'mock_token', 'mock_id');
});
As mentioned, failure due to passing an object instead of useRef during test.
Tried the following test instead to address this.
Following test throws this error.
TypeError: Cannot read property 'children' of undefined
import React, { useRef } from 'react';
jest.mock('react', () => {
const originReact = jest.requireActual('react');
return {
...originReact,
useRef: jest.fn(),
};
});
it('should work', () => {
const formRef = {
current: {
action: '',
submit: jest.fn(),
children: [
{ value: '', },
{ value: '', },
],
},
};
useRef.mockReturnValueOnce(formRef);
postData(useRef, 'mock_token', 'mock_id');
});
I did this and worked:
jest.mock("react", () => {
const originReact = jest.requireActual("react");
return {
...originReact,
useRef: jest.fn(() => ({ current: {} }));
};
});
Note the "current" key is added. The error of "Cannot read property 'children' of undefined" is because of the missing key.

VueJS: How to pass props while redirecting

I have two single-file components each with a named route. Setup.vue is a basic form that collect and forwards some data to Timer.vue which takes some props. Is there a way to push to a route giving it the props without passing them as url attributes?
Setup.vue
<script>
export default {
...
methods: {
startTimer() {
this.$router.push({
name: 'timer',
params: {
development: this.development,
inversion: this.inversion,
stop: this.stop,
fix: this.fix,
wash: this.wash
}
})
}
...
}
</script>
Timer.vue
<script>
export default {
name: 'timer',
...
props: {
development: { type: Number, required: true },
inversion: { type: Number, required: true },
stop: { type: Number, required: true },
fix: { type: Number, required: true },
wash: { type: Number, required: true }
}
router.js
{
// I want to avoid having to do this route, instead just /timer
path: '/timer/:development/:inversion/:stop/:fix/:wash',
name: 'timer',
component: Timer,
props: true
}
Yes, you can do it and the props came in the var below:
this.$route.params
But every time you reload the page the params that are not in the URL will be lost, so this case just work when changing the route inside the app without reload.
When I have a similar problem I use query variables instead of params to solved the problem, you can use this way or make a child routes tree to organize your props.
This may help -
this.$router.push({
name: "timer",
params: { fix: { type: 1, required: true } }
});
Invoke this code post form submission. However, if someone refreshes the timer page, the route params data will be gone and you will have to handle this scenario with some other way. If the data can be retrieved from an api, it will be better if you make an api call in created method of timer page and load the data in case of refresh.
I'll add another option for the sake of completeness. Henrique's answer above is a much simpler way to achieve what I initially wanted to do, which is to route and pass props to the component without url level route variables.
Using the store as part of vuex, we could save variables in a globally accessible object and access them later in different components.
store.js
export default new Vuex.Store({
state: {
development: null,
inversion: null,
stop: null,
fix: null,
wash: null
}
Setup.vue
export default {
data() {
return {
development: null,
inversion: null,
stop: null,
fix: null,
wash: null,
store: this.$root.$store // local pointer to the global store, for conciseness
}
},
methods: {
startTimer() {
// vuex uses a transactional history model, so we commit changes to it
this.store.commit('development', this.development * 60)
this.store.commit('inversion', this.inversion * 60)
this.store.commit('stop', this.stop * 60)
this.store.commit('fix', this.fix * 60)
this.store.commit('wash', this.wash * 60)
this.$router.push({
name: 'timer'
})
},
Timer.vue
export default {
name: 'timer',
data() {
return {
state: this.$root.$store.state
}
},
computed: {
// map local to state
development() {
return this.state.development
},
stop() {
return this.state.stop
}
...

call function inside data() property

I'm trying to fetching some data for my search tree and i'm not able to get the data directly from axios or to call a function because it can't find this.
export default {
name: 'SideNavMenu',
data () {
return {
searchValue: '',
treeData: this.getData(),
treeOptions: {
fetchData(node) {
this.onNodeSelected(node)
}
},
}
},
In the data() I have treeOptions where I want to call a function called onNodeSelected. The error message is:
"TypeError: this.onNodeSelected is not a function"
can anybody help?
When using this, you try to call on a member for the current object.
In JavaScript, using the {} is actually creating a new object of its own and therefore, either the object needs to implement onNodeSelected or you need to call a different function that will allow you to call it on an object that implements the function.
export default {
name: 'SideNavMenu',
data () {
return {
searchValue: '',
treeData: this.getData(), // <--- This
treeOptions: {
fetchData(node) {
this.onNodeSelected(node) // <--- and this
}
},
}
},
//are calling functions in this object :
{
searchValue: '',
treeData: this.getData(),
treeOptions: {
fetchData(node) {
this.onNodeSelected(node)
}
},
//instead of the object you probably are thinking
I would avoid creating object blocks within object blocks like those as the code quickly becomes unreadable and rather create functions within a single object when needed.
I am guessing you would have the same error message if you tried to get a value from treeData as well
You are not calling the function, or returning anything from it. Perhaps you're trying to do this?
export default {
name: 'SideNavMenu',
data () {
return {
searchValue: '',
treeData: this.getData(),
treeOptions: fetchData(node) {
return this.onNodeSelected(node)
},
}
},
Regardless, it is not considered good practice to put functions inside data properties.
Try declaring your variables with empty values first, then setting them when you get the data inside beforeCreate, created, or mounted hooks, like so:
export default {
name: 'SideNavMenu',
data () {
return {
searchValue: '',
treeData: [],
treeOptions: {},
}
},
methods: {
getData(){
// get data here
},
fetchData(node){
this.onNodeSelected(node).then(options => this.treeOptions = options)
}
},
mounted(){
this.getData().then(data => this.treeData = data)
}
},
Or if you're using async await:
export default {
name: 'SideNavMenu',
data () {
return {
searchValue: '',
treeData: [],
treeOptions: {},
}
},
methods: {
getData(){
// get data here
},
async fetchData(node){
this.treeOptions = await this.onNodeSelected(node)
}
},
async mounted(){
this.treeData = await this.getData()
}
},

Can't unit test redux-saga with selector function using Jest

Problem Explanation:
I want to unit test a redux-saga using Jest. I'm doing this the way it is described in the example provided within the redux-saga docs: https://redux-saga.js.org/docs/advanced/Testing.html
Within my Saga I'm calling a selector function selectSet that returns a specific object from the application store:
export const selectSet = state => state.setStore.set
In my saga I'm trying to yield this selector function:
import { put, select } from 'redux-saga/effects'
import { selectSet } from '../selectors'
export function* getSet() {
try {
const set = yield select(selectSet)
yield put({ type: 'SET_SUCCESS', payload: { set } })
} catch (error) {
yield put({ type: 'SET_ERROR', payload: { error } })
}
}
Within my test there is no valid application store so I'd have to mock the function to return the expected object:
import assert from 'assert'
import * as AppRoutines from './AppRoutines'
import { put, select } from 'redux-saga/effects'
describe('getSet()', () => {
it('should trigger an action type "SET_SUCCESS" with a payload containing a valid set', () => {
const generator = AppRoutines.getSet()
const set = {
id: 1,
slots: [{ id: 1 }, { id: 2 }, { id: 3 }, { id: 4 }, { id: 5 }],
}
const selectSet = jest.fn()
selectSet.mockReturnValue(set)
// Saga step 1
const actualFirst = generator.next().value
const expectedFirst = select(selectSet)
assert.deepEqual(
actualFirst,
expectedFirst,
'it should retreive a valid set from the store using the selectSet selector'
)
})
})
However - if I assert the saga to return a specific generator value using deepEqual and my mocked function, it expects my selector function to have the original selectSet constructor. But since I'm mocking the function with jest.fn() the constructor is actually equal to mockConstructor - which makes my test fail:
Expected value to deeply equal to:
{"##redux-saga/IO": true, "SELECT": {"args": Array [], "selector": [Function mockConstructor]}}
Received:
{"##redux-saga/IO": true, "SELECT": {"args": Array [], "selector": [Function selectSet]}}
Question: How can I make an assert.deepEqual containing a mock function without conflicting constructor types?
Alternative Question: Is there a way to make my assertion expect a mockConstructor instead of the actual selectSet constructor?
You should not need to mock the selector at all, as in a saga test of this nature, the selector is never actually called, instead you are testing the declarative instructions that are created for the redux saga middleware to act upon are as you expect
This is the instruction that the saga will create {"##redux-saga/IO": true, "SELECT": {"args": Array [], "selector": [Function selectSet]}}, but as the middleware is not running during this test scenario selectSelect will never actually get called
If you need to mock results that your selector returns for your action, then you do so by passing the mock data into the next step...
const set = {
id: 1,
slots: [{ id: 1 }, { id: 2 }, { id: 3 }, { id: 4 }, { id: 5 }],
}
// Saga step 1
const firstYield = generator.next().value
assertDeepEqual(firstYield, select(selectSet))
// Step 2 - successful so dispatch action
// mock data from the previous yield by passing into this step via `next(stubbedYieldedData)`
const secondYield = generator.next(set).value
assertDeepEqual(secondYield, put({type: 'SET_SUCCESS', payload: {set} }))
We can pass the mock store in a fake store as below. Below are the sample selector and generator function along with its test.
Selector
const authSelector = (state) => state.authReducer || initialState;
Saga Generator function
export function* getAuthToken(action) {
try {
const authToken = yield select(makeSelectAuthToken());
} catch (errObj) {}
}
Test Case
import { runSaga } from 'redux-saga'
const dispatchedActions = [];
const fakeStore = {
getState: () => ({ authReducer: { auth: 'test' } }),
dispatch: (action) => dispatchedActions.push(action)
}
await runSaga(fakeStore, getAuthToken, {
payload: {}
}).done;
expected case you can write here below this

Categories

Resources