How to take a jest snapshot after axios fetched data in componentDidMount? - javascript

Component to test
class Carousel extends React.Component {
state = {
slides: null
}
componentDidMount = () => {
axios.get("https://s3.amazonaws.com/rainfo/slider/data.json").then(res => {
this.setState({ slides: res.data })
})
}
render() {
if (!slides) {
return null
}
return (
<div className="slick-carousel">
... markup trancated for bravity
</div>
)
}
}
export default Carousel
Test
import React from "react"
import renderer from "react-test-renderer"
import axios from "axios"
import Carousel from "./Carousel"
const slides = [
{
ID: "114",
REFERENCE_DATE: "2018-07-02",
...
},
{
ID: "112",
REFERENCE_DATE: "2018-07-06",
...
},
...
]
jest.mock("axios")
it("", () => {
axios.get.mockImplementationOnce(() => Promise.resolve({ data: slides }))
const tree = renderer.create(<Carousel />).toJSON()
expect(tree).toMatchSnapshot()
})
snapshot only records null, since at the moment of execution I suppose state.slides = null.
Can't put my finger on how to run expectations after axios done fetching the data.
Most of the samples online either use enzyme, or show tests with async functions that return promises. I couldn't find one that would show example only using jest and rendered component.
I tried making test function async, also using done callback, but no luck.

in short:
it("", async () => {
axios.get.mockImplementationOnce(() => Promise.resolve({ data: slides }))
const tree = renderer.create(<Carousel />);
await Promise.resolve();
expect(tree.toJSON()).toMatchSnapshot()
})
should do the job
in details: besides you have mocked call to API data is still coming in async way. So we need toMatchSnapshot call goes to end of microtasks' queue. setTimeout(..., 0) or setImmediate will work too but I've found await Promise.resolve() being better recognizable as "everything below is coming to end of queue"
[UPD] fixed snippet: .toJSON must be after awaiting, object it returns will never be updated

The accepted answer started to fail the next day. After some tweaking, this seems to be working:
import React from "react"
import renderer from "react-test-renderer"
import axios from "axios"
import Carousel from "./Carousel"
jest.mock("axios")
const slides = sampleApiResponse()
const mockedAxiosGet = new Promise(() => ({ data: slides }))
axios.get.mockImplementation(() => mockedAxiosGet)
// eventhough axios.get was mocked, data still comes anychrnonously,
// so during first pass state.slides will remain null
it("returns null initally", () => {
const tree = renderer.create(<Carousel />).toJSON()
expect(tree).toMatchSnapshot()
})
it("uses fetched data to render carousel", () => {
const tree = renderer.create(<Carousel />)
mockedAxiosGet.then(() => {
expect(tree.toJSON()).toMatchSnapshot()
})
})
function sampleApiResponse() {
return [
{
ID: "114",
REFERENCE_DATE: "2018-07-02",
...
},
{
ID: "114",
REFERENCE_DATE: "2018-07-02",
...
},
]
}

Related

React push Api response in the setState

in my EventForm i have this const, this is a dialog form
this is my EventForm.js
const EventForm = (props) => {
const { setOpenPopup, records, setRecords, setMessage, setOpenSnackbar } = props
const addEvent = () => {
axios.post('https://jsonplaceholder.typicode.com/events', (event)
.then(resp => {
console.log(resp.data)
const newData = [{
title: resp.data.name,
start: resp.data.starts_at,
end: resp.data.ends_at
}]
setRecords([{ ...records, newData}])
//
setOpenPopup(false)
setMessage('New Event added')
setOpenSnackbar(true)
})
.catch([])
}
export default EventForm
EventForm.propTypes = {
setOpenPopup: PropTypes.func,
records: PropTypes.array,
setRecords: PropTypes.func,
setMessage: PropTypes.func,
setOpenSnackbar: PropTypes.func
}
}
in my EventTable.js
const [records, setRecords] = useState([]);
useEffect(() => {
axios.get('https://jsonplaceholder.typicode.com/events')
.then(resp => {
const newData = resp.data.map((item) => ({
title: item.name,
start: item.starts_at,
end: item.ends_at
}))
setRecords(newData)
})
.catch(resp => console.log(resp))
}, [])
fullcalendar...
events={records}
im trying to push the API post response to my setRecords. so when the dialog form close it will not use the GET response. ill just get the new record and render to my view
but im getting an error:
Unhanded Rejection (TypeError): setRecords is not a function
I suspect you are using React Hooks. Make sure that your records state looks like this
const [records, setRecords] = useState([]);
In your axios request, it looks like that you are trying to spread the values of records which is an array to an object. I'd suggest refactoring this to something like this. Instead of trying to spread an array into the object, take the previous state and merge it with the new one.
setRecords(prevRecords => [...prevRecords, ...newData])
Here's an example using React Hooks how the component could look like
import React from "react";
import axios from "axios";
const MyComponent = ({
setOpenPopup,
records,
setRecords,
setMessage,
setOpenSnackbar
}) => {
const addEvent = () => {
axios
.post("https://jsonplaceholder.typicode.com/events", event) // Make sure this is defined somewhere
.then((resp) => {
const { name, starts_at, ends_at } = resp.data;
const newData = [
{
title: name,
start: starts_at,
end: ends_at
}
];
setRecords((prevRecords) => [...prevRecords, ...newData]);
setOpenPopup(false);
setMessage("New Event added");
setOpenSnackbar(true);
})
.catch([]);
};
return (
<div>
<button onClick={addEvent}>Click me </button>
</div>
);
};
export default MyComponent;
If you are not using React Hooks and use Class components, then make sure that you pass setRecords to your component in props. Plus, in your props destructuring, make sure you add this to the props, otherwise, it can lead to unwanted behaviour. Also, move your request function out of the render method and destructure values from the props that you need inside the function. I've also noticed that your axios syntax was incorrect (forgot to close after the event) so I fixed that as well. Here's an example of how you can improve it.
import React from "react";
import axios from "axios";
class MyComponent extends React.Component {
addEvent = () => {
const {
setOpenPopup,
setRecords,
setMessage,
setOpenSnackbar
} = this.props;
axios
.post("https://jsonplaceholder.typicode.com/events", event)
.then((resp) => {
console.log(resp.data);
const newData = [
{
title: resp.data.name,
start: resp.data.starts_at,
end: resp.data.ends_at
}
];
setRecords((prevRecords) => [...prevRecords, ...newData]);
//
setOpenPopup(false);
setMessage("New Event added");
setOpenSnackbar(true);
})
.catch([]);
};
render() {
return (
<div>
<button onClick={() => this.addEvent()}>Click me</button>
</div>
);
}
}
export default MyComponent;

How to Mock A Simple API call (Get Todos ) in ComponentDidMount - React + Typescript + Jest + Enzyme

The app
The app is a simple To Do List. This App gets the todos from https://jsonplaceholder.typicode.com/todos?&_limit=5.
What I am trying to do
Test an API call that is executed in ComponentDidMount in App.tsx.
I want to mock the API call and return a list with two items. Then check if there are two items in the array or state.
What files are important for you?
App.tsx (Component to be tested)
ToDoList.test.tsx (Contains test function)
Small part of App.tsx to simplify it
class App extends Component {
public state: IState = {
items: [],
value : 5,
deleteItemParent : this.deleteItemParent
};
getAllTodos = async () => {
await fetch(
`https://jsonplaceholder.typicode.com/todos?&_limit=${this.state.value}`
)
.then((response) => response.json())
.then((json) => {
this.setState({ items: json })
});
};
componentDidMount() {
this.getAllTodos();
}
componentDidUpdate(prevProps : any, prevState: any) {
// Updates todo's if state changed
if (prevState.value !== this.state.value) {
this.getAllTodos();
}
}
render(){
return (
<div className="App">
<AddToDo addToDoParent={this.addToDo}/>
<div>
Todo's : {this.state.value}
</div>
<ToDoList items={this.state.items} deleteFromParent={this.deleteItemParent}/>
</div>
);
}
}
ToDoListMock.test.tsx
import React from 'react';
import { shallow , mount, ShallowWrapper} from 'enzyme';
import App from "../App";
global.fetch = jest.fn(() =>
Promise.resolve({
json: () => Promise.resolve(
[
{
title: "Todo1",
completed: false
},
{
title: "Todo2",
completed: false
}
]
)
})
) as jest.Mock<any>
it("must contain 2 items in state after mock api call", async () => {
const wrapper = shallow(<App />);
await new Promise(res => setTimeout(res));
// await jest.requireActual('promise').resolve()
// Collect items from state
const itemsFromState : any = wrapper.state('items');
// Expect it to have a length of 2
expect(itemsFromState.length).toBe(2);
})
The Error
Expected: 2
Received: 0
EDIT
SetupTests.ts
/* eslint-disable import/no-extraneous-dependencies */
import Enzyme from 'enzyme';
import ReactSixteenAdapter from 'enzyme-adapter-react-16';
Enzyme.configure({ adapter: new ReactSixteenAdapter() });
I noticed an error while doing the test.
There are a few things you have to change to make it work:
You forgot to trigger to fetch your notes as your component mounted by putting to componentDidMount:
componentDidMount() {
this.getAllTodos();
}
Like you said, the data response is the list which is not a literal object, so you have to change your mock returning an array instead of literal object with todos property. But to make sure it runs well, we have to move beforeEach:
beforeEach(() => {
global.fetch = jest.fn(() =>
Promise.resolve({
json: () => Promise.resolve([ // Returns as an array here
{
title: "Todo1",
completed : false
},
{
title: "Todo2",
completed : false
}
])
})
) as jest.Mock<any>
})
Delay your test before getting the state since you have a mock promise need to be done:
it("must contain 2 items in state after mock api call", async () => {
const wrapper = shallow(<App />);
// Delay with either following ways to wait your promise resolved
await new Promise(res => setTimeout(res));
// await jest.requireActual('promise').resolve()
// Collect items from state
const itemsFromState: Array<any> = wrapper.state('items');
// Expect it to have a length of 2
expect(itemsFromState.length).toBe(2);
})
I also created a link for you to compare your code to: https://repl.it/#tmhao2005/Todo

Waiting for async function in React component & Showing Spinner

Beginner here.
Trying to fetch some data from a server and display it in my react component once its fetched.
However, I am having trouble integrating the async function into my react component.
import React, { useState } from "react";
import { request } from "graphql-request";
async function fetchData() {
const endpoint = "https://localhost:3090/graphql"
const query = `
query getItems($id: ID) {
item(id: $id) {
title
}
}
`;
const variables = {
id: "123123123"
};
const data = await request(endpoint, query, variables);
// console.log(JSON.stringify(data, undefined, 2));
return data;
}
const TestingGraphQL = () => {
const data = fetchData().catch((error) => console.error(error));
return (
<div>
{data.item.title}
</div>
);
};
export default TestingGraphQL;
I'd like to simply show a spinner or something while waiting, but I tried this & it seems because a promise is returned I cannot do this.
Here you would need to use the useEffect hook to call the API.
The data returned from the API, I am storing here in a state, as well as a loading state to indicate when the call is being made.
Follow along the comments added in between the code below -
CODE
import React, { useState, useEffect } from "react"; // importing useEffect here
import Layout from "#layouts/default";
import ContentContainer from "#components/ContentContainer";
import { request } from "graphql-request";
async function fetchData() {
const endpoint = "https://localhost:3090/graphql"
const query = `
query getItems($id: ID) {
item(id: $id) {
title
}
}
`;
const variables = {
id: "123123123"
};
const data = await request(endpoint, query, variables);
// console.log(JSON.stringify(data, undefined, 2));
return data;
}
const TestingGraphQL = () => {
const [data, setData] = useState(null);
const [loading, setLoading] = useState(true);
// useEffect with an empty dependency array works the same way as componentDidMount
useEffect(async () => {
try {
// set loading to true before calling API
setLoading(true);
const data = await fetchData();
setData(data);
// switch loading to false after fetch is complete
setLoading(false);
} catch (error) {
// add error handling here
setLoading(false);
console.log(error);
}
}, []);
// return a Spinner when loading is true
if(loading) return (
<span>Loading</span>
);
// data will be null when fetch call fails
if (!data) return (
<span>Data not available</span>
);
// when data is available, title is shown
return (
<Layout>
{data.item.title}
</Layout>
);
};
since fetchData() returns a promise you need to handle it in TestingGraphQL. I recommend onComponentMount do your data call. Setting the data retrieved into the state var, for react to keep track of and re-rendering when your data call is finished.
I added a loading state var. If loading is true, then it shows 'loading' otherwise it shows the data. You can go about changing those to components later to suit your needs.
See the example below, switched from hooks to a class, but you should be able to make it work! :)
class TestingGraphQL extends Component {
constructor() {
super();
this.state = { data: {}, loading: true};
}
//when the component is added to the screen. fetch data
componentDidMount() {
fetchData()
.then(json => { this.setState({ data: json, loading: false }) })
.catch(error => console.error(error));
}
render() {
return (
{this.state.loading ? <div>Loading Spinner here</div> : <div>{this.state.data.item.title}</div>}
);
}
};

Handling promises with fetch() in React?

I'm creating my first MERN stack application, and trying to implement a simple API that calls my express server from my React front-end components. I have the API working on the back end, and it is sending the data correctly through fetch(), but I'm having trouble resolving the promise from fetch() in my React component, with the call not stopping firing. My code looks as follows (assuming as of right now all API calls return a dummy format like { title: 'foo', ... }:
import React, { useState } from 'react';
import 'core-js/stable';
import 'regenerator-runtime/runtime';
const getApiData = async (route) => {
try {
let apiData = await fetch(route);
let apiDataJson = await apiData.json();
return apiDataJson;
} catch (err) {
throw new Error('Error on fetch', {
error: err
})
}
}
var retrieve_data = async (route, setterCallback) => {
await getApiData(`/api/${route}`).then((data) => {
console.log('Data retrieved from API')
setterCallback(<div>{data.title}</div>)
}).catch(() => {
setterCallback(<div>ERROR</div>)
})
}
const MyComponent = () => {
const [innerDiv, setinnerDiv] = useState(0);
let data = retrieve_data('myEndpoint', setinnerDiv);
return(
<div>
<h1>Data Retrieved in MyComponent:</h1>
{innerDiv}
</div>
);
}
When I compile the above the component successfully renders (i.e. <MyComponent /> looks like:
<div>
<h1>Data Retrieved in MyComponent:</h1>
<div>foo</div>
</div>
However, then then block keeps executing (i.e. the 'Data retrieved from API' logs to the console hundreds of times/second until I close the application. How can I stop this from executing once it has set the component? Thanks!
You need to useEffect to stop the component from re-rendering. Try something like this.
const MyComponent = () => {
const [innerDiv, setinnerDiv] = useState(0);
useEffect(() => {
retrieve_data('myEndpoint', setinnerDiv);
}, []);
return(
<div>
<h1>Data Retrieved in MyComponent:</h1>
{innerDiv}
</div>
);
}

Trigger useEffect in Jest and Enzyme testing

I'm using Jest and Enzyme to test a React functional component.
MyComponent:
export const getGroups = async () => {
const data = await fetch(groupApiUrl);
return await data.json()
};
export default function MyWidget({
groupId,
}) {
// Store group object in state
const [group, setGroup] = useState(null);
// Retrive groups on load
useEffect(() => {
if (groupId && group === null) {
const runEffect = async () => {
const { groups } = await getGroups();
const groupData = groups.find(
g => g.name === groupId || g.id === Number(groupId)
);
setGroup(groupData);
};
runEffect();
}
}, [group, groupId]);
const params =
group && `&id=${group.id}&name=${group.name}`;
const src = `https://mylink.com?${params ? params : ''}`;
return (
<iframe src={src}></iframe>
);
}
When I write this test:
it('handles groupId and api call ', () => {
// the effect will get called
// the effect will call getGroups
// the iframe will contain group parameters for the given groupId
act(()=> {
const wrapper = shallow(<MyWidget surface={`${USAGE_SURFACES.metrics}`} groupId={1} />)
console.log(wrapper.find("iframe").prop('src'))
})
})
The returned src doesn't contain the group information in the url. How do I trigger useEffect and and everything inside that?
EDIT: One thing I learned is the shallow will not trigger useEffect. I'm still not getting the correct src but I've switched to mount instead of shallow
Here's a minimal, complete example of mocking fetch. Your component pretty much boils down to the generic fire-fetch-and-set-state-with-response-data idiom:
import React, {useEffect, useState} from "react";
export default function Users() {
const [users, setUsers] = useState([]);
useEffect(() => {
(async () => {
const res = await fetch("https://jsonplaceholder.typicode.com/users");
setUsers(await res.json());
})();
}, []);
return <p>there are {users.length} users</p>;
};
Feel free to run this component in the browser:
<script type="text/babel" defer>
const {useState, useEffect} = React;
const Users = () => {
const [users, setUsers] = useState([]);
useEffect(() => {
(async () => {
const res = await fetch("https://jsonplaceholder.typicode.com/users");
setUsers(await res.json());
})();
}, []);
return <p>there are {users.length} users</p>;
};
ReactDOM.render(<Users />, document.querySelector("#app"));
</script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/babel-standalone/6.26.0/babel.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.13.1/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.13.1/umd/react-dom.production.min.js"></script>
<div id="app"></div>
You can see the component initially renders a value of 0, then when the request arrives, all 10 user objects are in state and a second render is triggered showing the updated text.
Let's write a naive (but incorrect) unit test, mocking fetch:
import {act} from "react-dom/test-utils";
import React from "react";
import Enzyme, {mount} from "enzyme";
import Adapter from "enzyme-adapter-react-16";
import Users from "../src/Users";
Enzyme.configure({adapter: new Adapter()});
describe("Users", () => {
let wrapper;
let users;
beforeEach(() => {
const mockResponseData = [{id: 1}, {id: 2}, {id: 3}];
users = mockResponseData.map(e => ({...e}));
jest.clearAllMocks();
global.fetch = jest.fn(async () => ({
json: async () => mockResponseData
}));
wrapper = mount(<Users />);
});
it("renders a count of users", () => {
const p = wrapper.find("p");
expect(p.exists()).toBe(true);
expect(p.text()).toEqual("there are 3 users");
});
});
All seems well--we load up the wrapper, find the paragraph and check the text. But running it gives:
Error: expect(received).toEqual(expected) // deep equality
Expected: "there are 3 users"
Received: "there are 0 users"
Clearly, the promise isn't being awaited and the wrapper is not registering the change. The assertions run synchronously on the call stack as the promise waits in the task queue. By the time the promise resolves with the data, the suite has ended.
We want to get the test block to await the next tick, that is, wait for the call stack and pending promises to resolve before running. Node provides setImmediate or process.nextTick for achieving this.
Finally, the wrapper.update() function enables synchronization with the React component tree so we can see the updated DOM.
Here's the final working test:
import {act} from "react-dom/test-utils";
import React from "react";
import Enzyme, {mount} from "enzyme";
import Adapter from "enzyme-adapter-react-16";
import Users from "../src/Users";
Enzyme.configure({adapter: new Adapter()});
describe("Users", () => {
let wrapper;
let users;
beforeEach(() => {
const mockResponseData = [{id: 1}, {id: 2}, {id: 3}];
users = mockResponseData.map(e => ({...e}));
jest.clearAllMocks();
global.fetch = jest.fn(async () => ({
json: async () => mockResponseData
}));
wrapper = mount(<Users />);
});
it("renders a count of users", async () => {
// ^^^^^
await act(() => new Promise(setImmediate)); // <--
wrapper.update(); // <--
const p = wrapper.find("p");
expect(p.exists()).toBe(true);
expect(p.text()).toEqual("there are 3 users");
});
});
The new Promise(setImmediate) technique also helps us assert on state before the promise resolves. act (from react-dom/test-utils) is necessary to avoid Warning: An update to Users inside a test was not wrapped in act(...) that pops up with useEffect.
Adding this test to the above code also passes:
it("renders a count of 0 users initially", () => {
return act(() => {
const p = wrapper.find("p");
expect(p.exists()).toBe(true);
expect(p.text()).toEqual("there are 0 users");
return new Promise(setImmediate);
});
});
The test callback is asynchronous when using setImmediate, so returning a promise is necessary to ensure Jest waits for it correctly.
This post uses Node 12, Jest 26.1.0, Enzyme 3.11.0 and React 16.13.1.
With jest you can always mock. So what you need is:
In your unit test mock useEffect from React
jest.mock('React', () => ({
...jest.requireActual('React'),
useEffect: jest.fn(),
}));
That allows to mock only useEffect and keep other implementation actual.
Import useEffect to use it in the test
import { useEffect } from 'react';
And finally in your test call the mock after the component is rendered
useEffect.mock.calls[0](); // <<-- That will call implementation of your useEffect
useEffect has already been triggered and working, the point is that its an async operation. So you need to wait for the fetch to be completed. one of the ways that you can do that is:
1. write your assertion(s)
2. specify the number of assertion(s) in your test, so that jest knows that it has to wait for the operation to be completed.
it('handles groupId and api call ', () => {
// the effect will get called
// the effect will call getGroups
// the iframe will contain group parameters for the given groupId
expect.assertions(1)
const wrapper = shallow(<UsageWidget surface={`${USAGE_SURFACES.metrics}`} groupId={2} />)
wrapper.update()
expect(whatever your expectation is)
});
since in this example i just wrote on assertion,
expect.assertions(1)
if you write more, you need to change the number.
You can set a timeout to asynchronously check if the the expected condition has been met.
it('handles groupId and api call ', (done) => {
const wrapper = shallow(<UsageWidget surface={`${USAGE_SURFACES.metrics}`} groupId={1} />)
setTimeout(() => {
expect(wrapper.find("iframe").prop('src')).toBeTruthy(); // or whatever
done();
}, 5000);
}
The timeout lets you wait for the async fetch to complete. Call done() at the end to signal that the it() block is complete.
You probably also want to do a mock implementation of your getGroups function so that you're not actually hitting a network API every time you test your code.

Categories