How can I test a component created by asynchronous call in componentDidMount? - javascript

I'm making a GET request to my API http://localhost:3001/api/cards from the componentDidMount function of a component, so that the api request is made only after the component is rendered for the first time (As suggested by react official guide).
This API sets the state of an array data. In render function, I call data.map function to render multiple components from this array. How should I test whether the desired number of components have been rendered?
My component:
//CardGrid.js
import React from 'react';
import { Card, Col, Row } from 'antd';
import 'antd/dist/antd.css';
import { parseJSON } from './commonfunction';
import './CardGrid.css';
export default class extends React.Component {
constructor()
{
super();
this.state = {
data: {},
};
}
fetchData = async () => {
try
{
const data = await parseJSON(await fetch('http://localhost:3001/api/cards'));
console.log('data');
console.log(data);
this.setState({ data });
}
catch (e)
{
console.log('error is: ');
console.log(e);
}
}
componentDidMount() {
this.fetchData();
}
render() {
return (
<div style={{ background: '#ECECEC', padding: '30px' }}>
<Row gutter={16}>
{Object.keys(this.state.data).map((title) => {
return (<Col span="6" key={title}>
<Card title={title} bodyStyle={{
'fontSize': '6em',
}}>{this.state.data[title]}</Card>
</Col>);
})}
</Row>
</div>
);
}
};
Now I want to check if there are as many Card components being rendered as specified by my API.
I tried this by first mocking the fetch function to return 1 element. Then I use Full DOM Rendering of enzyme and mount the above component and expect it to contain 1 element.
Test case:
// It fails
import React from 'react';
import { Card } from 'antd';
import { mount } from 'enzyme';
import CardGrid from './CardGrid';
it('renders 1 Card element', () => {
fetch = jest.fn().mockImplementation(() =>
Promise.resolve(mockResponse(200, null, '{"id":"1234"}')));
const wrapper = mount(<CardGrid />);
expect(fetch).toBeCalled();
expect(wrapper.find(CardGrid).length).toEqual(1);
expect(wrapper.find(Card).length).toEqual(1);
});
All the tests are passing except that it can't find Card element. Even the fetch mock function is called. It fails until I put a setTimeout function before I try to find Card component.
//It succeeds
import React from 'react';
import { Card } from 'antd';
import { mount } from 'enzyme';
import sinon from 'sinon';
import CardGrid from './CardGrid';
it('renders 1 Card elements', async () => {
fetch = jest.fn().mockImplementation(() =>
Promise.resolve(mockResponse(200, null, '{"id":"1234"}')));
const wrapper = mount(<CardGrid />);
expect(fetch).toBeCalled();
expect(wrapper.find(CardGrid).length).toEqual(1);
await setTimeoutP();
expect(wrapper.find(Card).length).toEqual(1);
});
function setTimeoutP () {
return new Promise(function (resolve, reject) {
setTimeout(() => {
console.log('111111111');
resolve();
}, 2000);
});
}
Is there any concept I'm failing to understand? How should I ideally test such asynchronously loading components? How can I better design them to be easily testable? Any help would be greatly appreciated. Thanks

You have to wait for the resolved promise of your fetch result and for the promise from the parseJSON. Therefor we need to mock parseJSON and let it return a resolved promise as well. Please note that the path needs to be relative to the test file.
import {parseJSON} from './commonfunction'
jest.mock('./commonfunction', () => {parseJSON: jest.fn()}) //this will replace parseJSON in the module by a spy were we can later on return a resolved promise with
it('renders 1 Card elements', async () => {
const result = Promise.resolve(mockResponse(200, null, '{"id":"1234"}'))
parsedResult = Promise.resolve({"id":"1234"})
parseJSON.mockImplementation(()=>parsedResult)
fetch = jest.fn(() => result)
const wrapper = mount(<CardGrid />);
await result;
await parsedResult;
expect(fetch).toBeCalled();
expect(wrapper.find(CardGrid).length).toEqual(1);
expect(wrapper.find(Card).length).toEqual(1);
});

Related

Keep getting error message about avoiding wrapping code in act after wrapping code in act

I'm writing a test for a component that uses a search form. This is in search.js and it imports a Formik form from another file searchForm.js.
I've written this unit test which passes. I've based it off an example in the react testing docs here.
Every time I run it I get this error, which confuses me since I've wrapped all the code in act?
console.error
Warning: An update to Formik inside a test was not wrapped in act(...).
When testing, code that causes React state updates should be wrapped into act(...):
act(() => {
/* fire events that update state */
});
/* assert on the output */
This ensures that you're testing the behavior the user would see in the browser. Learn more at https://reactjs.org/link/wrap-tests-with-act
import React from "react";
import { render, screen, act, fireEvent } from "#testing-library/react";
import Search from "./search.js";
import { ChakraProvider } from "#chakra-ui/react";
test("Search form input", () => {
act(() => {
const setup = () => {
const utils = render(
<ChakraProvider>
<Search />
</ChakraProvider>
);
const searchTermsInput = utils.getByLabelText("search terms input");
return {
searchTermsInput,
...utils,
};
};
const { searchTermsInput } = setup();
fireEvent.change(searchTermsInput, { target: { value: "23" } });
});
expect(screen.getByLabelText("search terms input").value).toBe("23");
});
As per the comment above, fixed this by rewriting the test as follows to allow for some other changes to state that happen in the search.js component which weren't part of this specific test.
import React from "react";
import { render, screen, act, fireEvent, waitFor } from "#testing-library/react";
import Search from "./search.js";
import { ChakraProvider } from "#chakra-ui/react";
test("Search form input", async () => {
act(() => {
const setup = () => {
const utils = render(
<ChakraProvider>
<Search />
</ChakraProvider>
);
const searchTermsInput = utils.getByLabelText("search terms input");
return {
searchTermsInput,
...utils,
};
};
const { searchTermsInput } = setup();
fireEvent.change(searchTermsInput, { target: { value: "23" } });
});
expect(screen.getByLabelText("search terms input").value).toBe("23");
// This removes a code not wrapped in act warning triggered by other state events
// namely setQuery which is called after adding search term inputs
await waitFor(() => screen.queryByText(/Find results/))});

How to reset redux store after each test using #testing-library/react in Next.js?

I'm trying to get #testing-framework/react integrated into my Next.js workflow. To do that, I created a test-utils.js file as suggested in the documentation, where I re-export the render method with all my providers:
import React from 'react';
import { render } from '#testing-library/react';
import { ChakraProvider, ColorModeProvider } from '#chakra-ui/react';
import { Provider as ReduxStore } from 'react-redux';
import { useStore } from './lib/init-store';
import theme from './lib/theme';
const providersWrapper = ({ children }) => {
// As pageProps.initialReduxState is undefined in _app.js
const store = useStore(undefined);
return (
<ReduxStore store={store}>
<ChakraProvider resetCSS theme={theme}>
<ColorModeProvider
options={{
useSystemColorMode: true,
}}>
{children}
</ColorModeProvider>
</ChakraProvider>
</ReduxStore>
);
};
const customRender = (ui, options) =>
render(ui, { wrapper: providersWrapper, ...options });
// re-export everything
export * from '#testing-library/react';
// override render method
export { customRender as render };
On the other hand, I have a Counter component and its tests:
import React from 'react';
import { render, fireEvent, act, cleanup } from '../../test-utils';
import Counter from './index';
describe('Counter works properly', () => {
test('it should increment count when +1 button is clicked', async () => {
await act(async () => {
const { findByText } = render(<Counter />);
const initialCount = await findByText('0');
expect(initialCount).toBeInTheDocument();
const incrementButton = await findByText('+');
fireEvent.click(incrementButton);
const incrementedCount = await findByText('1');
expect(incrementedCount).toBeInTheDocument();
cleanup();
});
});
test('it should decrement count when -1 button is clicked', async () => {
await act(async () => {
const { findByText } = render(<Counter />);
const initialCount = await findByText('0');
expect(initialCount).toBeInTheDocument();
const decrementButton = await findByText('-');
fireEvent.click(decrementButton);
const decrementedCount = await findByText('-1');
expect(decrementedCount).toBeInTheDocument();
cleanup();
});
});
});
The jest setup works perfectly, but the second test, can't find a 0 text, because the state of the first test persists. I've confirmed this by swapping them resulting in the same: The first test passes, but the second fails.
This is weird, since the Testing Library documentation explicitly says that cleanup is called after each test automatically and by default (https://testing-library.com/docs/react-testing-library/setup/?fbclid=IwAR0CgDKrHalIhEUAEuP5S355uVYkTScMBATSIMgMPFcOz4ntsNCqgRA3Jyc#skipping-auto-cleanup).
So I'm a little bit lost here, I even tried to pull the celanup function out of render in both tests, and adding a cleanup() at the end of each test, having no different result.
Because of that, I thing that testing library is not the problem, instead, the redux store is not resetting after each test, causing the second test to read the state of the first. But I can't 100% prove it, and even if I could, I don't know how i'd reset it manually, since cleanup is supposed to happen automatically.
If you are curious, you can read the code of the whole project in this repo: https://github.com/AmetAlvirde/testing-library-redux-next-question/
I really hope you can help me, since being unable to test my code is something I really don't want to live with.
Thank you so much in advance.

How Do I get an HOC wrapped component function from enzyme instance when mounting?

I have parent -> child components, and the child runs fetch inside componentDidMount. The fetch sets state in a redux store that is above parent, and that causes child component to display differently.
The child is also using material-ui withStyles, so it creates an HOC around the component.
In my test, I need to mount the parent component, find the child component, and then see that the fetch has correctly changed state and caused the child to update.
My solution so far is this:
mount parent, find child
call child.instance().fetchFunction().then(() => expect(..))
However, calling instance() on child returns the HOC, and so I get the error:
child.instance(...).fetchFunction is not a function
All solutions I have seen use shallow and dive to get around the HOC, but if I use shallow I will have to create a mock store in the test, and it won't actually test this as an integration test.
I could test the individual fetch call, and then test the component using shallow and pass it the props as if the state were changed, but that doesn't prove that it all works together.
Here is a codesandbox where I have reproduced the issue:
Here is some example code (basically the codesandbox):
App.js
import React from "react";
import Child from "./Child";
class App extends React.Component {
render() {
return <Child />;
}
}
export default App;
Child.js
import React from "react";
import { withStyles } from "#material-ui/core/styles";
const childStyles = {
margin: 0
};
class Child extends React.Component {
state = {
groceries: [],
errorStatus: ""
};
componentDidMount() {
console.log("calling fetch");
this.fetchCall();
}
fetchCall = () => {
return fetch("/api/v1/groceries")
.then(this.checkStatus)
.then(this.parseJSON)
.then(this.setStateFromData)
.catch(this.setError);
};
checkStatus = results => {
if (results.status >= 400) {
console.log("bad status");
throw new Error("Bad Status");
}
return results;
};
setError = () => {
console.log("error thrown");
return this.setState({ errorStatus: "Error fetching groceries" });
};
parseJSON = results => {
console.log("parse json");
return results.json();
};
setStateFromData = data => {
console.log("setting state");
return this.setState({ groceries: data.groceries });
};
render() {
const { groceries } = this.state;
return (
<div id="app">
{groceries.map(grocery => {
return <div key={grocery.id}>{grocery.item}</div>;
})}
</div>
);
}
}
export default withStyles(childStyles)(Child);
App.test.js
import Enzyme from "enzyme";
import Adapter from "enzyme-adapter-react-16";
import React from "react";
import { mount } from "enzyme";
import App from "./App";
import Child from "./Child";
Enzyme.configure({ adapter: new Adapter() });
const mockResponse = (status, statusText, response) => {
return new window.Response(response, {
status: status,
statusText: statusText,
headers: {
"Content-type": "application/json"
}
});
};
describe("App", () => {
describe("ChildApp componentDidMount", () => {
it("sets the state componentDidMount", () => {
console.log("starting test for 200");
global.fetch = jest.fn().mockImplementation(() =>
Promise.resolve(
mockResponse(
200,
null,
JSON.stringify({
groceries: [{ item: "nuts", id: 10 }, { item: "greens", id: 3 }]
})
)
)
);
const renderedComponent = mount(<App />);
const childApp = renderedComponent.find(Child);
childApp
.instance()
.fetchCall()
.then(() => {
console.log("finished test for 200");
expect(childApp.state("groceries").length).toEqual(2);
});
});
it("sets the state componentDidMount on error", () => {
console.log("starting test for 500");
window.fetch = jest
.fn()
.mockImplementation(() =>
Promise.resolve(
mockResponse(
400,
"Test Error",
JSON.stringify({ status: 400, statusText: "Test Error!" })
)
)
);
const renderedComponent = mount(<App />);
const childApp = renderedComponent.find(Child);
childApp
.instance()
.fetchCall()
.then(() => {
console.log("finished test for 500");
expect(childApp.state("errorStatus")).toEqual(
"Error fetching groceries"
);
});
});
});
});
After writing this up, I found the answer, but I feel like this is worth sharing because I was very confused about it.
Instead of using app.find(Child) (component constructor), use app.find('Child') (component display name). This will find the actual component and not the hoc-wrapped component.
enzyme docs for find(selector)

Jest/Enzyme Class Component testing with React Suspense and React.lazy child component

So I converted an import used in a class component to React.lazy import api and wrapped it in a Suspense tag. When I test that class component, enzyme throws an error "Enzyme Internal Error: unknown node with tag 13". Is there a way to render and test the mount of the lazy loaded component rather than using shallow render?
I've already tried async await to wait until the promise of the lazy load resolved but that didn't work neither, like so:
it('async await mount', () async () => {
const wrapper = await mount(<Component />)
}
here's example code:
Component.js
import React, { PureComponent, Suspense } from 'react'
const ChildComponent = React.lazy(() => import('../ChildComponent'))
export default class Component extends PureComponent {
constructor() {
super()
this.state = {
example: null
}
}
doSomething = () => this.setState({
example: 'example'
})
render() {
return (
<div>
<p>Example</p>
<Suspense fallback={<div>...loading</div>}>
<ChildComponent
example={this.state.example}
doSomething={this.doSomething}
/>
</Suspense>
</div>
)
}
}
Component.test.js
import React from 'react'
import renderer from 'react-test-renderer'
import { mount } from 'enzyme'
import Component from '../../Component'
describe('Component', () => {
// snapshot renders loading and not children
it('example snapshot of tree', () => {
const tree = renderer.create(<Component />).toJSON()
expect(tree).toMatchSnapshot()
})
it('example mount test', () => {
// this test fails and throws error I state above
const wrapper = mount(<Component />)
wrapper.setState({ example: 'example' })
expect(wrapper.state.example).toBe('example')
})
})
I read that Enzyme does not support React 16.6 Suspense API yet but I wanted to know if there was still a way to test the mounted so I can use things like simulate and find API from Enzyme.
Solution
The GitHub issue linked by ChuckJHardy has been resolved merged and released. You are now able to use the mount API in enzyme as of 1.14.0.
References
https://github.com/airbnb/enzyme/pull/1975
I needed to test my lazy component using Enzyme. Following approach worked for me to test on component loading completion:
const myComponent = React.lazy(() =>
import('#material-ui/icons')
.then(module => ({
default: module.KeyboardArrowRight
})
)
);
Test Code ->
//mock actual component inside suspense
jest.mock("#material-ui/icons", () => {
return {
KeyboardArrowRight: () => "KeyboardArrowRight",
}
});
const lazyComponent = mount(<Suspense fallback={<div>Loading...</div>}>
{<myComponent>}
</Suspense>);
const componentToTestLoaded = await componentToTest.type._result; // to get actual component in suspense
expect(componentToTestLoaded.text())`.toEqual("KeyboardArrowRight");
This is hacky but working well for Enzyme library.

Array created in a Redux thunk is not being passed properly to the State tree

I'm building a React/Redux project that makes a couple calls to an S3 bucket to grab images, and then render them into the app.
I'm finding that for some reason, I cannot iterate through the array that I set in my state tree as a result of those calls to render them onto the app. I believe that I may be dealing with an array-like object or something within the Promises I created caused this mutation to occur.
First, I have this file in a folder titled utils:
const bucketName = 'foo';
const roleARN = 'arn:aws:s3:::foo';
const identityPoolId = 'us-west-2:anActualIdHere';
AWS.config.update({
region: 'us-west-2',
credentials: new AWS.CognitoIdentityCredentials({
IdentityPoolId: identityPoolId
})
});
const bucket = new AWS.S3({
params: {
Bucket: bucketName,
}
});
const textDecoder = new TextDecoder('utf8');
export function listObjects (callback) {
return bucket.listObjects((error, data) => {
if (error) {
console.error('error: ', error);
return;
}
callback(data.Contents);
});
}
export function getSingleObject (key) {
let getObject = new Promise((resolve, reject) => {
bucket.getObject({
Bucket: bucketName,
Key: key
}, (error, data) => {
if (error) {
console.error('error: ', error);
}
resolve(data.Body.toString('base64'));
});
})
return getObject.then((result) => {
return result;
})
}
What happens here is that listObjects will return an array of all items in a specific S3 bucket.
Then, the getSingleObject function grabs a single object's contents based on a key provided in that list of all items, grabbing its Uint8Array and converting it to a base-64 string.
These two functions are called in a thunk action:
import { listObjects, getSingleObject } from '../utils/index.js';
export function loadPhotos () {
return function (dispatch) {
listObjects((results) => {
let photos = [];
let mapPhotosToArray = new Promise((resolve, reject) => {
results.forEach((singlePhoto) => {
let getSinglePhoto = new Promise((resolve, reject) => {
resolve(getSingleObject(singlePhoto.Key));
});
getSinglePhoto.then((result) => {
photos.push(result);
});
});
resolve(photos);
})
mapPhotosToArray.then((result) => {
dispatch(savePhotos(result));
});
});
}
}
function savePhotos (photos) {
return {
type: 'GET_PHOTOS',
photos
}
}
loadPhotos is the thunk action exposed to my Redux containers. It returns a function that first calls the listObjects function in the utils file, passing it a callback that creates an array called photos.
Then, it creates a new Promise that loops through the results array returned by the listObjects utility function. In each iteration of this results array, I instantiate another new Promise that calls the getSingleObject utility.
Within that iteration, I push the results of getSingleObject into the photos array created within the callback passed into listObjects.
The last thing I do in the loadPhotos thunk action is call the outer Promise and then dispatch the result to the savePhotos action, which finally dispatches the payload object to the store for my reducer to catch.
This is how my reducer looks:
const defaultState = {
photos: []
}
const photos = (state = defaultState, action) => {
switch (action.type) {
case 'GET_PHOTOS':
return Object.assign({}, state, {
photos: action.photos
})
default:
return state;
}
};
export default photos;
Here is how I've set up the entry point to the app:
import React from 'react';
import ReactDOM from 'react-dom';
import { Provider } from 'react-redux';
import { createStore, compose, combineReducers, applyMiddleware } from 'redux';
import thunk from 'redux-thunk';
import photos from './reducers/';
import App from './components/app';
import styles from './styles/styles.css';
const createStoreWithMiddleware = compose(applyMiddleware(thunk))(createStore);
const store = createStoreWithMiddleware(photos, window.__REDUX_DEVTOOLS_EXTENSION__ && window.__REDUX_DEVTOOLS_EXTENSION__());
ReactDOM.render(
<Provider store={ store }>
<App />
</Provider>,
document.getElementById('root')
)
This is the App component rendered:
import React, { Component } from 'react';
import ReactDOM from 'react-dom';
import AllPhotos from '../containers/AllPhotos';
import Navbar from './Navbar';
import '../styles/styles.css';
export default class App extends Component {
constructor (props) {
super (props);
}
render () {
return (
<div className="app">
<Navbar />
<AllPhotos />
</div>
)
}
}
Here is the AllPhotos container it renders:
import { connect } from 'react-redux';
import AllPhotos from '../components/AllPhotos';
import {
loadPhotos
} from '../actions/index.js';
const mapDispatchToProps = (dispatch) => {
return {
loadPhotos: () => {
dispatch(loadPhotos());
}
}
}
const mapStateToProps = (state) => {
return {
photos: state.photos
}
}
export default connect(mapStateToProps, mapDispatchToProps)(AllPhotos);
And finally, this is the AllPhotos component:
import React, { Component } from 'react';
import _ from 'lodash';
export default class AllPhotos extends Component {
constructor(props) {
super(props);
}
componentWillMount () {
this.props.loadPhotos();
}
render () {
return (
<div>
<h1>Test</h1>
{ this._renderImages() }
</div>
)
}
_renderImages () {
_.map(this.props.photos, (image) => {
return (
<img
src={ 'data:image/png;base64,' + image }
width={ 480 }
/>
)
})
}
}
This is what happens when I attempt to log this.props.photos within _renderImages:
The first log occurs before the photos array is populated and loaded into my state.
However, if I were to log the length of this.props.photos in the same area that I log just the array itself, this is what I see:
When I call Array.isArray on this.props.photos in that same line, this is what I get:
I have also attempted to convert this into an array using Array.from, but have not been successful.
Going a bit deeper, I attempted to find the length of the photos array from the action payload in my reducer, and still received 0 as the output. I also tried this within the savePhotos action, and found the same result.
As a result, I believe I may have not written my Promises correctly. Could someone help point me in the right direction?
You can try rewriting listObjects and getSingleObject to return Promises and utilising Promise.all.
Then you can write loadPhotos
export function loadPhotos () {
return function (dispatch) {
listObjects().then((results) =>
Promise.all(results.map((singlePhoto) => getSingleObject(singlePhoto.Key)))
.then((result) => dispatch(savePhotos(result)))
)
}
}

Categories

Resources