I am trying to test a connected component but it does not seem to make the api calls in the componentDidMount function. I need it to make the api calls so i can test the how this component woudl render depending on the values returned from api calls. api calls are made by axios using redux actions. everything stored in redux.
here is my test
it('should dispatch an action on mount', () => {
const component = shallow(
<ProcessingStatus store={store}/>
);
const didMount = jest.spyOn(component, 'componentDidMount');
expect(didMount).toHaveBeenCalledTimes(1);
//console.log(component.html())
expect(store.dispatch).toHaveBeenCalledTimes(3);
});
this is the componentDidMount in my component
componentDidMount() {
const {
matches: { params: { id } },
processingStatus,
securityStatus,
routingStatus,
soxStatus,
remedyStatus,
user: {
priv: {
my_sox_requests,
view_remedy
}
}
} = this.props;
let params = 'id=' + id;
if(processingStatus !== undefined){
processingStatus(params)
.catch(thrown => {
console.log(thrown);
});
}
if(securityStatus !== undefined){
securityStatus(params)
.catch(thrown => {
console.log(thrown);
});
}
if(routingStatus !== undefined){
routingStatus(params)
.catch(thrown => {
console.log(thrown);
});
}
if(my_sox_requests && my_sox_requests === 'on' && soxStatus !== undefined){
soxStatus(params)
.catch(thrown => {
console.log(thrown);
});
}
if(view_remedy && view_remedy === 'on' && remedyStatus !== undefined){
remedyStatus(params)
.catch(thrown => {
console.log(thrown);
});
}
}
Error i get is
FAIL tests/jest/components/common/ProcessingStatus/index.test.js
<ProcessingStatus />
✓ should render with given state from Redux store (90ms)
✕ should dispatch an action on mount (7ms)
● <ProcessingStatus /> › should dispatch an action on mount
Cannot spy the componentDidMount property because it is not a function; undefined given instead
85 | <ProcessingStatus store={store}/>
86 | );
> 87 | const didMount = jest.spyOn(component, 'componentDidMount');
| ^
88 | expect(didMount).toHaveBeenCalledTimes(1);
89 |
90 | //console.log(component.html())
at ModuleMockerClass.spyOn (node_modules/jest-mock/build/index.js:841:15)
at Object.<anonymous> (tests/jest/components/common/ProcessingStatus/index.test.js:87:31)
I tried with const didMount = jest.spyOn(ProcessingStatus.prototype, 'componentDidMount'); and error i get is
● <ProcessingStatus /> › should dispatch an action on mount
expect(jest.fn()).toHaveBeenCalledTimes(expected)
Expected number of calls: 1
Received number of calls: 0
85 | );
86 | const didMount = jest.spyOn(ProcessingStatus.prototype, 'componentDidMount');
> 87 | expect(didMount).toHaveBeenCalledTimes(1);
| ^
88 |
89 | //console.log(component.html())
90 | expect(store.dispatch).toHaveBeenCalledTimes(3);
I managed to test didmount being called but not sure how to check if the api calls have been made.
it('should run componentDidMount', () => {
spy = jest.spyOn(ProcessingStatus.prototype, 'componentDidMount');
component = mount(
<ProcessingStatus store={store}/>
);
expect(spy).toHaveBeenCalledTimes(1);
});
I've been searching a similar question here and found out that you have some order errors:
you should set the spyOn componentDidMount before the shallow(Component)
you should ask for Component.prototype.componentDidMount was called after shallow component
jest.spyOn(Component.prototype, 'componentDidMount')
shallow(<Component/>)
expect(Component.prototype.componentDidMount).toHaveBeenCalled();
if the component expect to receive props functions that are going to be called inside the componentDidMount, you should add them when shallow Component like
const mockExpectedFunction= jest.fn()
shallow(<Component expectedFunction={mockExpectedFunction} />
Related
I'm trying to write a custom hook that uses useQuery from react-query. The custom hook takes in the id of an employee and fetches some data and returns it to the consuming component. I want to be able to dispatch a redux action to show a loading indicator or show an error message if it fails. Here is my custom hook.
export default function useEmployee(id) {
const initial = {
name: '',
address: '',
}
const query = useQuery(['fetchEmployee', id], () => getEmployee(id), {
initialData: initial,
onSettled: () => dispatch(clearWaiting()),
onError: (err) => dispatch(showError(err)),
})
if (query.isFetching || query.isLoading) {
dispatch(setWaiting())
}
return query.data
}
When I refresh the page, I get this error in the browser's console and I'm not sure how to fix this error?
Warning: Cannot update a component (`WaitIndicator`) while rendering a different component (`About`).
To locate the bad setState() call inside `About`, follow the stack trace as described in
The issue is likely with dispatching the setWaiting action outside any component lifecycle, i.e. useEffect. Move the dispatch logic into a useEffect hook with appropriate dependency.
Example:
export default function useEmployee(id) {
const initial = {
name: '',
address: '',
};
const { data, isFetching, isLoading } = useQuery(['fetchEmployee', id], () => getEmployee(id), {
initialData: initial,
onSettled: () => dispatch(clearWaiting()),
onError: (err) => dispatch(showError(err)),
});
useEffect(() => {
if (isFetching || isLoading) {
dispatch(setWaiting());
}
}, [isFetching, isLoading]);
return data;
}
We have implemented bot framework-webchat to create a bot. Currently, we handle the minimize and maximize with the event passed in component (code shown below) but the challenge occurs when I minimize and then maximize the chatbot I am seeing 'Unable to connect' message and then it flashes away and if after an hour-long later if we minimize and maximize I am getting 'Network interruption occurred, Reconnecting...' How do I keep webchat potentially automatically reconnect when I minimize and maximize Chabot.
MaximizeChatWndow () {
if (this.state.token === null &&
this.state.productService === null) {
return
}
this.setState({
directLine: this.createDirectLine()
}, () => {
this.setState({
minimized: false,
newMessage: false,
userId: 'User_' + Math.random
})
})
this.checkExpandFlag = true
}
The component:
render () {
const {
state: { minimized, store }
} = this
return (
<Row>
<Col md={12}>
<div>
{minimized ? (
<ChatDon
handleMaximizeButtonClick={this.handleMaximizeButtonClick}
/>
) : (
<ChatWin
handleMinimizeButtonClick={this.handleMinimizeButtonClick}
directLine={this.state.directLine}
userId={this.state.userId}
store={store}
/>
)}
</div>
</Col>
</Row>
)
}
It looks like you creating your directLine object in "MaximizeChatWndow()" which I think is the problem. In "MaximizeChatWndow()", you should be fetching your token and passing that to your web chat component. It is in the web chat component that you should use the token to call createDirectLine().
It appears that there have been various updates to the 06.recomposing-us/a.minimizable-web-chat sample. (The docs also look like they are out of date and no longer match the code). However, if comparing to the available sample code, you will want to do something like the following. Please look at the full code in the above link as I am only including the most relevant parts here.
When I tested, I had no issues with the conversation resetting or the network disconnecting/reconnecting.
MinimizableWebChat.js
import WebChat from './WebChat';
const MinimizableWebChat = () => {
[...]
const [token, setToken] = useState();
const handleFetchToken = useCallback(async () => {
if (!token) {
const res = await fetch('http://localhost:3500/directline/token', { method: 'POST' });
const { token } = await res.json();
setToken(token);
}
}, [setToken, token]);
[...]
return (
[...]
<WebChat
className="react-web-chat"
onFetchToken={handleFetchToken}
store={store}
styleSet={styleSet}
token={token}
/>
)
WebChat.js
const WebChat = ({ className, onFetchToken, store, token }) => {
const directLine = useMemo(() => createDirectLine({ token }), [token]);
[...]
useEffect(() => {
onFetchToken();
}, [onFetchToken]);
return token ? (
<ReactWebChat ...
);
};
Hope of help!
I have looked at the following tutorials https://hackernoon.com/unit-testing-redux-connected-components-692fa3c4441c https://airbnb.io/enzyme/docs/api/shallow.html and tried to create a shallow rendered test of a component but i have actions being triggered on render which collect data and help render the component. how can i mock this?
tests/jest/containers/homecontent.js
import configureStore from 'redux-mock-store'
import { shallow } from 'enzyme';
import { HomeContent } from '../../../app/containers/home';
const passMetaBack = meta => {
this.setState({
title: 'test',
description: 'test'
});
};
// create any initial state needed
const initialState = {};
// here it is possible to pass in any middleware if needed into //configureStore
const mockStore = configureStore();
describe('Login Component', () => {
let wrapper;
let store;
beforeEach(() => {
// our mock login function to replace the one provided by mapDispatchToProps
const mockLoginfn = jest.fn();
//creates the store with any initial state or middleware needed
store = mockStore(initialState)
wrapper = shallow(<HomeContent isGuest={false} isReady={true} priv={{}} passMetaBack={passMetaBack} fetchContents={mockLoginfn} />)
});
it('+++ render the DUMB component', () => {
expect(wrapper.length).toEqual(1)
});
});
The error i get is
FAIL tests/jest/containers/homecontent.test.js
Login Component
✕ +++ render the DUMB component (25ms)
● Login Component › +++ render the DUMB component
TypeError: Cannot read property 'then' of undefined
38 | if(this.props.isReady && this.props.priv != undefined){
39 | let self = this;
> 40 | this.props.fetchContents()
41 | .then(response => {
42 | let data = response.payload.data;
43 | if (data.header.error) {
at HomeContent.initData (app/containers/home.js:40:7)
at HomeContent.render (app/containers/home.js:71:12)
at ReactShallowRenderer._mountClassComponent (node_modules/react-test-renderer/cjs/react-test-renderer-shallow.development.js:195:37)
at ReactShallowRenderer.render (node_modules/react-test-renderer/cjs/react-test-renderer-shallow.development.js:143:14)
at node_modules/enzyme-adapter-react-16/build/ReactSixteenAdapter.js:287:35
at withSetStateAllowed (node_modules/enzyme-adapter-utils/build/Utils.js:103:16)
at Object.render (node_modules/enzyme-adapter-react-16/build/ReactSixteenAdapter.js:286:68)
at new ShallowWrapper (node_modules/enzyme/build/ShallowWrapper.js:119:22)
at shallow (node_modules/enzyme/build/shallow.js:19:10)
at Object.<anonymous> (tests/jest/containers/homecontent.test.js:24:19)
● Login Component › +++ render the DUMB component
TypeError: Cannot read property 'length' of undefined
26 |
27 | it('+++ render the DUMB component', () => {
> 28 | expect(wrapper.length).toEqual(1)
29 | });
30 | });
31 |
at Object.<anonymous> (tests/jest/containers/homecontent.test.js:28:24)
Test Suites: 1 failed, 1 total
Tests: 1 failed, 1 total
Snapshots: 0 total
Time: 2.218s
Ran all test suites matching /tests\/jest\/containers\/homecontent.test.js/i.
this.props.fetchContents() comes in from an action on the component
mockLoginfn is used as this.props.fetchContents in the component. fetchContents is a function that returns a promise, whereas mockLoginfn is a jest mock function that doesn't return anything.
So, you need to provide a mock implementation for the mockLoginfn so it behaves like a promise. For example (using the code snippet above):
const mockLoginfn = jest.fn();
mockLoginfn.mockImplementation(() => Promise.resolve({
payload: {
data: {
header: {
error: 'some error'
}
}
}
}));
I was wondering if there's an elegant way to trigger the refetch of a query in react-apollo when a subscription receives new data (The data is not important here and will be the same as previous one). I just use subscription here as a notification trigger that tells Query to refetch.
I tried both using Subscription component and subscribeToMore to call "refetch" method in Query's child component but both methods cause infinite re-fetches.
NOTE: I'm using react-apollo v2.1.3 and apollo-client v2.3.5
here's the simplified version of code
<Query
query={GET_QUERY}
variables={{ blah: 'test' }}
>
{({ data, refetch }) => (
<CustomComponent data={data} />
//put subscription here? It'll cause infinite re-rendering/refetch loop
)}
<Query>
Finally I figured it out myself with the inspiration from Pedro's answer.
Thoughts: the problem I'm facing is that I want to call Query's refetch method in Subscription, however, both Query and Subscription components can only be accessed in render method. That is the root cause of infinite refetch/re-rendering. To solve the problem, we need to move the subscription logic out of render method and put it somewhere in a lifecycle method (i.e. componentDidMount) where it won't be called again after a refetch is triggered. Then I decided to use graphql hoc instead of Query component so that I can inject props like refetch, subscribeToMore at the top level of my component, which makes them accessible from any life cycle methods.
Code sample (simplified version):
class CustomComponent extends React.Component {
componentDidMount() {
const { data: { refetch, subscribeToMore }} = this.props;
this.unsubscribe = subscribeToMore({
document: <SUBSCRIBE_GRAPHQL>,
variables: { test: 'blah' },
updateQuery: (prev) => {
refetch();
return prev;
},
});
}
componentWillUnmount() {
this.unsubscribe();
}
render() {
const { data: queryResults, loading, error } } = this.props;
if (loading || error) return null;
return <WhatEverYouWant with={queryResults} />
}
}
export default graphql(GET_QUERY)(CustomComponent);
It's possible if you use componentDidMount and componentDidUpdate in the component rendered by the Subscription render props function.
The example uses recompose higher order components to avoid too much boilerplating. Would look something like:
/*
* Component rendered when there's data from subscription
*/
export const SubscriptionHandler = compose(
// This would be the query you want to refetch
graphql(QUERY_GQL, {
name: 'queryName'
}),
lifecycle({
refetchQuery() {
// condition to refetch based on subscription data received
if (this.props.data) {
this.props.queryName.refetch()
}
},
componentDidMount() {
this.refetchQuery();
},
componentDidUpdate() {
this.refetchQuery();
}
})
)(UIComponent);
/*
* Component that creates the subscription operation
*/
const Subscriber = ({ username }) => {
return (
<Subscription
subscription={SUBSCRIPTION_GQL}
variables={{ ...variables }}
>
{({ data, loading, error }) => {
if (loading || error) {
return null;
}
return <SubscriptionHandler data={data} />;
}}
</Subscription>
);
});
Another way of accomplishing this while totally separating Query and Subscription components, avoiding loops on re-rendering is using Apollo Automatic Cache updates:
+------------------------------------------+
| |
+----------->| Apollo Store |
| | |
| +------------------------------+-----------+
+ |
client.query |
^ +-----------------+ +---------v-----------+
| | | | |
| | Subscription | | Query |
| | | | |
| | | | +-----------------+ |
| | renderNothing | | | | |
+------------+ | | | Component | |
| | | | | |
| | | +-----------------+ |
| | | |
+-----------------+ +---------------------+
const Component =() => (
<div>
<Subscriber />
<QueryComponent />
</div>
)
/*
* Component that only renders Query data
* updated automatically on query cache updates thanks to
* apollo automatic cache updates
*/
const QueryComponent = graphql(QUERY_GQL, {
name: 'queryName'
})(() => {
return (
<JSX />
);
});
/*
* Component that creates the subscription operation
*/
const Subscriber = ({ username }) => {
return (
<Subscription
subscription={SUBSCRIPTION_GQL}
variables={{ ...variables }}
>
{({ data, loading, error }) => {
if (loading || error) {
return null;
}
return <SubscriptionHandler data={data} />;
}}
</Subscription>
);
});
/*
* Component rendered when there's data from subscription
*/
const SubscriptionHandler = compose(
// This would be the query you want to refetch
lifecycle({
refetchQuery() {
// condition to refetch based on subscription data received
if (this.props.data) {
var variables = {
...this.props.data // if you need subscription data for the variables
};
// Fetch the query, will automatically update the cache
// and cause QueryComponent re-render
this.client.query(QUERY_GQL, {
variables: {
...variables
}
});
}
},
componentDidMount() {
this.refetchQuery();
},
componentDidUpdate() {
this.refetchQuery();
}
}),
renderNothing
)();
/*
* Component that creates the subscription operation
*/
const Subscriber = ({ username }) => {
return (
<Subscription
subscription={SUBSCRIPTION_GQL}
variables={{ ...variables }}
>
{({ data, loading, error }) => {
if (loading || error) {
return null;
}
return <SubscriptionHandler data={data} />;
}}
</Subscription>
);
});
Note:
compose and lifecycle are recompose methods that enable easier a cleaner higher order composition.
I am fetching data from a remote API in componentDidMount:
componentDidMount() {
this.props.fetchRemoteData('photos')
}
And then the received data is passed to my component props in mapStateToProps, using a selector to filter a specific object from the received array:
const mapStateToProps = (state, { params }) => {
const photoId = parseInt(params.photoId)
return {
singlePhoto: getSinglePhoto(state.filteredList.photos.jsonArray, photoId),
isFetching: state.filteredList.photos.isFetching
}
}
The content renders, but there is a split second before that, where it seems to be trying to the render the content before the data is successfully retrieved, which brings up the following error in the console:
Uncaught TypeError: Cannot read property 'charAt' of undefined
undefined is here referring to this.props.singlePhoto. But when singlePhoto receives the data payload the content renders.
Here is my container component:
class PhotoSingle extends Component {
componentDidMount() {
this.props.fetchRemoteData('photos')
}
render() {
const {singlePhoto, isFetching} = this.props
const photoTitle = capitalizeFirstLetter(singlePhoto.title)
return (
<div>
<PhotoSingleImg singlePhoto={singlePhoto} photoTitle={photoTitle} isFetching={isFetching}/>
</div>
)
}
}
const mapStateToProps = (state, { params }) => {
const photoId = parseInt(params.photoId)
return {
singlePhoto: getSinglePhoto(state.filteredList.photos.jsonArray, photoId),
isFetching: state.filteredList.photos.isFetching
}
}
import * as actions from '../actions/actionCreators'
PhotoSingle = connect(mapStateToProps, actions)(PhotoSingle)
export default PhotoSingle;
And my presentational component:
const PhotoSingleImg = ({ singlePhoto, photoTitle, isFetching }) => {
if (isFetching) {
return <h4>Fetching data...</h4>
}
return (
<div>
<h1>Single Photo</h1>
<h3>Title</h3>
<hr />
<img className='single-photo' src={singlePhoto.url} />
<p>Album ID: {singlePhoto.albumId} | Photo ID: {singlePhoto.id}</p>
</div>
)
}
export default PhotoSingleImg;
I'm unsure how to make it so the content will only attempt to render after I the API response has been received.
Any help appreciated.
Have you defined initial state in redux store?
You can try this way:
return singlePhoto ?
(<div>
<h1>Single Photo</h1>
<h3>Title</h3>
<hr />
<img className='single-photo' src={singlePhoto.url} />
<p>Album ID: {singlePhoto.albumId} | Photo ID: {singlePhoto.id}</p>
</div>) : null