I have a component that fetches data wrapped in a function to made async calls cancelable:
useEffect(() => {
const asyncRequest = makeCancelable(myService.asyncRequest());
asyncRequest.promise
.then((result) =>
setState(result),
)
.catch((e) => {
if (!e?.isCanceled) {
//Case the rejection is not caused by a cancel request
throw e;
}
});
return () => {
asyncRequest.cancel();
};
},[])
I want to test that, when the rejection is not coming from a cancel request, the error is re-thrown (I'm filtering out cancel rejections since they are not true errors). So the goal is intercept exceptions coming from useEffect
How can I test it with enzyme and/or jest?
it('should not filter rejection not caused by cancel', () => {
let promise = Promise.reject(new Error('Generic error'));
when(myService.asyncRequest()).thenReturn(promise); // This will cause useEffect to throw
const myComponent = mount(<MyComponent />) // How to intercept the error?
})
To give further context here is the code of makeCancelable:
export function makeCancelable<T>(promise: Promise<T>): CancelablePromise<T> {
let isCanceled = false;
const wrappedPromise = new Promise<T>((resolve, reject) => {
promise.then(
(val) => (isCanceled ? reject({ isCanceled: true }) : resolve(val)),
(error) => (isCanceled ? reject({ isCanceled: true }) : reject(error)),
);
});
return {
promise: wrappedPromise,
cancel() {
isCanceled = true;
},
};
}
Related
I have the following, which I am calling from a component
const getUserAction = () => {
return dispatch => {
dispatch(request());
return getUser()
.then(
response => {
dispatch(success());
console.log(response);
return response
},
error => {
dispatch(failure(error));
}
)
};
};
Then on the component, I am dispatching this action:
this.props.dispatch(getUserAction())
.then(response => console.log(response))
}
I am seeing the console log from the above component being fired before the console log from the action.
Why is this? Am I doing something wrong or is this the expected behavior? Why does the console log from the action wait for the response, and the return statement from the same action doesn't wait?
And then, how can I get the response in my component if not going through the reducer?
const getUserAction = (cb) => {
return dispatch => {
dispatch(request());
return getUser()
.then(
response => {
dispatch(success());
console.log(response);
// success callback with response
cb(null /** no error hence null */, response)
return response
},
error => {
dispatch(failure(error));
}
)
};
};
Call like this:
this.props.dispatch(getUserAction((err, success) => {
if(err){
// Handle error here
return;
},
// Success handle
console.log(response))
}));
Code
import { createUser } from '../services';
...
...
handleFormSubmit = () => {
this.setState({ loading: true });
createUser()
.then(() => {
this.setState({
loading: false,
});
})
.catch(e => {
this.setState({
error: e,
});
});
};
Test
it('rejects...', () => {
const Container = createUserContainer(CreateUser);
const wrapper = shallow(<Container />);
return wrapper.instance().handleFormSubmit()
.catch(e => {
console.log("State: ", wrapper.state());
expect(e).toEqual('error');
});
});
Mock
export const createUser = function() {
return new Promise((resolve, reject) => {
reject('error');
});
};
The test does force the code to go into the catch in the method. So the state does get set to 'error'.
But in my test, it doesn't do what I expect and wait for the Promise to reject before it tests for the state change.
I'm not sure what to try here, should I be using async/await?
So it's the createUser method I want to wait for but I'm not sure my implementation allows for this.
You should do something like this:
it('rejects...', () => {
const Container = createUserContainer(CreateUser);
const wrapper = shallow(<Container />);
return expect(wrapper.instance().handleFormSubmit()).rejects.toEqual('error');
});
I think it is cleaner this way. You can see this approach in the official docs.
It's important to note that .rejects (and .resolves) returns a promise, which is returned in the example above so that jest knows to wait on it. If you don't return it, you MUST await it:
it('rejects...', async () => {
const Container = createUserContainer(CreateUser);
const wrapper = shallow(<Container />);
await expect(wrapper.instance().handleFormSubmit()).rejects.toEqual('error');
});
The test fails because it's not aware that the subject is asynchronous. It can be fixed by using a done param or making the test function async.
Note it's also necessary to set the number of expected assertions so that the test will fail even if the catch branch is not taken.
async/await style:
it('rejects...', async () => {
expect.assertions(1);
const Container = createUserContainer(CreateUser);
const wrapper = shallow(<Container />);
await wrapper.instance().handleFormSubmit()
.catch(e => {
console.log("State: ", wrapper.state());
expect(e).toEqual('error');
});
});
Older style done param:
it('rejects...', done => {
expect.assertions(1);
const Container = createUserContainer(CreateUser);
const wrapper = shallow(<Container />);
wrapper.instance().handleFormSubmit()
.catch(e => {
console.log("State: ", wrapper.state());
expect(e).toEqual('error');
done();
});
});
Asynchronous Testing Reference
expect.assertions reference
Your code looks correct. Why do you say that it doesn't wait for the Promise to reject? The only difference I would make would be to make use of Jest's mocking capability, so change
Mock
export const createUser = function() {
return new Promise((resolve, reject) => {
reject('error');
});
};
to
Test
jest.mock('../services');
const services = require('../services');
const createUser = jest.spyOn(services, "createUser");
createUser.mockRejectedValue("error");
...
it('rejects...', () => {
There's no need to have a separate Mock file
In your code handleFormSubmit function should return Promise on which you can wait in your test. Also you need to return truthful data from success and error callback to resolve and reject the promise respectively.
handleFormSubmit = () => {
this.setState({ loading: true });
return createUser()
.then(() => {
this.setState({
loading: false,
});
return true;
})
.catch(e => {
this.setState({
error: e,
});
throw e;
});
};
Here in your actual code you have caught the error in catch handler and trying to catch it further in out test case code. Hence catch can not be chained further, while you can chain then multiple times.
For reference go through Promise documentations:
https://www.peterbe.com/plog/chainable-catches-in-a-promise
Context: React application with mobx.
Anyway I've a class (a store), catalogStore, with a loadProducts method. This method call a service to get the data and then, return it.
I've to write a test that say "If it cannot get the data, then throw an exception"
I mocked the function supposed to get the data, forcing it to reject... ok
This is the test I wrote
describe("catalogStore", () => {
describe("if the catalog fails to get the data", () => {
beforeAll(() => {
catalogService.get = jest.fn().mockImplementation(() => {
return new Promise((resolve, reject) => {
reject("rejected error");
});
});
});
it("should throw an error", () => {
return expect(() => catalogStore.loadProducts()).toThrow();
});
});
});
And this is the loadProducts function:
loadProducts() {
return catalogService
.get()
.then(result => {
this.products = result.services;
return {products: this.products};
})
.catch(error => {
console.log("CatalogStore loadProducts error catch: ", error);
return { error };
})
.then(({ error }) => {
if (error) {
console.log("Im gonna throw the error -> ", error);
throw error;
}
});
}
From the logs I can see "Im gonna throw the error -> rejected error", but the test fails with this message:
Expected the function to throw an error. But it didn't throw anything.
Why? I'm throwing the error.
Luca
Your error is thrown in the context of a Promise chain callback. It will be caught by the Promise and passed to the next catch handler.
To modify your test to inspect the error you could use Jest's Promise expectations:
describe("catalogStore", () => {
describe("if the catalog fails to get the data", () => {
beforeAll(() => {
catalogService.get = jest.fn().mockImplementation(() => {
return new Promise((resolve, reject) => {
reject("rejected error");
});
});
});
it("should throw an error", () => {
return expect(catalogStore.loadProducts()).rejects.toThrow('rejected error');
});
});
});
Its cause the function returns a promise, so all jest see is that the get() function is the called but as the error happens in a promise the test is finished before the error is thrown. To test that promises have look on how async error handling works.
Main idea is that you have an async function where you catch the failing promises by yourself
it('fails', async()=>{
try{
await catalogStore.loadProducts()
} catch(e) {
expect(e).toBeDefined()
}
})
I have the following code:
callAPI() {
return this.myModel.map(action(retValue => {
return this.myApi.doWork(retValue.Input).then(action(model => {
this.model.Input = Object.assign({}, model);
this.saveState();
})).catch(error => {
throw(error);
});
}));
if my client code I am doing something like this:
myStore.callAPI().then(() => {
console.log("completed");
this.setState({ loading: false });
});
I am getting an error
.then() is not a function
I want to get a callback when all async operations are completed.
Please use Promise.all to await multiple promises and return the result which is another Promise which you can call .then on.
In order to always return a promise after an imperative block of code you can return Promise.resolve() that will make the code chainable.
callAPI() {
return Promise.all(
this.myModel.map(action(retValue => (
this.myApi.doWork(retValue.Input).then(action(model => {
this.model.Input = Object.assign({}, model);
this.saveState();
return Promise.resolve();
}))
)));
);
}
Please see an example:
const sleep = timeout => new Promise(resolve =>
setTimeout(() => resolve(timeout), timeout)
);
const tap = timeout => {
console.log(`Task completed after ${timeout}ms`);
return timeout;
}
const tasks = [
sleep(1000),
sleep(2000),
sleep(500),
sleep(30),
sleep(2500),
sleep(450)
].map(task => task.then(tap));
Promise.all(tasks).then(x => {
console.log(x);
});
I have ajax call in componentdidmount. And and then setState inside the ajax promise.
The code is like this
componentDidMount(){
axios.post('mydomian.com/item/',this.state)
.then(function (response) {
const res = response.data
if (res.status === 'OK') {
this.setState({items :res.list})
}else{
console.log('can not load data', response)
}
}.bind(this))
}
componentWillUnmount(){
how to stop everything about axios?
}
This causes error 'can not setstate on an unmounted component', when I navigate to other route.
So I think what I should do is remove axios listener in the componentwillunmount. How to would you do it?
A very simple solution could be to set a flag on unmount and utilize it within the promise resolution, like so:
componentDidMount(){
axios.post('mydomian.com/item/',this.state)
.then(function (response) {
if (this.unmounted) return;
const res = response.data
if (res.status === 'OK') {
this.setState({items :res.list})
}else{
console.log('can not load data', response)
}
}.bind(this))
}
componentWillUnmount(){
this.unmounted = true;
}
I have find a great solution that has been defined by istarkov
const makeCancelable = (promise) => {
let hasCanceled_ = false;
const wrappedPromise = new Promise((resolve, reject) => {
promise.then((val) =>
hasCanceled_ ? reject({isCanceled: true}) : resolve(val)
);
promise.catch((error) =>
hasCanceled_ ? reject({isCanceled: true}) : reject(error)
);
});
return {
promise: wrappedPromise,
cancel() {
hasCanceled_ = true;
},
};
};
How to use:
const somePromise = new Promise(r => setTimeout(r, 1000));
const cancelable = makeCancelable(somePromise);
cancelable
.promise
.then(() => console.log('resolved'))
.catch(({isCanceled, ...error}) => console.log('isCanceled', isCanceled));
// Cancel promise
cancelable.cancel();
the solution has been found there.
My implementation.
Inside my function
const promiseShareByEmail = makeCancelable(this.props.requestShareByEmail(obj.email, obj.url));
promiseShareByEmail.promise.then(response => {
const res = response.data;
if (res.code != 0)
throw new Error(res.message);
this.setState({
message: {
text: TextMeasurements.en.common.success_share_test,
code: Constants.ALERT_CODE_SUCCESS
}
});
}).catch(err => {
if (err.isCanceled)
return;
this.setState({
message: {
text: err.message,
code: Constants.ALERT_CODE_ERROR
}
})
});
this.promiseShareByEmail = promiseShareByEmail;
this.props.requestShareByEmail(obj.email, obj.url) that function returns promise from axios.
when component will unmount cancel function should be invoked.
componentWillUnmount() {
this.promiseShareByEmail.cancel();
}
enjoy.