I'm trying to test my smart component using Jest and Enzyme but it has no data to render because it supposed to be fetched trought actions. Error is: getTasks is not a function
Enzyme.configure({ adapter: new Adapter() });
describe('Main', () => {
describe('when loading is true', () => {
it('should render loading div', () => {
const wrapper = mount(<Main.WrappedComponent loading={true} />);
expect(wrapper.html()).toEqual('<div>Loading</div>');
wrapper.unmount();
});
});
});
And this is component I'm trying to test, it fetching data trought actions and then doing some stuff with them, but if there is no data(loading === true) it just renders the <div> with "Loading" text. getTasks() just import the data:
class Main extends React.Component {
constructor(props) {
super();
this.handleData = this.handleData.bind(this);
this.handleHigh = this.handleHigh.bind(this);
}
componentDidMount() {
const { getTasks } = this.props;
getTasks();
}
render() {
const { data, loading } = this.props;
if (!loading) {
this.handleData(data);
return (
{data.map(task => {
if (task.obj_status === 'active')
return (
// Doing some stuff with data here
);
} else {
return <div>Loading</div>;
}
}
}
const mapStateToProps = state => ({
data: state.main.data,
loading: state.main.loading
});
const mapDispatchToProps = dispatch => ({
...bindActionCreators(
{
getTasks: loadTasks,
dispatch
},
dispatch
)
});
export default withRouter(
connect(
mapStateToProps,
mapDispatchToProps
)(Main)
);
You need to pass in getTasks as a function into your props:
const wrapper = mount(<Main.WrappedComponent loading={true} getTasks={() => {}} />);
As when Enzyme mounts it will invoke componentDidMount and will call the undefined prop and blow up
Better solution is mock function:
const getTasksMock = jest.fn();
const wrapper = mount(<Main.WrappedComponent loading={true} getTasks={getTasksMock}/>);
Then you are able check invoking function by for example: toHaveBeenCalled()
https://jestjs.io/docs/en/expect.html#tohavebeencalled
Related
I have a react component that calls an API that returns two different results which the default value is
{init:false}
And based on users actions, it will be true:
{init:true}
Now I want to test these two states in my app.test.tsx, It will work when I skip one of them(each working fine without another one):
import { screen } from '#testing-library/react';
import { render } from 'src/mocks/renderViaAllProviders';
import App from './app';
import * as apis from 'src/api/consul';
import { mockedRawConsul } from 'src/mocks/db/consul';
test("Show init page when 'initialized:false' in consul.", async () => {
render(<App />);
const loading = screen.getByRole('heading', { name: /loading/i });
expect(loading).toBeInTheDocument();
const initTitle = await screen.findByRole('heading', {
name: /init page/i
});
expect(initTitle).toBeInTheDocument();
});
test("Show Login page when 'initialized:true' in consul", async () => {
const initializedConsul = {
...mockedRawConsul,
...{ configs: { initialized: true } }
};
/*eslint-disable */
//#ts-ignore
apis.getConsulPublicConfig = jest.fn(() =>
Promise.resolve(initializedConsul)
);
render(<App />);
const loginButton = await screen.findByRole('button', {
name: /regularLogin/i
});
expect(loginButton).toBeInTheDocument();
});
How can I fix this?
Update
Here is the reprex and the error :
● Show Login page when 'initialized:true' in consul
Unable to find role="textbox"
console.error
TypeError: Cannot read property 'status' of undefined
at onResponseRejected (\src\api\
service\interceptors.ts:18:23)
at processTicksAndRejections (internal/process/task_queues.js:95:5)
at getLicense (\src\api\license .
ts:10:20)
I have tried to simulate the example that you are trying, I am able to mock the API which returns different results and test for the same, but since we want different results when a Component is rendered the API will be called only once(assuming the API is called on mounting) and upon some user actions the Component will be mounted again that's why called render function again not sure whether it is a good practice or not
//App.js
export default function App() {
const [value, setValue] = useState('loading');
const [show, setShow] = useState({init: false})
useEffect(() => {
setTimeout(() => {
setValue('init page')
fetchData().then(data => {
setShow(data)
}).catch((error) => {
console.log(`ERROR`)
})
},0)
},[])
const { init = false} = show
return (
<>
<p>IT S APP</p>
<h1>Value is {value}</h1>
{ init ? <button>regular Login</button> : null}
</>
);
}
//api.js
function fetchData() {
return fetch("https://jsonplaceholder.typicode.com/posts").then((response) =>
Promise.resolve({init: true})
);
}
export { fetchData };
//App.test.js
import App from "./App";
import { fetchData }from './api';
jest.mock('./api')
describe("<App />", () => {
it("check if loading, login button is present",async () => {
fetchData.mockImplementationOnce(() => Promise.resolve({init: false}))
fetchData.mockImplementationOnce(() => Promise.resolve({init: true}))
render(<App />);
const loading = screen.getByRole('heading', { name: /loading/i });
expect(loading).toBeInTheDocument();
const initTitle = await screen.findByRole('heading', {
name: /init page/i
});
expect(initTitle).toBeInTheDocument();
render(<App />);
await waitFor(() => {
expect(screen.queryByRole('button', {
name: /regular Login/i
})).toBeInTheDocument();
})
});
});
I have a react project that is using redux-thunk. I created an action that will hit an endpoint, then set store to data received. Currently, I am using .then but when I call the action in the componentdidmount, the data is not there. The component renders before the data is available. To fix this, I decided to turn my action into an async action and then await in my componentdidmount. The problem is, as soon as I put async in my action, I get this error....
Unhandled Rejection (Error): Actions must be plain objects. Use custom middleware for async actions.
Here is my code
Action
export const getCasesSuccess = async (data) => {
return {
type: GET_ALL_CASES,
data
}
};
export const getAllCases = () => {
return (dispatch) => {
axios.get('https://corona.lmao.ninja/all')
.then(res => {
const cases = res.data
dispatch(getCasesSuccess(cases))
})
.catch(error => {
throw(error)
})
}
}
Component where action is called
import React from "react";
import { connect } from "react-redux";
import { getAllCases } from "../../store/actions/index";
import AllCases from '../../components/allcases/allCases';
class DataContainer extends React.Component {
constructor(props) {
super(props);
this.state = { }
}
componentDidMount = async () => {
await this.props.getAllCases()
}
render() {
return (
<div>
<AllCases allCases={this.props.allCases} />
</div>
);
}
}
const mapStateToProps = (state) => (
{
allCases: state.allCases
}
)
const mapDispatchToProps = dispatch => {
return {
getAllCases: () => dispatch(getAllCases()),
}
}
export default connect(mapStateToProps, mapDispatchToProps)(DataContainer);
Remove the async from componentDidmount and use the async and await in getAllCases method
export const getAllCases = async () => {
return (dispatch) => {
await axios.get('https://corona.lmao.ninja/all')
.then(res => {
const cases = res.data
dispatch(getCasesSuccess(cases))
})
.catch(error => {
throw(error)
})
}
}
As the error messages says, Redux actions must be plain objects. Since you're using thunk middleware, you can dispatch functions. But you're returning a promise. Since the data loading is asynchronous, your component should check if the data exists and if it doesn't, render a loading indicator or something. In your reducer, you can set a default state for allCases to null which the DataContainer component will use when the component mounts.
export const getCasesSuccess = (data) => {
return {
type: GET_ALL_CASES,
data
}
};
import React from "react";
import { connect } from "react-redux";
import { getAllCases } from "../../store/actions/index";
import AllCases from '../../components/allcases/allCases';
class DataContainer extends React.Component {
componentDidMount() {
this.props.getAllCases()
}
render() {
const { allCases } = this.props
if (!allCases) {
return <div>Loading...</div>
}
return (
<div>
<AllCases allCases={this.props.allCases} />
</div>
);
}
}
const mapStateToProps = (state) => ({
allCases: state.allCases
})
const mapDispatchToProps = {
getAllCases,
}
export default connect(mapStateToProps, mapDispatchToProps)(DataContainer);
Need help in getting response from a function written inside reducer function
functional component
import {
getAssets,
} from '../../reducers';
const myFunctionalComponent = (props) => {
const dispatch = useDispatch();
const onLinkClick = () => {
dispatch(getAssets());
}
}
return (
<div>
<mainContent />
</div>
)
}
In my reducer
const reducer = (state = initialState, action) => {
switch (action.type) {
case ASSETS_LIST: {
return {
...state,
successToast: true,
isLoading: false,
data: action.payload,
};
}
}
export const listsDispactcher = () => dispatch => {
dispatch({ type: SHOW_LOADER });
performGet(ENDPOINT URL)
.then(response => {
debugger;
const payload = response.data;
dispatch({
type: ASSETS_LIST,
payload: {
...payload,
data: payload.results,
},
});
dispatch({ type: HIDE_LOADER });
})
.catch(err => {
dispatch({ type: GET_ASSETS_ERROR, payload: err });
);
});
};
when i click the link ,am getting my api called in function in reducer and its getting response in newtwork tab in developer console , but how to get the response (that is successToast,data,isLoading )in my functional component and to pass the same to child components ?
I advice you to change the structure of your project. Place all your network calls in a file and call them from your component. It is better for readability and understandability
import {
getAssets,
} from './actions';
const myFunctionalComponent = (props) => {
const dispatch = useDispatch();
const onLinkClick = async () => {
const data = await dispatch(getAssets());
}
}
return (
<div>
<mainContent />
</div>
)
}
In ./actions.js
const getAssets =()=>async dispatch =>{
const res = await axios.get();
dispatch(setYourReduxState(res.data));
return res.data;
}
Now your component will get the data of network call. and Your redux state also will get update
For functional components, to access state stored centrally in redux you need to use useSelector hook from react-redux
import React from 'react'
import { useSelector } from 'react-redux'
export const CounterComponent = () => {
const counter = useSelector(state => state.counter)
return <div>{counter}</div>
}
Official doc:
https://react-redux.js.org/api/hooks#useselector-examples
Also found this working example for you to refer.
https://codesandbox.io/s/8l0sv
So i'm doing a API GET request and set the data on reducer, but the component render twice, first before dispatch and another after, the first one is causing map function problem
what can i do to avoid render twice and solve map function problem?
App.js
componentDidMount(){
this.props.carregarLojas();
}
render(){
const { lojasTeste } = this.props;
//rendering 2 times
console.log(lojasTeste);
return(
<div>
lojasTeste.map((i, index) => (
<h1>{i.name}</h1>
))
</div>
)
}
const mapStateToProps = store => ({
lojasTeste: store.lojaState.lojasTeste
});
const mapDispatchToProps = dispatch => {
return {
carregarLojas: () => {
dispatch(carregarLojas());
}
};
};
export default connect(mapStateToProps, mapDispatchToProps)(App);
Action.js
export const setarLojas = (lojas) =>{
return {
type: SETAR_LOJAS,
data: lojas
}
}
export const carregarLojas = () => {
return (dispatch) => {
return API.get('loja')
.then(response => {
dispatch(setarLojas(response.data))
})
.catch(error => {
throw(error);
})
}
Reducer.js
const initialState ={
lojasTeste: {}
}
export const lojaReducer = (state = initialState, action) => {
switch (action.type){
case SETAR_LOJAS:
return {
...state,
lojasTeste: action.data
}
default:
return state;
}
}
The double render is totally normal:
Your component render once, then call the carregarLojas method which is async. When resolved, the method will update your redux store, which is connected with the props of your component (mapStateToProps). When a prop is updated, it cause automatically a rerender.
Also, for your map problem, you didn't initialized lojasTeste as an array, but as an object. You can't use map on an object (cf https://developer.mozilla.org/fr/docs/Web/JavaScript/Reference/Objets_globaux/Array/map)
I have a component parent and a component child with some props connected to the parent state.
In the parent I call setState but the componentWillReceiveProps function of the child is not fired.
More precisaly, its fired in a certain point of the parent, its not fired in another point.
This is the parent:
... imports
class HomeScreen extends Component {
constructor(props) {
super(props);
dispatchFbPermissionAction = this.dispatchFbPermissionAction.bind(this);
this.state = {
fbPermissions: [],
}
}
componentDidMount () {
this._loadInitialState();
}
_responsePermissionsCallback(error: ?Object, result: ?Object) {
if (error) {
log('Error fetching data: ' + error.toString());
} else {
dispatchFbPermissionAction(result.data);
}
}
dispatchFbPermissionAction = (data) => {
// **NOT FIRED**
this.setState({
fbPermissions: data
});
this.props.fbPermissionsLoaded(data);
}
async _loadInitialState() {
AccessToken.getCurrentAccessToken().then(
(data) => {
if (data) {
const infoRequest = new GraphRequest(
'/me/permissions',
null,
this._responsePermissionsCallback,
);
new GraphRequestManager().addRequest(infoRequest).start();
// **FIRED**
this.setState({
...
});
this.props.loggedIn();
}
}
);
}
render () {
const { navigation } = this.props;
return (
<Container>
<ScrollableTabView
<View tabLabel="ios-film" style={styles.tabView}>
<Text style={styles.tabTitle}>{_.toUpper(strings("main.theatres"))}</Text>
<ListTheatre navigation={this.props.navigation} filterText={this.state.filterText} isLoggedIn={this.state.isLoggedIn} fbPermissions={this.state.fbPermissions}></ListTheatre>
</View>
</ScrollableTabView>
</Container>
)
}
}
const mapStateToProps = (state) => {
return {
isLoggedIn: state.isLoggedIn,
listTheatre: state.listTheatre,
listMusic: state.listMusic
};
};
// wraps dispatch to create nicer functions to call within our component
const mapDispatchToProps = (dispatch) => ({
startup: () => dispatch(StartupActions.startup()),
loggedIn: () => dispatch({
type: LOGGED_IN
}),
fbPermissionsLoaded: (data) => dispatch({
type: FB_PERMISSIONS_LOADED,
fbPermissions: data
})
});
export default connect(mapStateToProps, mapDispatchToProps)(HomeScreen)
And this is the child:
... imports
class ListTheatre extends Component {
constructor(props) {
super(props);
this.state = {
...
}
}
componentWillReceiveProps(nextProps) {
log(this.props)
}
shouldComponentUpdate(nextProps, nextState) {
return !nextState.fetching;
}
render() {
const { navigate } = this.props.navigation;
return (
<SectionList
...
/>
)
}
}
ListTheatre.propTypes = {
isLoggedIn: PropTypes.bool.isRequired,
}
const mapStateToProps = (state) => {
return {
isLoggedIn: state.isLoggedIn
};
};
const mapDispatchToProps = (dispatch) => ({
startup: () => dispatch(StartupActions.startup())
});
export default connect(mapStateToProps, mapDispatchToProps)(ListTheatre);
I do not why the setState after the GraphRequestManager().addRequest call works like a charm (the componentWillReceiveProps function of the child is fired), while the setState in the dispatchFbPermissionAction function does not fire the componentWillReceiveProps function of the child.
This is due to connect/Connect(ListTheatre) that wraps your ListTheatre component implemented sCU(shouldComponentUpdate) internally for you, turn it off by setting pure option of connect to false like
export default connect(mapStateToProps, mapDispatchToProps, null, {pure: false})(ListTheatre)
[pure] (Boolean): If true, connect() will avoid re-renders and calls to mapStateToProps, mapDispatchToProps, and mergeProps if the relevant state/props objects remain equal based on their respective equality checks. Assumes that the wrapped component is a “pure” component and does not rely on any input or state other than its props and the selected Redux store’s state. Default value: true