Jest test that value is set in .finally() block of fetch - javascript

I am testing a Vue component's method that performs a basic fetch, and updates a data value in the .finally() block of that fetch. I can confirm that my test reaches that .finally() block, but the data value is never updated.
My method is:
updateProfile () {
fetch(updateProfileEndPoint, {
method: 'POST',
body: {email: test#test.com, id: 1234, name: 'bob},
})
.catch((error) => {
this.errorField = true;
})
.finally(() => {
this.profileUpdated = true;
});
In my Jest test, I have:
const wrapper = mount(ProfileComponent, { store,
data () {
return {
profileUpdated: false,
};
},
});
global.fetch = jest.fn(() =>
Promise.resolve({
profileUpdate: 'complete',
})
);
wrapper.vm.updateProfile();
expect(wrapper.vm.profileUpdated).toBe(true);
However, profileUpdated remains false. Strangely, if I console.log(this.profileUpdate) in that method, the updated value true does log. However, my tests still receive false.

Your assertion occurs before the asynchronous fetch call actually completes.
One solution is to return the fetch result (a Promise) from updateProfile(), allowing the test to await the call:
// MyComponent.vue
export default {
methods: {
updateProfile() {
return fetch(...).catch(...).finally(...)
} 👆
}
}
// MyComponent.spec.js 👇
it('updateProfile() sets profileUpdated flag', async () => {
const wrapper = mount(...)
👇
await wrapper.vm.updateProfile()
expect(wrapper.vm.profileUpdated).toBe(true)
})

Related

Using FetchMock to mock fetch not working

Im trying to mock a fetch request to confirm if the right actions were dispatched:
the redux function:
export function loginRequest(email, password) {
return (dispatch) => {
dispatch(login(email, password))
return fetch('http://localhost:8000/login-success.json')
.then((response) => response.json())
.then((response) => dispatch(loginSuccess()))
.catch((error)=>dispatch(loginFailure()))
}
}
the test:
test("API returns the right response, the store received two actions LOGIN and LOGGING_SUCCESS", () => {
const store = mockStore({})
fetchMock.get('*', {})
return store.dispatch(loginRequest('jack', 124)).then(()=>{
const actions = store.getActions()
console.log(actions)
expect(actions).toEqual([login('jack', 124), loginSuccess()])
})
})
the console.log output:
[
{ type: 'LOGIN', user: { email: 'jack', password: 124 } },
{ type: 'LOGIN_FAILURE' }
]
im expecting the second action to be LOGIN_SUCCESS action instead. it seems like the mock isnt working at all. Am i missing something here
SOLVED: in the file where i defined the loginRequest function, i was importing fetch (import fetch from 'node-fetch';) hence calling the function in the test resulted in an actual fetch call rather than fetch-mock

Storing data from Axios Promise for multiple uses

I have created an endpoint in express that handles get requests. From a react component, I make a get request to said endpoint using axios. I want to store the data in an object in my Component class so that it can be accessed at multiple times (onComponentDidLoad, multiple onClick event handlers, etc). Is there a way to store the data outside of the axios promise, and/or preserve the promise so that I can do multiple .then calls without the promise being fulfilled?
I have tried using setState(), returning the promise, and returning the actual data from the get request.
Here is what I have right now:
constructor {
super();
this.myData = [];
this.getData = this.getData.bind(this);
this.storeData = this.storeData.bind(this);
this.showData = this.showData.bind(this);
}
// Store data
storeData = (data) => {this.myData.push(data)};
// Get data from API
getData() {
axios
.get('/someEndpoint')
.then(response => {
let body = response['data'];
if(body) {
this.storeData(body);
}
})
.catch(function(error) {
console.log(error);
});
}
showData() {
console.log(this.myData.length); // Always results in '0'
}
componentDidMount = () => {
this.getData(); // Get data
this.showData(); // Show Data
}
render() {
return(
<Button onClick={this.showData}> Show Data </Button>
);
}
Edit
I was incorrect in my question, storing the promise and then making multiple .then calls works. I had it formatted wrong when i tried it.
This code won't quite work because you're attempting to show the data without waiting it to be resolved:
componentDidMount = () => {
this.getData();
this.showData();
}
As you hinted toward in your original post, you'll need to extract the data from the Promise and there's no way to do that in a synchronous manner. The first thing you can do is simply store the original Promise and access it when required - Promises can be then()ed multiple times:
class C extends React.Component {
state = {
promise: Promise.reject("not yet ready")
};
showData = async () => {
// You can now re-use this.state.promise.
// The caveat here is that you might potentially wait forever for a promise to resolve.
console.log(await this.state.promise);
}
componentDidMount() {
const t = fetchData();
this.setState({ promise: t });
// Take care not to re-assign here this.state.promise here, as otherwise
// subsequent calls to t.then() will have the return value of showData() (undefined)
// instead of the data you want.
t.then(() => this.showData());
}
render() {
const handleClick = () => {
this.showData();
};
return <button onClick={handleClick}>Click Me</button>;
}
}
Another approach would be to try to keep your component as synchronous as possible by limiting the asyncrony entirely to the fetchData() function, which may make your component a little easier to reason about:
class C extends React.Component {
state = {
status: "pending",
data: undefined
};
async fetchData(abortSignal) {
this.setState({ status: "pending" });
try {
const response = await fetch(..., { signal: abortSignal });
const data = await response.json();
this.setState({ data: data, status: "ok" });
} catch (err) {
this.setState({ error: err, status: "error" });
} finally {
this.setState({ status: "pending" });
}
}
showData() {
// Note how we now do not need to pollute showData() with asyncrony
switch (this.state.status) {
case "pending":
...
case "ok":
console.log(this.state.data);
case "error":
...
}
}
componentDidMount() {
// Using an instance property is analogous to using a ref in React Hooks.
// We don't want this to be state because we don't want the component to update when the abort controller changes.
this.abortCtrl = new AbortController();
this.fetchData(this.abortCtrl.signal);
}
componentDidUnmount() {
this.abortCtrl.abort();
}
render() {
return <button onClick={() => this.showData()}>Click Me</button>
}
}
If you just store the promise locally and access it as a promise it should work fine.
getData() {
// if request has already been made then just return the previous request.
this.data = this.data || axios.get(url)
.then( response => response.data)
.catch(console.log)
return this.data
}
showData() {
this.getData().then(d => console.log('my data is', data));
}

Proper way to handle a page refresh based on a redux request change

I have created a redux that is going to request an API and if the result is 200, I want to redirect the user to another page using history.
The problem is: I don't know how to trigger this change if the action is a success.
I could redirect the user in my useCase function but I can't use history.push pathName/state argument because it only works in a React component.
So this is what I have done in my React component:
const acceptProposalHandler = () => {
store.dispatch(acceptProposal(id)).then(() => {
setTimeout(() => {
if (isAccepted) { //isAccepted is false by default but is changed to true if the
//request is 200
history.push({
pathname: urls.proposal,
state: {
starterTab: formatMessage({id: 'proposalList.tabs.negotiation'}),
},
});
}
}, 3000);
});
};
Sometimes it works but other times it wont. For some reason, .then is called even if the request fails.
I'm using setTimeOut because if I don't, it will just skip the if statement because the redux hasn't updated the state with isAccepted yet.
This is my useCase function from redux:
export const acceptProposal = (id: string) => async (
dispatch: Dispatch<any>,
getState: () => RootState,
) => {
const {auth} = getState();
const data = {
proposalId: id,
};
dispatch(actions.acceptProposal());
try {
await API.put(`/propostas/change-proposal-status/`, data, {
headers: {
version: 'v1',
'Content-Type': 'application/json',
},
});
dispatch(actions.acceptProposalSuccess());
} catch (error) {
dispatch(actions.acceptProposalFailed(error));
}
};
What I'm doing wrong? I'm using Redux with thunk but I'm not familiar with it.
".then is called even if the request fails." <- this is because acceptProposal is catching the API error and not re-throwing it. If an async function does not throw an error, it will resolve (i.e. call the .then). It can re-throw the error so callers will see an error:
export const acceptProposal = (id: string) => async (
// ... other code hidden
} catch (error) {
dispatch(actions.acceptProposalFailed(error));
// ADD: re-throw the error so the caller can use `.catch` or `try/catch`
throw error;
}
};

How do i combine asynchronous js with vuex store?

So i've been trying to make a 'log in' for my vue app. User logs in by clicking on the button and running the following method:
async signIn() {
this.getfireBaseData(await this.authenticate());
},
Here are both of the methods used inside the previous one:
async authenticate() {
auth
.signInWithPopup(provider)
.then((result) =>
this.$store.commit("setUserData", {
email: result.additionalUserInfo.profile.email,
picture: result.additionalUserInfo.profile.picture,
name: result.additionalUserInfo.profile.name,
})
)
.catch((err) => console.log(err.message));
return this.$store.state.userData;
},
async getfireBaseData(x) {
db.collection("rooms")
.get()
.then((snapshot) => {
this.firebaseData = snapshot.docs.map((doc) => ({
id: doc.id,
data: doc.data(),
}));
console.log(x);
if (x) {
this.$router.push(`/rooms/${this.firebaseData[0].id}`);
}
});
},
And the store:
state: {
userData: null,
},
mutations: {
setUserData(state, payload) {
state.userData = payload;
},
}
I'm expecting it to run getfireBaseData as soon as the authenticate() resolves its promise. For some reason - when i'm running the code for the first time - authenticate() leaves me with fullfiled promise with the value of "null" - the value in the store. It seems like authenticate() does modify the store value but does not return it as i run it instantly. Any solves to that issue?
That's because you are returning directly your store data from authenticate(), not the promise chain.
So you are not waiting your auth.signInWithPopup but resolve directly the current value of your store.
try to returning directly auth.signInWithPopup(provider) and return your userData from the resolve callback or via an other "then" after your catch.
But it might not be a good idea to mix async/await with .then.catch.
It's error prone.

Why async function continues after await (in vuex)

From one vuex action (togglePostLike) I'm trying to await another vuex action (requiresAuth) using async/await.
I had expected that if return Promise.resolve() was never called then the first action should get stopped at await dispatch (and get garbage collected), but it proceeds to the console.log('this is called SECOND') even without the resolve.
Shouldn't await stop the execution without resolve? How can I get that intended behavior?
/store/modules/post.js
async togglePostLike ({ dispatch, rootState }, post) {
await dispatch('user/requiresAuth', null, { root: true })
console.log('this is called SECOND, but why without resolve')
// I don't want this stuff to execute without the resolve
if (post.likes.includes(rootState.user._id)) {
dispatch('addPostUnlike', post)
} else {
dispatch('addPostLike', post)
}
},
/store/modules/user.js
async requiresAuth ({ state }) {
if (!state.authenticated) {
router.push({ name: 'login', query: { comeBack: true } })
$(`.modal`).modal('hide')
console.log('this is called first')
} else {
console.log('this is NOT called')
return Promise.resolve()
}
},
EDIT: how it looks without async/await
As I understand from the vuex docs, dispatch returns a promise anyway. Using the following:
post.js
if (dispatch('user/requiresAuth', null, { root: true })) {
// still executes before return false
}
user.js
requiresAuth ({ state }) {
if (!state.authenticated) {
// do stuff (does it)
return false
}
return true
}
An async function like requiresAuth always returns a promise that is resolved with the return value (including undefined if no return statement is evaluated). It does not make a difference whether the function ends without a return, with a return;, with a return undefined;, with a return Promise.resolve() or with a return Promise.resolve(undefined) - they are indistinguishable from the outside.
Either return a boolean and then branch on it, or just throw an exception.

Categories