Mock multiple fetch calls with state updates in ReactJS - reactjs

I am having a ReactJS component which does two things:
- on ComponentDidMount it will retrieve a list of entries
- on Button click it will submit the select entry to a backend
The problem is that i need to mock both requests (made with fetch) in order to test it properly. In my current testcase i want to test a failure in the submit on the button click. However due some odd reason the setState is triggered however the update from that is received after i want to compare it.
Dumps i did for the test. First one is the state as listen in the test. The second is from the code itself where it is setting state().error to the error received from the call
FAIL react/src/components/Authentication/DealerSelection.test.jsx (6.689s)
● Console
console.log react/src/components/Authentication/DealerSelection.test.jsx:114
{ loading: true,
error: null,
options: [ { key: 22, value: 22, text: 'Stationstraat 5' } ] }
console.log react/src/components/Authentication/DealerSelection.jsx:52
set error to: my error
The actual test code:
it('throws error message when dealer submit fails', done => {
const mockComponentDidMount = Promise.resolve(
new Response(JSON.stringify({"data":[{"key":22,"value":"Stationstraat 5"}],"default":22}), {
status: 200,
headers: { 'content-type': 'application/json' }
})
);
const mockButtonClickFetchError = Promise.reject(new Error('my error'));
jest.spyOn(global, 'fetch').mockImplementation(() => mockComponentDidMount);
const element = mount(<DealerSelection />);
process.nextTick(() => {
jest.spyOn(global, 'fetch').mockImplementation(() => mockButtonClickFetchError);
const button = element.find('button');
button.simulate('click');
process.nextTick(() => {
console.log(element.state()); // state.error null even though it is set with setState but arrives just after this log statement
global.fetch.mockClear();
done();
});
});
});
This is the component that i actually use:
import React, { Component } from 'react';
import { Form, Header, Select, Button, Banner } from '#omnius/react-ui-elements';
import ClientError from '../../Error/ClientError';
import { fetchBackend } from './service';
import 'whatwg-fetch';
import './DealerSelection.scss';
class DealerSelection extends Component {
state = {
loading: true,
error: null,
dealer: '',
options: []
}
componentDidMount() {
document.title = "Select dealer";
fetchBackend(
'/agent/account/dealerlist',
{},
this.onDealerListSuccessHandler,
this.onFetchErrorHandler
);
}
onDealerListSuccessHandler = json => {
const options = json.data.map((item) => {
return {
key: item.key,
value: item.key,
text: item.value
};
});
this.setState({
loading: false,
options,
dealer: json.default
});
}
onFetchErrorHandler = err => {
if (err instanceof ClientError) {
err.response.json().then(data => {
this.setState({
error: data.error,
loading: false
});
});
} else {
console.log('set error to', err.message);
this.setState({
error: err.message,
loading: false
});
}
}
onSubmitHandler = () => {
const { dealer } = this.state;
this.setState({
loading: true,
error: null
});
fetchBackend(
'/agent/account/dealerPost',
{
dealer
},
this.onDealerSelectSuccessHandler,
this.onFetchErrorHandler
);
}
onDealerSelectSuccessHandler = json => {
if (!json.error) {
window.location = json.redirect; // Refresh to return back to MVC
}
this.setState({
error: json.error
});
}
onChangeHandler = (event, key) => {
this.setState({
dealer: event.target.value
});
}
render() {
const { loading, error, dealer, options } = this.state;
const errorBanner = error ? <Banner type='error' text={error} /> : null;
return (
<div className='dealerselection'>
<Form>
<Header as="h1">Dealer selection</Header>
{ errorBanner }
<Select
label='My dealer'
fluid
defaultValue={dealer}
onChange={this.onChangeHandler}
maxHeight={5}
options={options}
/>
<Button
primary
fluid
onClick={this.onSubmitHandler}
loading={loading}
>Select dealer</Button>
</Form>
</div>
);
}
}
export default DealerSelection;

Interesting, this one took a little while to chase down.
Relevant parts from the Node.js doc on Event Loop, Timers, and process.nextTick():
process.nextTick() is not technically part of the event loop. Instead, the nextTickQueue will be processed after the current operation is completed, regardless of the current phase of the event loop.
...any time you call process.nextTick() in a given phase, all callbacks passed to process.nextTick() will be resolved before the event loop continues.
In other words, Node starts processing the nextTickQueue once the current operation is completed, and it will continue until the queue is empty before continuing with the event loop.
This means that if process.nextTick() is called while the nextTickQueue is processing, the callback is added to the queue and it will be processed before the event loop continues.
The doc warns:
This can create some bad situations because it allows you to "starve" your I/O by making recursive process.nextTick() calls, which prevents the event loop from reaching the poll phase.
...and as it turns out you can starve your Promise callbacks as well:
test('Promise and process.nextTick order', done => {
const order = [];
Promise.resolve().then(() => { order.push('2') });
process.nextTick(() => {
Promise.resolve().then(() => { order.push('7') });
order.push('3'); // this runs while processing the nextTickQueue...
process.nextTick(() => {
order.push('4'); // ...so all of these...
process.nextTick(() => {
order.push('5'); // ...get processed...
process.nextTick(() => {
order.push('6'); // ...before the event loop continues...
});
});
});
});
order.push('1');
setTimeout(() => {
expect(order).toEqual(['1','2','3','4','5','6','7']); // ...and 7 gets added last
done();
}, 0);
});
So in this case the nested process.nextTick() callback that logs element.state() ends up running before the Promise callbacks that would set state.error to 'my error'.
It is because of this that the doc recommends the following:
We recommend developers use setImmediate() in all cases because it's easier to reason about
If you change your process.nextTick calls to setImmediate (and create your fetch mocks as functions so Promise.reject() doesn't run immediately and cause an error) then your test should work as expected:
it('throws error message when dealer submit fails', done => {
const mockComponentDidMount = () => Promise.resolve(
new Response(JSON.stringify({"data":[{"key":22,"value":"Stationstraat 5"}],"default":22}), {
status: 200,
headers: { 'content-type': 'application/json' }
})
);
const mockButtonClickFetchError = () => Promise.reject(new Error('my error'));
jest.spyOn(global, 'fetch').mockImplementation(mockComponentDidMount);
const element = mount(<DealerSelection />);
setImmediate(() => {
jest.spyOn(global, 'fetch').mockImplementation(mockButtonClickFetchError);
const button = element.find('button');
button.simulate('click');
setImmediate(() => {
console.log(element.state()); // state.error is 'my error'
global.fetch.mockClear();
done();
});
});
});

There are several asynchronous calls required to update the state, so your process.nextTick() isn't sufficient. To update the state, this needs to happen:
your test code clicks, and the event handler callback is queued
the event handler callback runs, runs fetch, gets a promise rejection, and runs the error handler
the error handler runs setState, which queues the state update (setState is asynchronous!)
your test code runs, checking the element's state
the state update runs
In short, you need to wait longer before asserting on the state.
A useful idiom to "wait" without nested process.nextTick() calls is to define a test helper
function wait() {
return new Promise((resolve) => setTimeout(resolve));
}
and then do
await wait();
as many times as required in your test code. Note that this requires you to define test functions as
test(async () => {
})
rather than
test(done => {
})

Related

Waiting for useEffect to fire after simulating clicking a MUI MenuItem in test

I have a test where I am trying to confirm my mocked fetch() called is fired from inside a useEffect():
await waitFor(async () => {
fireEvent.click(await screen.getByTestId('customMenuItem-0'));
expect(mockFetch).toHaveBeenNthCalledWith(
2,
'Document/ColumnTemplate/GetTemplateColumns?id=' + columnTemplate1.Id,
{
headers: { 'Content-Type': 'application/json' },
method: 'get'
}
);
});
The click() does correctly get the onChange() in my Select element to fire:
<FormControl>
<Select
data-testid='columnTemplatesDropDown'
value={fieldsSettings.columnTemplateSelection}
onChange={(e) => {
setFieldsSettings({
...fieldsSettings,
columnTemplateSelection: e.target.value
});
}}
But this useEffect():
useEffect(() => {
// debugger;
if (fieldsSettings.columnTemplateSelection === TemplateDropdownOptionsType.SELECT_TEMPLATE) {
return;
} else if (fieldsSettings.columnTemplateSelection === TemplateDropdownOptionsType.ALL_FIELDS) {
// fieldListType=5 makes this call return the same fields displayed to the user
// in the Migration Column Template wizard
ReadAndAppendFieldsToRows('Search/Results/GetColumnPickerFields?fieldListType=5', setRows);
} else if (parseInt(fieldsSettings.columnTemplateSelection) > 0) {
GetAsync('Document/ColumnTemplate/GetTemplateColumns?id=' + fieldsSettings.columnTemplateSelection).then(
(response: TemplateColumnsType) => setRows(response.results.columns)
);
}
}, [fieldsSettings.columnTemplateSelection]);
fails to fire, and the test informs me the mockFetch I expected was never called. Am I not properly waiting for my expect(mockFetch)? The useEffect gets called just fine when I run the code in production, just not in this test.

React testing error "Cannot read get property of undefined"

I am trying to test the function searchTrigger in my CardMain component.
export default class CardMain extends Component {
state = {
Pools : [],
loading: false,
}
componentDidMount(){
axios.get('/pools')
.then (res => {
//console.log(res.data.data);
this.setState({
Pools: res.data.data,
loading: true,
message: "Loading..."
},()=>{
if (res && isMounted){
this.setState({
loading: false
});
}
})
}
)
.catch(err=>{
console.log(err.message);
})
}
// the function is for search method
// upon search, this function is called and the state of the pools is changed
searchTrigger = (search) => {
Search = search.toLowerCase();
SearchList = this.state.Pools.filter((e)=> {
if (e.name.toLowerCase().includes(Search)){
this.setState({
loading: false
})
return e
}
})
if (SearchList.length === 0){
this.setState({
loading: true,
message: "No pools found"
})
}
}
render() {
return (
<div>
<Searchbar trigger={this.searchTrigger}/>
{ this.state.loading ?
<div className="d-flex justify-content-center">{this.state.message}</div>
:<div>
{Search === "" ? <Card1 pools={this.state.Pools}/> : <Card1 pools={SearchList}/> }
</div>
}
</div>
)
}
}
The function searchTrigger is passed to another class component called Searchbar which basically displays the search bar. Upon searching something, the function searchTrigger is called and the searched value is passed as an argument to this function.
So, I am trying to test this function and I am new to react and testing. I found some examples online and tried a simple testing whether the function is called or not. My CardMain.test.js code looks like this:
describe("callback function test", ()=> {
it("runs it", () => {
//const spy = jest.spyOn(CardMain.prototype,"searchTrigger");
const cardmain = shallow(<CardMain/>)
const spy = jest.spyOn(cardmain.instance(), "searchTrigger");
expect(spy).toHaveBeenCalled()
})
});
I get the TypeError: Cannot read property 'get' of undefined pointing to the axios.get("/pools") in the CardMain component inside componentDidMount. axios is being imported from another component api.js which creates the instance of axios using axios.create. I have no idea what the problem is. I am very new to react. I have absolutely no idea, how do I test these components? Could somebody help me?
Update:
So, i tried mocking axios call:
let Wrapper;
beforeEach(() => {
Wrapper = shallow( <CardMain/>);
});
describe("Card Main", ()=> {
it("returns data when called", done => {
let mock = new MockAdapter(axios);
const data = [{
name: "Test",
response: true
}];
mock.onGet('My_URL')
.reply(200,data);
const instance = Wrapper.instance();
instance.componentDidMount().then(response => {
expect(response).toEqual(data);
done();
});
});
});
It says "cannot read property .then of undefined"

useState referring to stale value

I have a keeper app where I am adding notes and storing them in database. When I make a post request to the server, I am trying to fetch the _id from database, which will eventually help me to later delete the note ( if needed).
Here is my jsx file
function CreateMessage(props) {
const [currentGuest, setCurrentGuest] = useState({
guestName: '',
guestMessage: '',
id:''
});
function handleMessages(event) {
const {name, value} = event.target;
setCurrentGuest(prevGuest => {
return {
...prevGuest,
[name] : value
};
});
}
function submitMessage(event) {
//props.onAdd(currentGuest);
const params = {
guestName: currentGuest.guestName,
guestMessage: currentGuest.guestMessage,
}
axios
.post("http://localhost:8000/notes", params)
.then(res => {
console.log("The response is"+res.data._id);
console.log(res.status);
setCurrentGuest(prevGuest => {
console.log(res.data._id)
return {
...prevGuest,
id: res.data._id
};
});
console.log(currentGuest);
})
event.preventDefault();
}
return (
<div>
<form>
<input
name="guestName"
placeholder="Guest Name"
value={currentGuest.guestName}
onChange={handleMessages}
/>
<textarea
name="guestMessage"
placeholder="Write a Message"
rows="3"
value={currentGuest.guestMessage}
onChange={handleMessages}
/>
<button onClick={submitMessage}>Add</button>
</form>
</div>
);
}
The id is properly being fetched and displayed in ```console.log("The response is"+res.data._id"). But on first submit, the is always empty and stale id gets attached to the currentGuest object on next submit
function submitMessage(event) {
//props.onAdd(currentGuest);
const params = {
guestName: currentGuest.guestName,
guestMessage: currentGuest.guestMessage,
}
axios
.post("http://localhost:8000/notes", params)
.then(res => {
console.log("The response is"+res.data._id);
console.log(res.status);
setCurrentGuest(prevGuest => {
console.log(res.data._id)
return {
...prevGuest,
id: res.data._id
};
});
console.log(currentGuest);
})
event.preventDefault();
}
In the above snippet, after getting the response you're correctly changing the state but the problem is with where you're checking the changed state(console.log(currentGuest)). You're basically logging before the state is changed.
You can use useEffect hook and log the state inside it. This runs every time the currentGuest Changes.
useEffect(() => {
console.log(currentGuest)
}, [currentGuest])
Update
You can use the modified currentGuest inside the useEffect hook:
useEffect(() => {
console.log(currentGuest)
if(currentGuest.id) {
props.onAdd(currentGuest);
// You can also reset the state here as follows
setCurrentGuest({
guestName: '',
guestMessage: '',
id:''
});
}
}, [currentGuest]) // You might need to add the necessary dependencies to this array.

React app render function executes before state is set?

I am building a chatting app in React, but it crashes right after sending a message. Here is part of the code
state ={
conversationData: null,
message: ''
};
componentDidMount() {
this.setState({conversationData: this.props.conversationData});
};
onSendClicked = () => {
con.addMessage(this.state.conversationData.id, data).then(() => {
this.setState({message: ''}, () => {
this.setState({conversationData: this.state.conversationData.message.push(data)});
});
});
};
and in my render function, I have this
<List>
{this.state.conversationData.message !== null && this.state.conversationData.message !== undefined ?
this.state.conversationData.message.map((mes, index)
........
: <div/>....
The problem is in that loop, it works fine when I load it, but once I press send message and set the state on conversationData, it either returns an empty div and never updates or crashes with .map() is undefined if I eliminate that div. What am I doing wrong?
Based on your current code this is how I would set it up:
state ={
conversationData: this.props.conversationData, // no need to set it as null then update on mount
message: ''
};
onSendClicked = () => {
con.addMessage(this.state.conversationData.id, data).then(() => {
this.setState(({ conversationData }) => ({
message: '',
conversationData: {
...conversationData,
message: [ ...conversationData.message, data ] // where is this data coming from?
} // update conversationData with message
});
};
assuming that 'con' is defined and has a method named 'addMessage' that isn't returning an error
i'm also assuming that you have a type-o and data is being passed in from onSendClicked(data)
then i would do this. take special notice to the spread operator
[...this.state.conversationData.message, data.id]
as opposed to mutating state with .push()
as well as initializing conversationData.message state with an [] to avoid iteration errors on null (check for conversationData.message.length instead of conversationData.message !== null)
state ={
conversationData: {message:[]},
message: ''
};
componentDidMount() {
this.setState({conversationData: this.props.conversationData});
};
onSendClicked = (data) => {
con.addMessage(this.state.conversationData.id, data)
.then(() => {
this.setState({
message: '',
conversationData: [...this.state.conversationData.message, data]
})
});
};
in render:
<List>
{
this.state.conversationData.message.length && this.state.conversationData.message.map((mes, index)=>{
//dosomething with mes and index
})
}
</List>

How to handle errors in React Native

I am developing a react native project.
I am first in React Native.
I have some errors in my project.
I 'd like to know how to handle error in React native.
And how can I see the errors?
if ((this.state.loadedUrl === 'https://www.truthbaron.com/') && (!this.state.newsflag)){
const html = event.nativeEvent.data;
const $ = CheerIO.load(html);
isLoggedIn = Object.keys($(PROFILE_SELECTOR)).includes('0');
if (isLoggedIn) {
if(this.state.messagesflag){
profileLink = $(PROFILE_SELECTOR).eq(0).children().attr('href');
username = profileLink.match(/members\/[a-z]+/)[0].slice(8);
if (username.endsWith('/')) username = username.slice(0, username.length - 1);
this.setState({ url: `${profileLink}messages`, loading: true });
console.log('messages page:' + profileLink);
}else{
profileLink = $(PROFILE_SELECTOR).eq(0).children().attr('href');
console.log('profile page!!!' + profileLink);
this.setState({ url: profileLink, loading: true });
}
}
else {
const loginLink = $(LOGIN_SELECTOR).eq(0).children().attr('href');
console.log('loginLink:' + loginLink);
this.setState({ url: loginLink, loading: true });
}
}
There are 2 ways to handle errors.
1.try {
var test;
test.color;
} catch(err) {
// handle error here
}
2.const previousHandler = ErrorUtils.getGlobalHandler();
ErrorUtils.setGlobalHandler((error, isFatal) => {
// handle the error here
console.log(error);
});
And you can monitor the errors in React Native with Rollbar.
For further more information, you can visit this URL.
https://rollbar.com/blog/react-native-error-monitoring/
There is multiple ways to handle error in react, and it is based on the architecture or code you building.
A standard way might be
try {
//code blocks
catch {
// error .log
}
You can relay on method function such as
console.error
If you are making api request
class IsLoading extends React.Component {
constructor(props) {
super(props);
// initialise our state
this.state = { isLoading: false };
}
componentDidCatch(error, info) {
// if we have a promise then we can deal with it
if(error instanceof Promise) {
// we have a promise so lets update our state
this.setState({ isLoading: true });
// once the promise has resolved we can update our state again and grab the data
error.then((data) => this.setState({ isLoading: false, data }));
}
}
render() {
// as props.children is a function, let's invoke it and p ass in out state
return this.props.children(this.state) }
}
}
const Loader = props => (
<IsLoading>
// this is the function that gets called in the render met hod above
{({isLoading, data}) => (
isLoading
// show some loading text if we're loading
? "Loading..."
// copy our children and pass in the data as a prop :
React.cloneElement(props.children, {data})
)}
</IsLoading>
);

Resources