I have a test that is passing, but I don't understand why I have to write this way. Here is the test setup:
describe("Photo Due", () => {
const localVue = createLocalVue();
localVue.use(Vuex);
let store;
let actions = {
"api_data/photoAction": jest.fn(),
"api_data/selectPhoto": jest.fn(),
"modals/openCloseModal": jest.fn()
},
beforeEach(() => {
store = new Vuex.Store({
state: {
token: "abc",
GLN: "123",
GLT: "456",
isDesktop: true
},
getters: {
"auth/getToken": state => state.token,
"auth/getGLN": state => state.GLN,
"auth/getGLT": state => state.GLT,
"app/getIsDesktop": state => state.isDesktop
},
actions
});
});
...
Here is my test that calls a function, which calls an action:
it("reportPhoto", async () => {
const wrapper = factory();
wrapper.vm.reportPhoto();
wrapper.vm.$nextTick();
expect(actions["api_data/selectPhoto"]).toHaveBeenCalled(); // THIS IS THE LINE IN QUESTION
});
When the test is setup this way everything passes. But it doesn't seem right that I am defining actions outside of the store and accessing it that way in the test by calling actions["api_data/selectSnap"]. I copied this approach from a guide on from the vue-test-utils website. If I don't need to access actions and getters from the store then why not bypass the store altogether and jut define random functions that mock my vuex functionality?
I think I don't understand what is happening under the hood fully, but shouldn't I be accessing the action through the store? This is what I am having trouble doing.
My questions are 1) Do I need to access the actions through the store or am I overthinking what should be a basic test? 2) If I do need to access the action through the store, how do I go about doing that?
It really depends on what kind of test you are trying to do and if you want to test a fully functional store or just check that it gets called.
I recommend this post. There are differents approach explained along with several examples. It helped me when I needed to write some store oriented unit tests
Related
Here's a simple example on the forgot password reset page of my app, I would want to bypass the server side and just have the password reset to succeed on click so I would write a test and use the custom test store like so:
const customStore = {
state() {
return {
Authentication: {
passwordResetSuccess: false,
},
};
},
mutations: {
SET_RESET_PASSWORD_SUCCESS(state) {
state.Authentication.passwordResetSuccess = true;
},
},
actions: {
forgotPasswordResetPassword() {
this.commit('SET_RESET_PASSWORD_SUCCESS');
},
},
};
Then I could include the custom store in my beforeEach() and it worked great. I've tried everything I can think of to get this to work with pinia, but it doesn't seem to work.
I'm using jest along with vue/test-utils.
I basically tried just creating the test pinia store, but I can't figure out how to get the component to use the custom test store.
const useCustomStore = defineStore('AuthenticationStore', {
state: () => ({
passwordResetSuccess: false,
}),
actions: {
forgotPasswordResetPassword() {
this.passwordResetSuccess = true;
},
},
});
const authenticationStore = useCustomStore();
I can't directly add it as a plugin because it can't find an active instance of pinia.
I went through this guide: https://pinia.vuejs.org/cookbook/testing.html#unit-testing-a-store
and I also tried using jest mock as described here: https://stackoverflow.com/a/71407557/4697639
But it still failed.
If anyone has any idea how to create a custom store that can be used by the component and actually hits the custom actions, I could really use some help figuring this out. Thank you!!
Tao mentioned in the comments that this isn't a good way to do unit tests. I will mark this as resolved and fix how I do the testing.
I have a design question on how to manage firebase auth & redux saga states with react-native-firebase.
Example use-case
Let's start from the scenario that I have an app that uses the idToken for a variety of use cases, some in the views using information from the claims, and some in redux actions to make api calls.
Using redux-saga, I would expect to implement these two cases like so:
// in selectors.js
const getIdToken = (state) => state.idTokenResult?.token
const getUserRole = (state) => state.idTokenResult?.claims.role
// in view.js
const role = useSelector(Selectors.getUserRole)
// in actions.js
const idToken = yield select(Selectors.getIdToken)
With this in mind I want to make sure the idTokenResult is available & up to date in my state. I can do this we a few actions and reducers, by calling a login method & then relying on the dispatched event onIdTokenChanged to update my state on login & tokenRefreshes. Something like the following:
// in actions.js
function* onLogin(email, password){
yield call([auth(), 'signInWithEmailAndPassword'], email, password)
}
// This action would be called by an eventChannel which emits on each onIdTokenChanged
function* onIdTokenChanged(user){
yield put({ type: "UPDATE_USER", user: user, })
if (user){
const idTokenResut = yield call([auth().currentUser, 'getIdTokenResult'])
yield put({ type: "UPDATE_ID_TOKEN_RESULT", idTokenResult: idTokenResult, })
}
}
// in reducers.js
const reducer = (state = {}, action) => {
switch (action.type) {
case 'UPDATE_USER':
return { ...state, user: action.user };
case 'UPDATE_ID_TOKEN_RESULT':
return { ...state, idTokenResult: action.idTokenResult }
}
}
Problem
Here is when we run into a problem. I recently learned that the onIdTokenChanged is dispatched lazily, only when the getIdTokenResult() method is invoked link. This means that with the code above we cannot expect our state to be accurate, because when we call yield select(Selectors.getIdToken) it doesn't check getIdTokenResult() and therefore the onIdTokenChanged event is never dispatched.
Potential solutions
How do we overcome this problem?
Set up a timer which periodically calls getIdTokenResult() before the token expires, to trigger the event.
Should work, but defeats the purpose of having an onIdTokenChanged event. Also this means it will refresh the token hourly, even if it isn't needed or being accessed
Somehow call getIdTokenResult() in the selector?
It's an async method so it seems like an anti-pattern here and I'm not even sure it's possible
Use the library directly to fetch user states with auth().currentUser, and forget redux-saga
We lose the nice rerender functionalities that redux's useSelector provides. By accessing the state directly we'll need to figure out another way to trigger rerenders on auth changes, which defeats the purpose of using redux-saga
Something I didn't consider/implemented incorrectly?
Your suggestions are welcome and thanks in advance for you help! :)
How can I in user.js call mutation from whoToFollow.js called reset ? Is it even possible ? Heres my code:
async logOut({commit}) {
this.$cookies.remove('token');
commit('set_token', null);
commit('whoToFollow/reset');
this.$router.push('/sign-in');
},
But it doesn't work I get this error:
unknown local mutation type: whoToFollow/reset, global type: user/whoToFollow/reset
It is possible to call mutations from other stores directly. You are just missing the option '{root: true}', which is needed for namespaced modules.
I would recommend calling an action in the other store first though, which then again calls the mutations to stay true to the Vuex pattern. Actions -> Mutations
async logOut({commit, dispatch}) {
this.$cookies.remove('token');
commit('set_token', null);
// in the reset action you can then call the commit
dispatch('whoToFollow/reset', payloadHere, { root: true })
this.$router.push('/sign-in');
},
I advise you to check out the Vuex Api Documentation to learn more about this and why you will need 'root: true'.
https://vuex.vuejs.org/api/#vuex-store-instance-methods
Specifically, code that runs before the app actually loads. I'm using vuex and the first thing I want to do (regardless of what route the user is on) is to dispatch a getUser action to get currently user details from the API (or alternatively, redirect if not authenticated).
If I place it in my App.vue mounted component, I believe it might be too late? Don't children components load before parents?
If I get it right you want to do something before the application initialize. For that you can just perform async method in app initialization. Something like that as an example:
function initializeApp (vueCreated) {
return new Promise((resolve, reject) => {
switch (vueCreated) {
case false: // "prevue" initialization steps
console.log('vue not yet created, prevue steps happens')
// ...
setTimeout(_ => resolve(), 3500) // async call
break;
case true: // we can continue/prepare data for Vue
console.log('vue created, but waiting for next initialization steps and data')
// ...
setTimeout(_ => resolve('Mounted / shown when app ready'), 3500) // async call
}
})
}
initializeApp(false).then(_ => {
new Vue({
template: '#app',
data: {
content: null
},
async created () {
this.content = await initializeApp(true)
this.$mount('#app')
console.log('all inicialization steps done, data arrived, vue mounted')
}
})
})
I have found some article related to your question may be this help you out. Link
If you are using vue-router you can use beforeEach to prevent some routes of unauthenticated users.
You can read more here.
If you get stuck here provide code what you tried with router.
Also good example of using navigation guards.
Good luck!
Problem: Having started multiple long-polling streams that need to persist throughout the app lifecycle (regardless of the lifecycle of individual components), I'm looking for a way to unsubscribe in response to various events (e.g. route change, but not limited to). To that end I wrote the following code:
export const actions: ActionTree<TasksState, RootState> = {
async pollEventTasks({ dispatch, commit, state, rootState }, payload: any) {
const pollEventTasks$ = timer(0, 5000).pipe(
switchMap(_ => tasksService.loadTasksForEvent(payload.eventId)),
map((response: any) => {
commit('setTasks', response);
})
).subscribe();
// this won't work in strict mode. Hot observables ~can't~ shouldn't be written to store:
// commit('longPolling/eventTasksPollingStarted', pollEventTasks$, {root: true});
},
A hot observable "updates itself", thus mutating store outside of mutation handler. What would be a neat solution fitting vue/vuex best practices?
We ended up building a plugin injected via Vue.use and storing Observable subscriptions there