Writing test case for for simulating onClick event in Enzyme? - reactjs

I want to write test cases for HeaderBar.jsx in the file HeaderBar.spec.jsx.
First I want to write test case for checking my history.push() is executing call then I want to check for history.push() executing with mockURL.
history.push() is found in onLogout()
here is HeaderBar.jsx
import React from 'react';
import { PropTypes } from 'prop-types';
import { bindActionCreators } from 'redux';
import { makeStyles } from '#material-ui/core/styles';
import AppBar from '#material-ui/core/AppBar';
import Toolbar from '#material-ui/core/Toolbar';
import Typography from '#material-ui/core/Typography';
import Button from '#material-ui/core/Button';
import IconButton from '#material-ui/core/IconButton';
import MenuIcon from '#material-ui/icons/Menu';
import { connect } from 'react-redux';
import { withTranslation } from 'react-i18next';
import { ROUTE_CONSTANTS, I18N_CONSTANTS } from '../../../../constants';
import { commonAction, sessionAction } from '../../../../redux/actions';
const useStyles = makeStyles(theme => ({
root: {
flexGrow: 1
},
menuButton: {
marginRight: theme.spacing(2)
},
title: {
flexGrow: 1
}
}));
const HeaderBar = ({ history, t, actions }) => {
const classes = useStyles();
const onLogout = () => {
history.push(ROUTE_CONSTANTS.MEMBER.LOGOUT);
actions.logout();
};
// const onLogout = () => { throw new Error('Failed to run') };
return (
<div className={classes.root}>
<AppBar position="static" color="default">
<Toolbar>
<IconButton edge="start" className={classes.menuButton} color="inherit" aria-label="menu">
<MenuIcon />
</IconButton>
<Typography variant="h6" className={classes.title}>
{t('headerBar.title')}
</Typography>
<Button color="inherit" onClick={onLogout}>
{t('headerBar.logout')}
</Button>
</Toolbar>
</AppBar>
</div>
);
};
HeaderBar.defaultProps = {
t: () => {},
history: {
push: () => {}
}
};
const mapStateToProps = state => {
return {
isLoading: state.common.loadingIndicator.isLoading
};
};
const mapDispatchToProps = dispatch => {
return {
actions: bindActionCreators({ ...commonAction, ...sessionAction }, dispatch)
};
};
HeaderBar.propTypes = {
actions: PropTypes.shape({
logout: PropTypes.func.isRequired
}).isRequired,
t: PropTypes.func,
history: PropTypes.shape({
push: PropTypes.func
})
};
const component = connect(mapStateToProps, mapDispatchToProps)(HeaderBar);
export default withTranslation(I18N_CONSTANTS.NAMESPACE.APP)(component);
HeaderBar.spec.jsx
import React from 'react';
import { mount } from 'enzyme';
import { Provider } from 'react-redux';
import configureStore from '../../../../redux/store/configureStore';
import HeaderBar from './HeaderBar';
const setup = (props = {}) => {
const store = configureStore();
window.location.replace = jest.fn();
const defaultProps = {
isLoading: false,
history: {
replace: jest.fn(x => x)
},
...props
};
return mount(
<Provider store={store}>
<HeaderBar {...defaultProps} />
</Provider>
);
};
describe('HeaderBar component', () => {
it('should call translation 2 times', () => {
const props = { t: jest.fn() };
const wrapper = setup(props);
const { t, isLoading } = wrapper.find('HeaderBar').props();
expect(t).toHaveBeenCalledTimes(2);
expect(isLoading).toBeFalsy();
});
it('should call history on onClick event', () => {
const props = { history: { replace: jest.fn(x => x) }, onLogout: jest.fn(x => x) };
const wrapper = setup(props);
// console.log(wrapper.find('button').debug());
wrapper.find('button').simulate('click');
expect(props.history.replace).toHaveBeenCalledTimes(1);
});
it('should call history with mock URL', () => {
const props = { history: { replace: jest.fn(x => x) } };
const wrapper = setup(props);
const mockURL = '/';
wrapper
.find('button')
.at(0)
.simulate('click');
expect(props.history.replace).toHaveBeenCalledWith(mockURL);
});
});
should call history on onClick event and should call history with mock URL is not working for me.
please help me out on this.

You mock history.replace, but in your component you use history.push.
You mock your functions’ implementations as (x => x), but these functions actually don’t receive any arguments.
Is your logout button really the only in this wrapper’s DOM? Maybe add some id?

Related

Why does React re-render the components multiple times when using Zustand?

I think the Space.js and Loading.js components re-render far too many times. I can understand it up til the very last 4 re-renders. What is causing them is a mystery.
[slug].js
import { useRouter } from "next/router";
import AuthLayout from "#/components/layouts/AuthLayout";
import { useEffect, useState } from "react";
import axios from "#/src/lib/axios";
import Container from "#mui/material/Container";
import Typography from "#mui/material/Typography";
import useBearStore from "stores/pages";
import Loading from "#/components/layouts/Auth/Loading";
export default function Space() {
console.log('Space.js: function()');
const {setLoading} = useBearStore();
const router = useRouter();
const { slug } = router.query;
const [space, setSpace] = useState(null);
const getSpace = async () => {
const { data } = await axios.get(`api/spaces/${slug}`);
setSpace(data.data);
setLoading(false);
};
useEffect(() => {
console.log("Space.js: useEffect()");
getSpace();
}, [slug]);
return (
<Loading>
<Container maxWidth="sm">
<Typography variant="h1" component="h1" gutterBottom>
{space?.name}
</Typography>
</Container>
</Loading>
);
}
Space.getLayout = function getLayout(page) {
return <AuthLayout>{page}</AuthLayout>;
};
Loading.js
import useBearStore from "stores/pages";
import { LinearProgress } from "#mui/material";
import { useEffect } from 'react'
export default function Loading({children}) {
const {loading, setLoading} = useBearStore();
console.log('Loading.js: function()');
useEffect(() => {
console.log('Loading.js: useEffect()');
setLoading(true);
}, []);
return loading ? <LinearProgress /> : children;
}
pages.js
import create from 'zustand'
const useBearStore = create((set) => ({
loading: true,
setLoading: (value) => set((state) => ({ loading: value }))
}))
export default useBearStore;
next.config.js
module.exports = {
// reactStrictMode: true,
// images: {
// domains: ["images.pexels.com"],
// },
};

react testing library Redux with thunk testing, action not being dispatched

i am trying to test if a like counter has been increased after user clicks a button. I am using react testing library where i find a button and preform userEvent.click this should dispatch an action behind the scenes and increment the counter where i can then assert the new value.
This works when i manually go through the ui but cannot get the test to work.
Button:
<Button
size="small"
color="primary"
onClick={() => dispatch(likePosts(post._id))}
>
<ThumbUpIcon fontSize="small" />
Like {`${post.likeCount}`}
{}
</Button>
Thunk action:
export const likePosts = (id) => async (dispatch) => {
try {
const { data } = await api.likePost(id);
dispatch({ type: LIKE, payload: data });
} catch (error) {
console.log(error);
}
I have also set up a test-util to help me test connected component TEST UTIL LINK
I have also added applyMiddleware(thunk) to support thunk when a for a connected component
test-util:
import React from "react";
import { render as rtlRender } from "#testing-library/react";
import { legacy_createStore, applyMiddleware } from "redux";
import { Provider } from "react-redux";
import thunk from "redux-thunk";
// Replace this with the appropriate imports for your project
import reducers from "../redux/reducers";
const render = (
ui,
{
store = legacy_createStore(reducers, applyMiddleware(thunk)),
...renderOptions
} = {}
) => {
const Wrapper = ({ children }) => (
<Provider store={store}>{children}</Provider>
);
return rtlRender(ui, { wrapper: Wrapper, ...renderOptions });
};
export * from "#testing-library/react";
export * from "#testing-library/jest-dom";
// override render method
export { render };
My Test:
import Post from "./Post";
import { render, screen } from "../../../utils/test-utils";
import userEvent from "#testing-library/user-event";
describe("Post", () => {
let initialState;
beforeEach(() => {
initialState = {
_id: "1234",
title: "post title",
message: "post message",
creator: "post creator",
tags: ["postTag", "postTag"],
selectedFile: "path/to/file",
likeCount: 0,
createdAt: "2022-07-20T23:54:25.251Z",
};
});
test("should increment post likes when like button clicked", () => {
render(<Post post={initialState} />, { initialState });
const postLikeButton = screen.getByRole("button", { name: /Like/i });
userEvent.click(postLikeButton);
const clickedPostLikeButton = screen.getByRole("button", {
name: /Like 1/i,
}).textContent;
// expect().toHaveBeenCalled();
expect(clickedPostLikeButton).toBe(100);
});
});
Test error:
TestingLibraryElementError: Unable to find an accessible element with the role "button" and name `/Like 1/i`
That would suggest that the the action was not dispatched when then button was clicked in the test.
UPDATE:
The button is from MUI library:
import { Button } from "#material-ui/core";
The post prop is passed from its parent component Posts:
import React from "react";
import { useSelector } from "react-redux";
import { Grid, CircularProgress } from "#material-ui/core";
import Post from "./Post/Post";
import useStyles from "./styles";
const Posts = ({ setCurrentId }) => {
const posts = useSelector((state) => state.posts);
const classes = useStyles();
return !posts.length ? (
<CircularProgress />
) : (
<Grid
className={classes.container}
container
alignItems="stretch"
spacing={3}
>
{posts.map((post, index) => (
<Grid key={index} item xs={12} sm={6}>
<Post key={post.id} post={post} setCurrentId={setCurrentId} />
</Grid>
))}
</Grid>
);
};
export default Posts;
Also all of this works just fine when using the UI, its just in react testing library test the button onClick seems the not to dispatch likePosts
Did you try with redux-mock-store?
import configureStore from 'redux-mock-store'
const mockStore = configureStore()
const store = mockStore(reducers) // add your reducers here
// ...
render(
<Provider store={store}>
{children}
</Provider>
)

how can i mount component redux antd jest

import React from 'react';
import { Breadcrumb as AntBreadcrumb, Breadcrumb } from 'antd';
import './breadcrumb.scss';
import { Link, withRouter, RouteComponentProps } from 'react-router-dom';
import { updateBreadcrumb } from './../../../redux/actions/baseLayout';
import { connect } from 'react-redux';
import { HomeOutlined, RightOutlined } from '#ant-design/icons';
interface NewProps {
breadcrumb: any;
}
type Props = NewProps & RouteComponentProps<{}>;
// #TODO
class Render extends React.Component<Props> {
state = {
routes: [{ path: '', breadcrumbName: '' }]
};
// eslint-disable-next-line react/no-deprecated
componentWillReceiveProps(nextProps: any) {
this.setState({
routes: this.props.breadcrumb
});
}
componentDidMount = () => {
this.setState({
routes: this.props.breadcrumb
});
};
itemRender = (route: any, params: any, routes: any, paths: any) => {
const last = routes.indexOf(route) === routes.length - 1;
return last ? (
<span>{route.breadcrumbName}</span>
) : (
<Link to={paths.join('/')}>{route.breadcrumbName}</Link>
);
};
shouldComponentUpdate = () => {
return true;
};
componentDidUpdate = (prevProps: RouteComponentProps) => {
if (prevProps !== this.props) {
this.setState({
routes: this.props.breadcrumb
});
}
};
render() {
return (
<div style={{ color: 'red' }}>
<Breadcrumb
style={{ fontWeight: 600, fontSize: '15px', color: 'black' }}
separator={
<RightOutlined
style={{ transform: 'scalex(0.7)', fontSize: '16px' }}
/>
}
>
{this.state.routes.map((route, index) => {
return (
<Breadcrumb.Item key={index} href={`${route.path}`}>
{`${route.breadcrumbName}`}
{/* <HomeOutlined /> */}
</Breadcrumb.Item>
);
})}
</Breadcrumb>
</div>
);
}
}
const mapDispatchToProps = {
updateBreadcrumb: updateBreadcrumb
};
const mapStateToProps = (state: any) => {
return {
breadcrumb: state.breadcrumb.breadcrumb || []
};
};
export default withRouter(connect(mapStateToProps, mapDispatchToProps)(Render));
above code i wanted to write test case but i am not able to mount it i tried to mount it with just but it's not creating snapshot it's givingtest case fail please guide me to right direction or some documentations i am really new to raect and my company gave me this assignment to write test case but i am not finding anywhere any relevent documentations.
import React from 'react';
import Enzyme, { shallow, mount } from 'enzyme';
import Componnent from '../breadcrumb';
import { BrowserRouter as Router } from 'react-router-dom';
import configureStore from 'redux-mock-store'; //ES6 modules
import { Provider } from 'react-redux';
import thunk from 'redux-thunk';
import renderer from 'react-test-renderer';
import Adapter from 'enzyme-adapter-react-16';
Enzyme.configure({
adapter: new Adapter()
});
const setUp = (initprops:any) => {
const wrapper = mount(<Router><Provider store={initprops}><Componnent /></Provider></Router>);
return wrapper;
};
describe('Login Component', () => {
const middlewares = [thunk];
const mockStore = configureStore(middlewares);
const initialState = {};
const props = mockStore(initialState);
let wrapper:any,instancewrapper:any;
beforeEach(() => {
wrapper = setUp(props);
instancewrapper = wrapper.instance();
});
it('should render correctly', () => {
const tree = renderer.create(<Router><Provider store={props}><Componnent /></Provider></Router>).toJSON();
expect(tree).toMatchSnapshot();
});
});
You can still shallow render a component and use .dive() function to unwrap the main component.

How to load data from redux store when redirecting to a new page

I have two pages; the first one called QuizHomePage and which contains a welcome message and a button which allows to user to start a quiz.
QuizHomePage.tsx:
import Button from "#material-ui/core/Button";
import { createStyles, makeStyles, Theme } from "#material-ui/core/styles";
import Typography from "#material-ui/core/Typography";
import React from "react";
import { connect } from "react-redux";
import { Dispatch } from "redux";
import { questionRequest, startQuiz } from "../../actions/index";
import AppBar from "../../components/common/AppBar";
import history from "../../history/history";
const useStyles = makeStyles((theme: Theme) =>
createStyles({
root: {
textAlign: "center",
margin: theme.spacing(10)
},
button: {
marginTop: theme.spacing(6)
}
}));
interface IProps {
questionRequest: () => void;
startQuiz: () => void;
}
const QuizHomePage = (props: IProps) => {
const classes = useStyles();
const { questionRequest, startQuiz } = props;
const handleStartQuiz = () => {
questionRequest();
startQuiz();
return history.push("/contentQuiz");
};
return (<>
<AppBar />
<div className={classes.root}>
<Typography
color="textPrimary"
gutterBottom
variant="h2">
Test your javascript skills
</Typography>
<Typography
color="textSecondary"
gutterBottom
variant="h6">
Please click the start button to launch the Quiz
</Typography>
<Button
className={classes.button}
color="secondary"
onClick={handleStartQuiz}
variant="contained">Start</Button>
</div>
</>);
};
const mapDispatchToProps = (dispatch: Dispatch) => {
return {
startQuiz: () => dispatch(startQuiz()),
questionRequest: () => dispatch<any>(questionRequest())
};
};
export default connect(null, mapDispatchToProps)(QuizHomePage);
When I click the Start button I dispatch 2 actions questionRequest which executes a promise and return the list of all questions from the database and startQuiz which dispatch an action to update the state of the quiz, then the user will be redirected to the quiz question page which described by this code:
import { Typography } from "#material-ui/core";
import React from "react";
import { connect } from "react-redux";
import SyntaxHighlighter from "react-syntax-highlighter";
import { dark } from "react-syntax-highlighter/dist/esm/styles/prism";
import { incrementQuestion, IQuestion } from "../../actions/index";
import ContentQuiz from "../../components/ContentQuiz";
interface IProps {
currentQuestionNumber: number;
questions: IQuestion[];
}
const QuizzContainer = (props: IProps) => {
const { currentQuestionNumber, questions } = props;
const currentQuestion = questions[currentQuestionNumber];
const handleNextQuiz = () => {
incrementQuestion();
};
return (
<ContentQuiz
questionNumber={currentQuestionNumber}
handleClick={handleNextQuiz}>
<div>
<Typography variant="h3" gutterBottom> What's the output of </Typography>
<>
<SyntaxHighlighter language="javascript" style={dark}>
{currentQuestion.questionDescription}
</SyntaxHighlighter>
</>
</div>
</ContentQuiz>
);
};
const mapStateToProps = (state: any) => {
const { currentQuestionNumber, questions } = state.quiz;
return {
currentQuestionNumber,
questions
};
};
export default connect(mapStateToProps, { incrementQuestion })(QuizzContainer);
actions.ts:
export const questionRequest = (): ThunkAction<void, AppState, null, Action<string>> => {
return async (dispatch: Dispatch) => {
dispatch(startQuestionRequest());
getQuestionsApi().then((response: AxiosResponse) => {
const { data } = response;
dispatch(questionSuccess(data.result));
},
(error: AxiosError) => {
let errorMessage = "Internal Server Error";
if (error.response) {
errorMessage = error.response.data.error;
}
dispatch(questionFail(errorMessage));
dispatch(errorAlert(errorMessage));
});
};
};
I got an error :
TypeError: Cannot read property 'questionDescription' of undefined
it's normally because for react the questionsvariable is undefined. I realized that the questions array is not updated quickly but after some amount of time due to the server response that's why the QuizzContainerreturns the error mentioned below when it tries to mount the component.
Is it a good approach to lazy load the component in order to wait the fetching of questions from server and then mounting the QuizContainer component? I trieduseEffectwhich normally behaves ascomponentDidMount` but it does not work with my issue.
How can I fix that?
You need to use async and await here. If you don't wait until the promise gets resolved and navigate the user to the next page, you can never guarantee that the user will see the question as soon as page loads.
const handleStartQuiz = async () => {
awit questionRequest();
await startQuiz();
return history.push("/contentQuiz");
}
Second approach: (I don't recommend)
Don't render the question unless you have questions filled in the redux state.
return(
{ questions && <ContentQuiz> ... </ContentQuiz> }
)
I resolved my question using this update:
import { Typography } from "#material-ui/core";
import React from "react";
import { connect } from "react-redux";
import SyntaxHighlighter from "react-syntax-highlighter";
import { dark } from "react-syntax-highlighter/dist/esm/styles/prism";
import { incrementQuestion, IQuestion } from "../../actions/index";
import ContentQuiz from "../../components/ContentQuiz";
interface IProps {
currentQuestionNumber: number;
loadingData: boolean;
questions: IQuestion[];
questionRequest: () => void;
}
const QuizzContainer = (props: IProps) => {
const { currentQuestionNumber, loadingData, questions, questionRequest } = props;
useEffect(() => {
questionRequest();
});
const currentQuestion = questions[currentQuestionNumber];
const handleNextQuiz = () => {
incrementQuestion();
};
return (
<div>
{loadingData ? ("Loading ...") : (
<ContentQuiz
questionNumber={currentQuestionNumber}
handleClick={handleNextQuiz}>
<div>
<Typography variant="h3" gutterBottom> What's the output of </Typography>
<>
<SyntaxHighlighter language="javascript" style={dark}>
{currentQuestion.questionDescription}
</SyntaxHighlighter>
</>
</div>
</ContentQuiz>
)}
</div>
);
};
const mapStateToProps = (state: any) => {
const { currentQuestionNumber, loadingData, questions } = state.quiz;
return {
currentQuestionNumber,
loadingData,
questions
};
};
const mapDispatchToProps = (dispatch: Dispatch) => {
return {
incrementQuestion: () => dispatch(incrementQuestion()),
questionRequest: () => dispatch<any>(questionRequest())
};
};
export default connect(mapStateToProps, mapDispatchToProps)(QuizzContainer);

Integration test React+Redux

I am trying to test my simple component (previous toggle, today named Switch in material-ui library).
I have this wrapped into:
class AutoRefreshSwitch extends React.Component {
constructor(props) {
super(props);
this.input = null;
}
handleChange = () => {
console.log('handler ')
this.props.onAutoRefreshClick(!this.props.autoRefreshStatus);
};
render() {
const {classes} = this.props;
return (
<FormControlLabel
control={
<Switch
checked={this.props.autoRefreshStatus}
onChange={this.handleChange}
color="primary"
classes={{
switchBase: classes.switchBase,
checked: classes.checked,
colorPrimary: classes.colorPrimary,
bar: classes.bar,
icon: classes.icon,
root: classes.root,
}}
disableRipple
inputProps={{id: "switch12345"}}
/>
}
label="Auto refresh"
classes={{label: classes.label}}
/>
);
}
}
export default withStyles(styles)(AutoRefreshSwitch);
This component is placed like it:
<Container> -> it has mapToState and mapToProps with this onAutoRefreshClick which is passed as a prop to component and then to AutoRefreshSwitch
<Component>
<AutoRefreshSwitch onAutoRefreshClick={onAutoRefreshClick}
autoRefreshStatus={autoRefreshStatus}
/>
Now my test is:
import {applyMiddleware, combineReducers, createStore} from 'redux';
import thunk from 'redux-thunk';
import React from 'react';
import {Provider} from 'react-redux';
import {configure, mount} from 'enzyme';
import {myReducer} from "../../src/reducers/overview";
import AutoRefreshSwitch from "../../src/blahblah/auto-refresh-switch";
import Adapter from 'enzyme-adapter-react-16';
import {setAutoRefreshStatus} from "../../src/actions/overview";
// from https://medium.freecodecamp.org/real-integration-tests-with-react-
// redux-and-react-router-417125212638
export function setupIntegrationTest(reducers, initialRouterState = {}) {
const dispatchSpy = jest.fn(() => ({}));
const reducerSpy = (state, action) => dispatchSpy(action);
const emptyStore = applyMiddleware(thunk)(createStore);
const combinedReducers = combineReducers({
reducerSpy,
...reducers,
});
const store = emptyStore(combinedReducers);
return { store, dispatchSpy };
}
configure({adapter: new Adapter()});
describe('integration tests', () => {
let store;
let dispatchSpy;
let wrapper;
beforeEach(() => {
({store, dispatchSpy} = setupIntegrationTest({myReducer}));
wrapper = mount(
<Provider store={store}>
<AutoRefreshSwitch onAutoRefreshClick={setAutoRefreshStatus}
autoRefreshStatus={store.getState().myReducer.autoRefreshStatus}
/>
</Provider>)
});
it('should change the status', () => {
wrapper.find('#switch12345').simulate('change');
wrapper.update();
expect(store.getState().myReducer.autoRefreshStatus).toBe(false)
});
});
Now problem is that, code goes to handleChange in AutoRefreshSwitch but it does not invoke rest of code (this.props.onAutoRefreshClick is not triggered)
I wonder if it is because I don't mount the parents of AutoRefreshSwitch.
This was supposed to be integration test inspired by
https://medium.freecodecamp.org/real-integration-tests-with-react-redux-and-react-router-417125212638
Thanks in advance for any help :)
There was missing dispatch in
beforeEach(() => {
({store, dispatchSpy} = setupIntegrationTest({myReducer}));
wrapper = mount(
<Provider store={store}>
<AutoRefreshSwitch onAutoRefreshClick={() => store.dispatch(setAutoRefreshStatus)}
autoRefreshStatus={store.getState().myReducer.autoRefreshStatus}
/>
</Provider>)
});

Resources