I created a global guard that uses getters from store.
I am trying to mock some getters from store for testing purpose. The problem is that mocking
does not work.
// router/index.ts
export function beforeEach(to: any, from: any, next: any) {
const isLoggedIn = store.getters.isLoggedIn();
const isGuest = to.matched.some((record: any) => (record.meta.guest));
if (isLoggedIn) { // isLoggedIn is always false
if (isGuest) {
next('/');
}
}
}
}
//router/index.spec.ts
describe('shoud test routing functionality', () => {
it('should redirect to / when isLoggedIn is true and IsGuest is true', () => {
// given
jest.mock('#/store', () => ({
getters: {
isLoggedIn: jest.fn().mockImplementation(
() => true, // <----------- this value is always false
),
},
}));
// even this one does not work
// jest.spyOn(getters, 'isLoggedIn').mockImplementation(() =>
// ()=> true);
const to = {
matched: [{ meta: { guest: true } }],
};
const next = jest.fn();
// when
beforeEach(to, undefined, next);
// then
expect(next).toHaveBeenCalledWith('/');
});
})
I inspired from this example.
Thanks #EstusFlask comment, I solved the problem.
The keyword is that jest.mock inside a test can't affect top-level imports.
jest.mock('#/store', () => ({
getters: {
isLoggedIn: jest.fn(),
// other methods should be declared here, otherwise an exception is thrown
isSuperAdmin: jest.fn(),
isAdmin: jest.fn(),
isReadUser: jest.fn(),
},
}));
describe('should test routing functionality', () => {
it('should redirect to / when isLoggedIn is true and IsGuest is true', () => {
// given
store.getters.isLoggedIn.mockImplementation(() => () => false);
const to = {
matched: [{ meta: { guest: true } }],
};
const next = jest.fn();
// when
beforeEach(to, undefined, next);
// then
expect(next).toHaveBeenCalledWith('/');
});
})
Related
Anyone knows how to achieve result on this case? Seems like this in delayed function is not reactive.
state: () => ({
test: 'test',
}),
actions: {
testAction() {
const fn = () => {
this.test = 'changed!'
}
setTimeout(() => {
fn()
}, 1000);
}
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.6.3/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.6.3/umd/react-dom.production.min.js">
import { createSlice } from "#reduxjs/toolkit";
import { getUser, updateUser } from "./index";
import { getAllData, getData } from "../../logs/store/index";
const manageErrorAndLoading = (state, actionType, error) => {
state.loading[actionType] = true;
state.error[actionType] = error;
};
export const loadingSlice = createSlice({
name: "loading",
initialState: {
loading: false
},
reducer: {
toggleLoading: (state) => !state,
},
extraReducers: {
[getUser.pending]: () => true,
[getUser.fulfilled]: () => false,
[getUser.rejected]: () => false,
[updateUser.pending]: () => true,
[updateUser.fulfilled]: () => false,
[updateUser.rejected]: () => false,
[getData.pending]: () => true,
[getData.fulfilled]: () => false,
[getData.rejected]: () => false,
},
});
export const { toggleLoading } = loadingSlice.actions;
export default loadingSlice.reducer;
</script>
I have used this method to add loading but its not efficient. the loading is running parallel on all api, when one api is loading state gets true.
In the RTK matching utilities there is the addMatcher utility, which can catch all the pending, fulfilled and/or rejected promises. For example:
extraReducers(builder) {
builder.addMatcher(isPending, (state, action) => {
// do what you want when a promise is pending
})}
isPending can be replaced also with isRejected and isFulfilled.
I need to implement a test that checks if the function has been called on the button click
onSave (e) {
this.$qiwaApi.createDimension()
.then(() => {})
.catch(err => this.showSnackbar(err.message))}
I need to test the function createDimension. In my test i mocked it
const createComponent = () => {
wrapper = mount(dimensions, {
store,
localVue,
mocks: {
$qiwaApi: {
createDimension: function (e) {
return new Promise((resolve) => { resolve({}) })
}
}
},
router
})
}
In the project, the function exported this way
export default $axios => ({
createDimension (data, surveyId) {
return $axios.post(`/lmi-admin/surveys/${surveyId}/dimension`, {
data: {
attributes: {
...data
}
}
})
}
})
I expect this test to work. But for some reason wrapper.qiwaApi or wrapper.createDimension return undefined
expect(wrapper.$qiwaApi.createDimension).toHaveBeenCalled()
The wrapper doesn't provide access to your mocks that way.
You would have to hold a reference to a jest.fn(), and then verify the calls on that reference directly instead of trying to pull it out of the wrapper:
it('calls createDimension on button click', async () => {
const createDimension = jest.fn(() => Promise.resolve())
const wrapper = mount(dimensions, {
mocks: {
$qiwaApi: {
createDimension
}
}
})
await wrapper.find('[data-testid="save"]').trigger('click')
expect(createDimension).toHaveBeenCalled()
})
demo
I found numorous of similar questions, tried almost all of them including defining params value as a string, and so on but still can't make it work.
I have defined the route to accept props as written on the docs:
const routes = [
...
...
{
// this doesn't work, but passed on the url
// path: '/profile/personal/update/:original',
path: '/profile/personal/update',
name: 'profile.personal.update',
component: () => import('#/views/Profile/Personal/PersonalUpdate.vue'),
props: true,
// props: route => ({ default: route.params.original }), // this doesn't work
},
]
const router = createRouter({
history: createWebHistory(process.env.BASE_URL),
routes,
})
export default router
When I try to push the route and pass the data, typescript complains that I can't pass object to the params property, so I tried to cast it as any and string by doing like this, it passed:
// Somewhere inside the usePersonalInfoComposition
const router = useRouter()
const personalInfo = reactive({
data: new PersonalInfo(),
isLoading: true,
})
const editPersonalInfo = () => {
logger.info('Preparing data to be edited and redirect')
router.push({
name: 'profile.personal.update',
params: {
original: JSON.stringify(personalInfo.data)
// original: personalInfo.data as any
}
})
}
However, when I try to grab the value of the prop which the value is given from route params, I got undefined.
export default defineComponent({
props: ['original'],
setup(props) {
const router = useRouter()
const { personalInfo, updatePersonalInfo } = usePersonalInfoComposition()
onBeforeMount(() => console.log(props.original)) // undefined
onMounted(() => console.log(props.original)) // undefined
// I would do this personally but it was kinda problematic
// and error-prone especially when the page is refreshed.
// So I decided to not doing it. Also, the `props` are now useless.
onBeforeMount(() => {
const original = router.currentRoute.value.params['original'] // "{ user: { id: 1, name: 'joe' } }"
let parsed;
// I need to do this since `params` returns either `string | string[]`
if (original instanceof Array) parsed = JSON.parse(original[0])
else parsed = JSON.parse(original)
console.log(parsed) // { user: { id: 1, name: 'joe' } }
})
}
})
Is there any way to pass the original data without only passing id and make the user fetch the whole time they refreshes the page? I see Vuex has some solution for this, but I don't want to rebuild the whole app from scratch only for this kind of problem.
I was wondering about using local storage like sqlite or the browser's built in local storage but I think it will add more unnecessary complexity. How do you think?
After a while, It comes to a solution but with different approach to fix this problem. I'm not a state management expert but it works.
Figuring out the problem
Let's say I have this on the personal info page:
PersonalInfo.vue
export default defineComponent({
name: 'PersonalInfoPage',
setup() {
const {
personalInfo,
fetchPersonalInfo,
editPersonalInfo,
} = usePersonalInfoComposition()
onBeforeMount(() => fetchPersonalInfo())
return {
personalInfo,
}
}
})
And an update page:
PersonalInfoUpdate.vue
export default defineComponent({
name: 'PersonalInfoUpdatePage',
setup() {
const { personalInfo } = usePersonalInfoComposition()
return {
personalInfo,
}
}
})
Both has the same state being used, but from what I see, the composition was creating a new instance every time I call the method so the previously fetched state is not there and I need to re-fetch the same stuff all over again.
Solution
Instead of doing it that way, I lifted the state up above the composition method to keep the fetched state and make a "getters" by using the computed property to be consumed inside my PersonalUpdate.vue
PersonalInfoComposition.ts -- before
export const usePersonalInfoComposition = () => {
const personalInfo = reactive({
data: new PersonalInfo(),
isLoading: true,
})
const editPersonalInfo = () => {
router.push({
name: 'profile.personal.update',
params: {
original: JSON.stringify(personalInfo.data),
}
})
}
const fetchPersonalInfo = () => {
state.isLoading = true
repository
.fetch()
.then(info => state.data = info)
.catch(error => logger.error(error))
.finally(() => state.isLoading = false)
}
return {
personalInfo,
fetchPersonalInfo,
editPersonalInf,
}
}
PersonalInfoComposition.ts -- after
const state = reactive({
data: new PersonalInfo(),
isLoading: true,
})
export const usePersonalInfoComposition = () => {
const personalInfo = computed(() => state.data)
const isLoading = computed(() => state.isLoading)
const editPersonalInfo = () => {
router.push({ name: 'profile.personal.update' })
}
const fetchPersonalInfo = () => {
state.isLoading = true
repository
.fetch()
.then(info => state.data = info)
.catch(error => logger.error(error))
.finally(() => state.isLoading = false)
}
return {
personalInfo,
isLoading,
fetchPersonalInfo,
editPersonalInfo,
}
}
Now it works, and no Vuex or local storage or even route.params needed.
The Downside
Since the state is shared, whenever I made any changes on the state attached to a v-model, the data inside the PersonalInfo.vue are also be changed.
This could be easily fixed by creating a clone of the state object to handle the updates and sync it whenever the new data persisted in the database.
PersonalInfoComposition.ts -- fixing the downside
const state = reactive({
data: new PersonalInfo(),
isLoading: true,
})
const editState = reactive({
data: new PersonalInfo(),
isLoading: true,
})
export const usePersonalInfoComposition = () => {
const personalInfo = computed(() => state.data)
const personalInfoClone = computed(() => editState.data)
const isLoading = computed(() => state.isLoading)
const editIsLoading = computed(() => editState.isLoading)
const editPersonalInfo = () => {
// This will clone the object as well as its reactivity, dont do this
// editState.data = state.data
// Clone the object and remove its reactivity
editState.data = JSON.parse(JSON.stringify(state.data))
router.push({ name: 'profile.personal.update' })
}
const fetchPersonalInfo = () => {
state.isLoading = true
repository
.fetch()
.then(info => state.data = info)
.catch(error => logger.error(error))
.finally(() => state.isLoading = false)
}
const updatePersonalInfo = () => {
editState.isLoading = true
repository
.update(editState.data.id)
.then(info => {
// This could be extracted to a new method, but keep it simple for now
state.data = info
if (state.data === info) router.back()
})
.catch(error => logger.error(error))
.finally(() => editState.isLoading = false)
}
return {
personalInfo,
personalInfoClone,
isLoading,
editIsLoading,
fetchPersonalInfo,
}
}
I am struggling to write JEST test cases for below method
getStudentList (studentList:}[]) {
if (studentList.length < 1) {
Promise.resolve()
}
let promises = []
for (const student of StudentList) {
if (!student.name) {
Promise.resolve()
}
var url = `${API_URL}/${student.name}/`
promises.push(Axios.get(url}))
}
return Axios.all(promises)
.then(Axios.spread((...args) => {
// customise the response here
return args
.map(response => response.data)
.map(data => {
//do something with data
return data
})
}))
It uses axios.all and axios.spread to get the data back..i have written simple test cases for Axios.get..but how to write test case for this? This method is in a vue project in a service class
This is a short example of how you can write your expectations (with 100% coverage) for the code above:
import myService from './myService';
import Axios from 'axios';
jest.mock('axios');
global.API_URL = 'http://example.com/mock_api';
describe('myService', () => {
describe('getStudentList', () => {
describe('without students in the list', () => {
it('should result undefined', () => {
const result = myService.getStudentList();
expect(result).resolves.toEqual( undefined );
});
});
describe('with students in the list', () => {
const mockStudentList = [{
name: 'student1',
}, {
someProp: 'some value',
}, {
name: 'student3',
}];
const results = [];
const mockAxiosSpreadResult = jest.fn();
beforeAll(() => {
Axios.get.mockClear();
Axios.all.mockResolvedValue(results);
Axios.spread.mockReturnValue(mockAxiosSpreadResult);
myService.getStudentList( mockStudentList );
});
it('should call Axios.get once for each student with name', () => {
expect(Axios.get).toHaveBeenCalledWith(`${API_URL}/student1/`);
expect(Axios.get).toHaveBeenCalledWith(`${API_URL}/student3/`);
});
it('should call Axios.spread with a callback', () => {
expect(Axios.spread).toHaveBeenCalledWith(expect.any(Function));
});
it('should call the result of Axios.spread with the resolved value of Axios.all', () => {
expect(mockAxiosSpreadResult).toHaveBeenCalledWith(results);
});
describe('Axios.spread callback', () => {
let callback;
beforeAll(() => {
callback = Axios.spread.mock.calls[0][0];
});
describe('called with parameters', () => {
let result;
beforeAll(() => {
result = callback({
data: 1
},{
data: 2
},{
data: 3
},{
data: 4
});
});
it('should do something with the data', () => {
expect(result).toEqual([1,2,3,4]);
});
});
});
});
});
});
working example