Get updated state of component when calling method on class via enzyme - reactjs

How can I get my test to reflect the new state of my component after calling updateItem on it. I did try rendered.update() but I must be missing on how something works. Thanks.
Method on class:
updateItem (id) {
return updateItem(id)
.then((result) => {
this.setState({
item: {blah:'blah'}
});
}, (error) => {
throw error;
});
}
Test:
it("should update item accordingly", () => {
const rendered = shallow(React.createElement(Item));
const result = rendered.instance().updateItem();
expect(rendered.state('item')).toEqual({blah:'blah'});
})

You have to get the function from the childs prop and call it. As it returns a promise you need to use async/await or return the promise from your test, have a look at docs for more infos. Then test the state of the rendered component.
it("should update item accordingly", async() => {
const rendered = shallow(React.createElement(Item));
await rendered.find('someChild).prop('updateItem')('someId')
expect(item.state).toEqual({
item: {blah:'blah'}
})
})

Related

How to trigger setState on a mocked hook

I have a component that uses a hook that fetches data from the server, and I have mocked that hook to return my testing data.
Now if the mutate function (returned by the hook) is called, the normal implementation fetches the data again and causes a re-render (I'm using swr, here the mutate reference).
How to I trigger a re-render / setState on a mocked hook?
What I want to test: simply, if the user creates an item the item list should be re-fetched and displayed.
Code to illustrate the issue:
const existing = [...]
const newlyCreated = [...];
useData.mockReturnValue({ data: [existing] });
const { getByRole, findByText } = render(<MyComponent />);
const form = getByRole("form");
const createButton = within(form).getByText("Create");
useData.mockReturnValue({ data: [existing, newlyCreated] });
// createButton.click();
// Somehow trigger re-render???
for (const { name } of [existing, newlyCreated]) await findByText(name);
You don't need to trigger a re-render in your test.
The issue is: the button on your UI mutates the data, but since you're mocking useData that mutation isn't happening.
You can simply add mutate() to your mock and assign it a mock function.
You don't need to unit test the inner working of SWR's own mutate() - that's already covered by their own project.
const existing = ["Mercury", "Venus", "Earth", "Mars"];
const newItem = "Jupiter";
test("Create button should call mutate() with correct value", async () => {
const mutate = jest.fn();
jest.spyOn(useData, "default").mockImplementation(() => ({
data: existing,
mutate
}));
let result;
act(() => {
result = render(<Form />);
});
await waitFor(() => {
existing.forEach((item) => {
expect(result.getByText(item)).toBeInTheDocument();
});
});
const input = result.container.querySelector("input");
fireEvent.change(input, { target: { value: newItem } });
const createButton = result.getByText("Create");
createButton.click();
expect(mutate).toBeCalledWith([...existing, newItem], false);
});
Example on CodeSandbox

Race condition in React setState and Promise

In my Context I have a LocalFunction that returns a promise.
LocalFunction: () => Promise<void>
LocalFunction: () => {
return externalCall.getBooks().then((books) => {
this.setState({ Books: books })
})
}
I can call this function in another component based on the updated Books object in the Context state like:
this.props.LocalFunction().then(() => {
// do something with this.props.Context.Books
})
But I know React updates states in batches. So could I run into a race condition when calling LocalFunction without the Books state being updated with the new books?
I know a way to avoid it is to wrap LocalFunction in a new Promise and resolve it in this.setState({ Books: books }, resolve), but I wanna avoid doing that if possible.
How about to use async/await?
LocalFunction: async (needUpdate = false) => {
const result = await externalCall.getBooks();
if(needUpdate){
this.setState({ Books: result })
}
return result;
}
this.props.LocalFunction().then((res) => {
console.log(res)
// do something with this.props.Context.Books
})
When you need to update state
LocalFunction(true)

Jest/Enzyme/Reactjs testing function used by react component

Hi I have this function (apiCall) that calls an API inside a component and uses the data to update state (to then render a chart with chartjs). I want to test specifically the process inside componentDidMount that updates state without calling the API. After lots of time spent searching for a way of mocking this I still haven't been able to figure it out. Trying to assert the changed state from a mock apiCall function.
this is the apiCall function:
const apiCall = (uri) => {
return fetch(uri)
.then( (res) => {
return res
})
.catch( (ex) => {
return 0
})
}
export default apiCall;
// and this is the componentDidMount
componentDidMount() {
apiCall(this.props.uri)
.then((result) => result.json())
.then((result) => {
this.setState({ data: result });
})
this.setState({ legend: this.props.legend })
}
One of the options is to use fetch-mock
http://www.wheresrhys.co.uk/fetch-mock/
Use proxyquire and mock promise function

React, The function does not load data

How to rewrite the function so that it is updated and loaded every time you change pages. The fact is that the loading function works only on one page, but it does not pass to others, how to change it?
function loadModel(model) {
return function(dispatch) {
dispatch(moveToPending(model))
const resource = require(`../resources/${model}`)
const resourceActions = bindActionCreators(resource.actions, dispatch)
const toaster = new Toaster(dispatch)
return new Promise((resolve, reject) => {
resourceActions[getFunctionName(model)]()
.then(res => {
resolve(model)
dispatch(resolveSubscriptions(model))
})
.catch(err => {
if (debug) console.error(err)
reject({ ...err, model })
dispatch(resolveSubscriptions(model))
toaster.error(`Could not load ${model}!`)
})
})
}
}
Update.
Here's the componentWillMount(), I already have it, what do I need to add to it?
componentWillMount() {
this.props.actions.subscribe(this.subscriptions)
.then(() => {
this.props.actions.fetchRegisters({year: this.state.currentYear, month: defaultMonths()})
.then(() => {
if (!this.props.registers.length) {
this.toaster.warning('There is no data for charts')
}
this.createReportState()
})
})
}
React has some lifecycle methods. You can use componentWillMount or componentDidMount for this purpose. You can pass this function as a prop to other pages and there you can call it in componentWillMount, something like:
componentWillMount() {
this.props.loadModel(//arg);
}
For reference: Component life-cycle methods

How to wait in enzyme for a promise from a private function?

I'm a novice at react and any javascript testing frameworks.
I have a simple component that retrieves an item from the API and shows them to the screen.
The function getItems() is called from componentWillMount.
Is it possible to wait until getItems() has finished before making my assertions?
ItemDetails.js
class ItemDetails extends Component {
constructor(props) {
super(props);
this.state = {
details: ''
}
}
componentWillMount() {
this.getItem();
}
getItem() {
const itemId = this.props.match.params.id;
fetch(`/api/items/${itemId}`)
.then(res => res.json())
.then(details => this.setState({ details }));
}
render() {
const details = this.state.details;
return (
<div>
<h1>{details.title}</h1>
...
</div>
);
}
}
export default ItemDetails;
ItemDetails.test.js
describe('ItemDetails', () => {
it('should render a div with title', () => {
const details = {
_id: 1,
title: 'ItemName'
};
fetch.mockResponseOnce(JSON.stringify(details));
const wrapper = mount(<ItemDetails match={{ params: {id: 1} }} />);
expect(wrapper.find('div').find('h1').text()).toBe('ItemName');
});
});
The answer above works, but it invites to test implementation details:
as a rule of thumb, accessing the instance of a wrapper (.instance()) and calling update() are smells of tests that know too much about the way the code under test works in my opinion, it is not testing a behavior, it is testing an implementation
having the method return a promise and wait on that promise before executing the expectation breaks the privacy of the method: it make the code hard to refactor
What you really want is to wait on all promises to be resolved, without accessing a handle to those promises (that would break privacy). Try this
const wait = () => new Promise(resolve => setTimeout(resolve));
it('does something', () => {
renderFnThatCallsAPromiseInternally();
return wait().then(() => {
expect(somethingDependentOnPromiseExecution).to.be.present();
});
});
This will wait for all inner promises to resolve, provided wait is called after the code that enqueues promises (rendering the component) is called. The reason lies in how the JS event loop works, it's finicky, but very well explained by Jake Archibald in this talk: https://www.youtube.com/watch?v=cCOL7MC4Pl0.
Hope that helps.
Could you try:
describe('ItemDetails', () => {
it('should render a div with title', () => {
const details = {
_id: 1,
title: 'ItemName'
};
fetch.mockResponseOnce(JSON.stringify(details));
const wrapper = shallow(<ItemDetails match={{ params: {id: 1} }} />);
// manually call function
wrapper.instance().getItem();
// update to re-render component
wrapper.update();
expect(wrapper.find('div').find('h1').text()).toBe('ItemName');
});
});
If it doesn't help I think you will need to return Promise from your function (base on this example):
getItem() {
const itemId = this.props.match.params.id;
return fetch(`/api/items/${itemId}`)
.then(res => res.json())
.then(details => this.setState({ details }));
}
describe('ItemDetails', () => {
it('should render a div with title', () => {
const details = {
_id: 1,
title: 'ItemName'
};
fetch.mockResponse(JSON.stringify(details)); //response gets called twice
const wrapper = mount(<ItemDetails match={{ params: {id: 1} }} />);
// return Promise so Jest will wait until it's finished
return wrapper.instance().getItem().then(() => {
wrapper.update();
}).then(() => {
expect(wrapper.find('div').find('h1').text()).toBe('ItemName');
})
});
});

Resources