How to test react component mocking fetch api? - ts-jest

I'm trying to test a react component where a fetch call occurs. The component:
class SearchResults extends React.Component<{ tag: string; setSpinner: (value: boolean) => void }> {
state: searchResultsState = {
results: [],
tag: '',
cardFull: null,
};
constructor(props: { tag: string; setSpinner: (value: boolean) => void }) {
super(props);
this.handleCardModal = this.handleCardModal.bind(this);
}
componentDidMount() {
getResponse(
this.props.tag,
(res) => {
this.setState({ results: res, tag: this.props.tag });
},
this.props.setSpinner
);
}
componentDidUpdate(prevProps: { tag: string }) {
if (this.props.tag !== prevProps.tag) {
getResponse(
this.props.tag,
(res) => this.setState({ results: res, tag: this.props.tag }),
this.props.setSpinner
);
}
}
handleCardModal() {
this.setState({ cardFull: null });
}
render() {
return (
<Fragment>
{this.state.cardFull && (
<CardFull data={this.state.cardFull} handleClick={this.handleCardModal} />
)}
{this.state.cardFull && <div className="blackout" onClick={this.handleCardModal} />}
<div className="search-results">
{this.state.results.length === 0 && this.state.tag !== '' && <div>Sorry, no matched</div>}
{this.state.results.map((result) => (
<CardUI
key={result.id}
className="card"
variant="outlined"
sx={{ minWidth: 275 }}
onClick={() => {
const currentCard = this.state.results.filter((res) => res.id === result.id)[0];
this.setState({ ...this.state, cardFull: currentCard });
}}
>
<CardMedia component="img" height="194" image={getUrl(result)} alt={result.title} />
<CardContent>
<p>{result.title}</p>
</CardContent>
</CardUI>
))}
</div>
</Fragment>
);
}
}
First I tried to use jest-fetch-mock.
import '#testing-library/jest-dom';
import { render } from '#testing-library/react';
import renderer from 'react-test-renderer';
import SearchResults from '../../src/components/SearchResults/SearchResults';
import sampleSearchResults from '../__fixtures__/sampleSearchResults';
import fetch from 'jest-fetch-mock';
fetch.enableMocks();
beforeEach(() => {
fetch.resetMocks();
});
const setSpinner = jest.fn();
describe('Search Results component', () => {
fetch.mockResponseOnce(JSON.stringify({ photos: sampleSearchResults }));
test('Search Results matches snapshot', () => {
const searchResults = renderer
.create(<SearchResults tag={''} setSpinner={setSpinner} />)
.toJSON();
expect(searchResults).toMatchSnapshot();
});
test('search results renders correctly', () => {
render(<SearchResults setSpinner={setSpinner} tag={'dove'} />);
});
});
But it gives the error during tests:
console.error
FetchError {
message: 'invalid json response body at reason: Unexpected end of JSON input',
type: 'invalid-json'
}
So, I've decided to mock fetch manually
import React from 'react';
import '#testing-library/jest-dom';
import { render, screen } from '#testing-library/react';
import renderer from 'react-test-renderer';
import SearchResults from '../../src/components/SearchResults/SearchResults';
import sampleSearchResults from '../__fixtures__/sampleSearchResults';
const setSpinner = jest.fn();
global.fetch = jest.fn(() =>
Promise.resolve({
json: () => Promise.resolve({ photos: sampleSearchResults }),
})
) as jest.Mock;
describe('Search Results component', () => {
test('Search Results matches snapshot', () => {
const searchResults = renderer
.create(<SearchResults tag={''} setSpinner={setSpinner} />)
.toJSON();
expect(searchResults).toMatchSnapshot();
});
test('search results renders correctly', () => {
render(<SearchResults setSpinner={setSpinner} tag={'dove'} />);
const title = screen.getByText(/Eurasian Collared Dove (Streptopelia decaocto)/i);
expect(title).toBeInTheDocument(); //mistake
});
});
Now fetch mock works correct, but it renders only one div - search results and doesn't render card. How can I test my component? Thank you.

Related

Testing useSubscription apollo hooks with react

Testing the useSubscription hook I'm finding a bit difficult, since the method is omitted/not documented on the Apollo docs (at time of writing). Presumably, it should be mocked using the <MockedProvider /> from #apollo/react-testing, much like the mutations are in the examples given in that link.
Testing the loading state for a subscription I have working:
Component:
const GET_RUNNING_DATA_SUBSCRIPTION = gql`
subscription OnLastPowerUpdate {
onLastPowerUpdate {
result1,
result2,
}
}
`;
const Dashboard: React.FC<RouteComponentProps & Props> = props => {
const userHasProduct = !!props.user.serialNumber;
const [startGetRunningData] = useMutation(START_GET_RUNNING_DATA);
const [stopGetRunningData] = useMutation(STOP_GET_RUNNING_DATA);
useEffect(() => {
startGetRunningData({
variables: { serialNumber: props.user.serialNumber },
});
return () => {
stopGetRunningData();
};
}, [startGetRunningData, stopGetRunningData, props]);
const SubscriptionData = (): any => {
const { data, loading } = useSubscription(GET_RUNNING_DATA_SUBSCRIPTION);
if (loading) {
return <Heading>Data loading...</Heading>;
}
const metrics = [];
if (data) {
console.log('DATA NEVER CALLED IN TEST!');
}
return metrics;
};
if (!userHasProduct) {
return <Redirect to="/enter-serial" />;
}
return (
<>
<Header />
<PageContainer size="midi">
<Panel>
<SubscriptionData />
</Panel>
</PageContainer>
</>
);
};
And a successful test of the loading state for the subscription:
import React from 'react';
import thunk from 'redux-thunk';
import { createMemoryHistory } from 'history';
import { create } from 'react-test-renderer';
import { Router } from 'react-router-dom';
import wait from 'waait';
import { MockedProvider } from '#apollo/react-testing';
import { Provider } from 'react-redux';
import configureMockStore from 'redux-mock-store';
import Dashboard from './Dashboard';
import {
START_GET_RUNNING_DATA,
STOP_GET_RUNNING_DATA,
GET_RUNNING_DATA_SUBSCRIPTION,
} from './queries';
const mockStore = configureMockStore([thunk]);
const serialNumber = 'AL3286wefnnsf';
describe('Dashboard page', () => {
let store: any;
const fakeHistory = createMemoryHistory();
const mocks = [
{
request: {
query: START_GET_RUNNING_DATA,
variables: {
serialNumber,
},
},
result: {
data: {
startFetchingRunningData: {
startedFetch: true,
},
},
},
},
{
request: {
query: GET_RUNNING_DATA_SUBSCRIPTION,
},
result: {
data: {
onLastPowerUpdate: {
result1: 'string',
result2: 'string'
},
},
},
},
{
request: {
query: STOP_GET_RUNNING_DATA,
},
result: {
data: {
startFetchingRunningData: {
startedFetch: false,
},
},
},
},
];
afterEach(() => {
jest.resetAllMocks();
});
describe('when initialising', () => {
beforeEach(() => {
store = mockStore({
user: {
serialNumber,
token: 'some.token.yeah',
hydrated: true,
},
});
store.dispatch = jest.fn();
});
it('should show a loading state', async () => {
const component = create(
<Provider store={store}>
<MockedProvider mocks={mocks} addTypename={false}>
<Router history={fakeHistory}>
<Dashboard />
</Router>
</MockedProvider>
</Provider>,
);
expect(component.root.findAllByType(Heading)[0].props.children).toBe(
'Data loading...',
);
});
});
});
Adding another test to wait until the data has been resolved from the mocks passed in, as per the instructions on the last example from the docs for testing useMutation, you have to wait for it.
Broken test:
it('should run the data', async () => {
const component = create(
<Provider store={store}>
<MockedProvider mocks={mocks} addTypename={false}>
<Router history={fakeHistory}>
<Dashboard />
</Router>
</MockedProvider>
</Provider>,
);
await wait(0);
});
Error the broken test throws:
No more mocked responses for the query: subscription OnLastPowerUpdate {
Dependencies:
"#apollo/react-common": "^3.1.3",
"#apollo/react-hooks": "^3.1.3",
"#apollo/react-testing": "^3.1.3",
Things I've tried already:
react-test-renderer / enzyme / #testing-library/react
awaiting next tick
initialising the client in the test differently
Github repo with example:
https://github.com/harrylincoln/apollo-subs-testing-issue
Anyone out there able to help?
The problem I can see here is that you're declaring the SubscriptionData component inside the Dashboard component so the next time the Dashboard component is re-rendered, the SubscriptionData component will be re-created and you'll see the error message:
No more mocked responses for the query: subscription OnLastPowerUpdate
I suggest that you take the SubscriptionData component out of the Dashboard component so it will be created only once
const SubscriptionData = (): any => {
const { data, loading } = useSubscription(GET_RUNNING_DATA_SUBSCRIPTION);
if (loading) {
return <Heading>Data loading...</Heading>;
}
const metrics = [];
if (data) {
console.log('DATA NEVER CALLED IN TEST!');
}
return metrics;
};
const Dashboard: React.FC<RouteComponentProps & Props> = props => {
const userHasProduct = !!props.user.serialNumber;
const [startGetRunningData] = useMutation(START_GET_RUNNING_DATA);
const [stopGetRunningData] = useMutation(STOP_GET_RUNNING_DATA);
useEffect(() => {
startGetRunningData({
variables: { serialNumber: props.user.serialNumber },
});
return () => {
stopGetRunningData();
};
}, [startGetRunningData, stopGetRunningData, props]);
if (!userHasProduct) {
return <Redirect to="/enter-serial" />;
}
return (
<>
<Header />
<PageContainer size="midi">
<Panel>
<SubscriptionData />
</Panel>
</PageContainer>
</>
);
};
And for the tests you can try something like this:
let component;
it('should show a loading state', async () => {
component = create(
<Provider store={store}>
<MockedProvider mocks={mocks} addTypename={false}>
<Router history={fakeHistory}>
<Dashboard />
</Router>
</MockedProvider>
</Provider>,
);
expect(component.root.findAllByType(Heading)[0].props.children).toBe(
'Data loading...',
);
await wait(0);
});
it('should run the data', async () => {
expect(
// another test here
component.root...
).toBe();
});

Problem with calling action method through dispatch with webext-redux in browser extension

I'm trying to call apiAction in constructor method through the dispatch redux method in ReactJS Component:
import React, { Component } from 'react';
import { connect } from 'react-redux';
import './styles.scss'
import { fetchData, testSet } from '../../../../../event/src/cg-store/actions';
class AppDetails extends Component {
constructor(props) {
super(props);
this.state ={
testowaZmienna: ''
}
this.props.fetchData('5576900');
}
componentDidMount() {
document.addEventListener('click', () => {
this.props.addCount()
});
this.props.testSet()
this.props.fetchData('5576900');
console.log('dhsadhnaskjndaslndsadl-----------------------------------------')
}
render() {
const { error, test, count, testSetData, data } = this.props;
return (
<div>
TEST--------------------------
Count: {count}
Error: {error}
Test: {test}
TestSet: {testSetData}
Fetch: {data[0]}
</div>
);
}
}
const mapStateToProps = (state) => {
return {
count: state.count,
test: state.cg.test,
data: state.cg.data,
error: state.cg.error,
testSetData: state.cg.testSet,
};
};
const mapDispatchToProps = (dispatch) => {
return {
fetchData: (offerId) => dispatch(fetchData(offerId)),
addCount: () => dispatch({
type: 'ADD_COUNT'
}),
testSet: () => dispatch(testSet()),
};
};
export default connect(mapStateToProps, mapDispatchToProps)(AppDetails);
As you can see there is addCount, testSet and fetchData methods. addCount and testSet works but problem is with fetchData:
This is apiAction method:
const fetchProductsPending = () => {
return {
type: actionTypes.FETCH_DATA_PENDING
};
};
const fetchProductsSuccess = fetchedData => {
return {
type: actionTypes.FETCH_DATA_SUCCESS,
data: fetchedData
};
};
const fetchProductsError = errorMessage => {
return {
type: actionTypes.FETCH_DATA_ERROR,
error: errorMessage
};
};
export const testSet = () => {
return {
type: actionTypes.TEST_SET
};
};
export const fetchData = (offerId) => (dispatch) => {
console.log('Im inside fetch before set pending'); // It does not want to go here
dispatch(fetchProductsPending());
axios
.get(config.api.host + offerId, {
headers: {
"Content-Type": "application/json",
}
})
.then(response => {
return response.data;
})
.then(response => {
dispatch(fetchProductsSuccess(response.data));
console.log("Fetch data success: ----------------------");
console.log(response.data);
})
.catch(error => {
dispatch(fetchProductsError(error.statusText));
console.log("Fetch data success: ----------------------");
console.log(error);
});
};
So as you can see testSet works fine but fetchData does not want to work.
What I'm doing wrong?

BeforeUpload do not trigger upload on promise resolved

Using React, and antd
I have the following code in my component:
<Upload
action={HttpService.getBaseUrl(`post_import_csv`, HttpService.AuditcoreAPIBasePath)}
headers={{"Authorization": `Bearer ${AuthHelper.getAuthKey()}`}}
showUploadList={false}
multiple={false}
beforeUpload={(file: RcFile): PromiseLike<any> => {
this.setCSV(file);
return new Promise((resolve) => {
this.state.requestUpload.pipe(take(1)).subscribe(() => {
resolve(file);
console.log('resolved')
});
})
}}></Upload>
Basically I want my beforeUpload to wait for the user to click on a button before uploading the file. I did so by returning a Promise and waiting for a rxjs Suject that is triggered on button click to resolve the promise. Pretty much following the doc
Here is the button code :
<Button
onClick={(e): void => {
this.state.requestUpload.next(true);
}}
>
Upload
</Button>
It works nice, but the file is never uploaded, I do see my log resolved but there is no trace of network call in my console.
I fixed using this approach which is cleaner :
https://codesandbox.io/s/xvkj90rwkz
Basically, having a custom function that handle upload. It doesn't explain why my tricky solution was not working, but I got it working.
import React from 'react';
import ReactDOM from 'react-dom';
import 'antd/dist/antd.css';
import './index.css';
import { Upload, Button, Icon, message } from 'antd';
import reqwest from 'reqwest';
class Demo extends React.Component {
state = {
fileList: [],
uploading: false,
};
handleUpload = () => {
const { fileList } = this.state;
const formData = new FormData();
fileList.forEach(file => {
formData.append('files[]', file);
});
this.setState({
uploading: true,
});
// You can use any AJAX library you like
reqwest({
url: 'https://www.mocky.io/v2/5cc8019d300000980a055e76',
method: 'post',
processData: false,
data: formData,
success: () => {
this.setState({
fileList: [],
uploading: false,
});
message.success('upload successfully.');
},
error: () => {
this.setState({
uploading: false,
});
message.error('upload failed.');
},
});
};
render() {
const { uploading, fileList } = this.state;
const props = {
onRemove: file => {
this.setState(state => {
const index = state.fileList.indexOf(file);
const newFileList = state.fileList.slice();
newFileList.splice(index, 1);
return {
fileList: newFileList,
};
});
},
beforeUpload: file => {
this.setState(state => ({
fileList: [...state.fileList, file],
}));
return false;
},
fileList,
};
return (
<div>
<Upload {...props}>
<Button>
<Icon type="upload" /> Select File
</Button>
</Upload>
<Button
type="primary"
onClick={this.handleUpload}
disabled={fileList.length === 0}
loading={uploading}
style={{ marginTop: 16 }}
>
{uploading ? 'Uploading' : 'Start Upload'}
</Button>
</div>
);
}
}
ReactDOM.render(<Demo />, document.getElementById('container'));

Trouble rendering react components that import google-maps-react on Heroku only

I have a react-in-rails application that utilizes the google-maps-react api. The app works fine locally but when deployed to heroku, any component that imports google-maps-react does not render. Since this is generally the landing page for most users, the app is not accessible at all.
When all the components that import or render google-maps-react are removed, the app deploys correctly.
import React from "react"
import MapContainer from "./MapContainer"
import StoreList from './StoreList'
class FindBar extends React.Component {
render () {
const {stores, openTab, success} = this.props
return (
<div className="findbar" >
<div className="mapcomponent">
<MapContainer
stores={stores}
openTab={openTab}
success={success}
/>
</div>
<br/>
<StoreList
stores={stores}
openTab={openTab}
/>
{this.props.success &&
<Redirect to="/user_home/opentabs" />
}
</div>
);
}
}
export default FindBar
import React, { Component } from 'react';
import { Button, Card } from 'reactstrap';
import { Map, GoogleApiWrapper, Marker, InfoWindow } from 'google-maps-react';
import UserHome from './UserHome.js'
import StoreMarkerWindow from './StoreMarkerWindow.js'
import InfoWindowEx from './InfoWindowEx.js'
const mapStyles = {
width: '100%',
height: '100vh',
};
class MapContainer extends Component {
constructor(props) {
super(props)
this.state = {
showingInfoWindow: false,
activeMarker: {},
selectedPlace: {},
address: [],
location: {},
displayMarkers: [],
success: false,
}
}
componentDidMount = () => {
this.fetchMarkers()
}
componentDidUpdate = (prevProps) => {
if (prevProps.stores === this.props.stores){
return true
}
this.fetchMarkers()
}
openTab = () => {
console.log(this.state.selectedPlace.storeId)
// this.props.openTab(this.state.selectedPlace.storeId)
}
onClick = (props, marker, e) => {
this.setState({
selectedPlace: props,
activeMarker: marker,
showingInfoWindow: true
})
}
onClose = props => {
if (this.state.showingInfoWindow) {
this.setState({
showingInfoWindow: false,
activeMarker: null
});
}
}
fetchMarkers = () => {
const newMarkers = []
this.props.stores.map((store, index) => {
const location = `${store.address1}, ${store.city}, ${store.state}, ${store.zip}`
this.geocodeAddress(location)
.then((geoco)=>{
newMarkers.push({lat: geoco.lat,
lng: geoco.lng,
storeId: store.id,
name: store.establishmentname,
location: location,
info: store.additionalinfo,
})
this.setState({ displayMarkers:newMarkers})
})
})
}
// create a function that maps stores.address, stores.city, stores.state, stores.zipcode
// and returns it to the geocodeAddress and then geocodeAddress returns it to
// the displayMarkers
geocodeAddress = (address) => {
const geocoder = new google.maps.Geocoder()
return new Promise((resolve, reject) => {
geocoder.geocode({'address': address}, function(results, status) {
if (status === google.maps.GeocoderStatus.OK) {
resolve(results[0].geometry.location.toJSON())
} else {
reject()
}
})
})
}
render() {
const{
activeMarker,
showingInfoWindow,
selectedPlace,
onMapOver,
}=this.props
return (
<div className="mapContainer" style={mapStyles}>
<Map
google={this.props.google}
onMouseover={this.onMapOver}
zoom={14}
style={mapStyles}
initialCenter={{
lat: 32.7091,
lng: -117.1580
}}
>
{this.state.displayMarkers.map((coordinates, index) => {
const{storeId, lat, lng, name, location, info} = coordinates
return (
<Marker onClick={this.onClick}
key={index}
id={storeId}
name={name}
position = {{lat, lng}}
location={location}
info= {info}
>
</Marker>
)
})}
<InfoWindowEx
marker={this.state.activeMarker}
visible={this.state.showingInfoWindow}
onClose={this.onClose}
>
<div>
<StoreMarkerWindow
name={this.state.selectedPlace.name}
location={this.state.selectedPlace.location}
info={this.state.selectedPlace.info}
id={this.state.selectedPlace.id}
openTab={this.props.openTab}
/>
</div>
</InfoWindowEx>
</Map>
</div>
);
}
}
export default GoogleApiWrapper({
apiKey: 'xxxx'
})(MapContainer);
TypeError: t is not a function
at Object.a (windowOrGlobal.js:18)
at Object.<anonymous> (windowOrGlobal.js:5)
at Object.<anonymous> (windowOrGlobal.js:5)
at n (bootstrap:19)
at Object.<anonymous> (ScriptCache.js:3)
at n (bootstrap:19)
at Object.<anonymous> (GoogleApiComponent.js:5)
at n (bootstrap:19)
at Object.<anonymous> (index.js:5)
at n (bootstrap:19)

Fetch request in React: How do I Map through JSON array of objects, setState() & append?

This API returns a JSON array of objects, but I'm having trouble with setState and appending. Most documentation covers JSON objects with keys. The error I get from my renderItems() func is:
ItemsList.js:76 Uncaught TypeError: Cannot read property 'map' of undefined
in ItemsList.js
import React, { Component } from "react";
import NewSingleItem from './NewSingleItem';
import { findDOMNode } from 'react-dom';
const title_URL = "https://www.healthcare.gov/api/index.json";
class ItemsList extends Component {
constructor(props) {
super(props);
this.state = {
// error: null,
// isLoaded: false,
title: [],
url: [],
descrip: []
};
}
componentDidMount() {
fetch(title_URL)
.then(response => {
return response.json();
})
.then((data) => {
for (let i = 0; i < data.length; i++) {
this.setState({
title: data[i].title,
url: data[i].url,
descrip: data[i].bite,
});
console.log(data[i])
}
})
.catch(error => console.log(error));
}
renderItems() {
return this.state.title.map(item => {
<NewSingleItem key={item.title} item={item.title} />;
});
}
render() {
return <ul>{this.renderItems()}</ul>;
}
}
export default ItemsList;
Above, I'm trying to map through the items, but I'm not quite sure why I cant map through the objects I set in setState(). Note: even if in my setState() I use title: data.title, it doesnt work. Can someone explain where I'm erroring out? Thanks.
in App.js
import React, { Component } from "react";
import { hot } from "react-hot-loader";
import "./App.css";
import ItemsList from './ItemsList';
class App extends Component {
render() {
return (
<div className="App">
<h1> Hello Healthcare </h1>
<ItemsList />
<article className="main"></article>
</div>
);
}
}
export default App;
in NewSingleItem.js
import React, { Component } from "react";
const NewSingleItem = ({item}) => {
<li>
<p>{item.title}</p>
</li>
};
export default NewSingleItem;
The problem is this line:
this.setState({
title: data[i].title,
url: data[i].url,
descrip: data[i].bite,
});
When you state this.state.title to data[i].title, it's no longer an array. You need to ensure it stays an array. You probably don't want to split them up anyway, just keep them all in a self contained array:
this.state = {
// error: null,
// isLoaded: false,
items: [],
};
...
componentDidMount() {
fetch(title_URL)
.then(response => {
return response.json();
})
.then((data) => {
this.setState({
items: data.map(item => ({
title: item.title,
url: item.url,
descrip: item.bite,
})
});
console.log(data[i])
}
})
...
renderItems() {
return this.state.items.map(item => {
<NewSingleItem key={item.title} item={item.title} />;
});
}

Resources