I have a connected component with Form and button. While unit testing the component using mount , i get the error "Actions must be plain objects."
This is happening due to the redux calls being made in the handleSubmit method, is there a way to mock the calls from happening.
import React from "react";
import {Provider} from "react-redux";
import configureStore from "redux-mock-store";
import {Props, InitialState} from "../mock_data/mock.data.jsx";
const mockStore = configureStore();
let store = mockStore(InitialState);
const params = {
url: "http://localhost:9879"
};
let props = {};
beforeEach(() => {
// reset props before each test
props = JSON.parse(JSON.stringify(Props));
props.actions = {};
props.email = Props.email;
props.params = params;
});
handleSubmit({ values }) {
const {url} = this.props;
const val = values.valcode;
this.props.actions.getCode(url);
this.props.makeExternalCall(val);
}
return (
<Form
initialValues={{ ...personalUser }}
onSubmit={this.handleSubmit}>
{
({ form, errors }) => {
const isDisabled = errors !== null || this.state.disableReset;
return (
<div>
<button name="btnConfirm" type="submit"
className={styles.btnConfirm} disabled={isDisabled}>/>
</button>
</div>
);
}
}
</Form>
);
const mapDispatchToProps = (dispatch) => ({
actions: bindActionCreators(actions, dispatch),
makeExternalCall: bindActionCreators(makeExternalCallAction, dispatch)
});
export default connect(null, mapDispatchToProps)(injectIntl(TestComponent));
Test
it(" - should display the proper label", () => {
props.error = "This is an error message";
props.actions.getCode = sinon.stub().returns({
then: () => {}
});
props.makeExternalCall = sinon.stub().returns({
then: () => {}
});
store = mockStore(props);
const wrapper = mountWithIntl(<Provider store={store}>
<TestComponent {...props}/>
</Provider>);
expect(wrapper).to.not.be.null;
expect(wrapper.find("button").prop("disabled")).equals(true);
wrapper.find("TestComponent").get(0).handleSubmit({values:{valcode: "AkPdQ2" }});
});
Is it possible to mock the mapDispatchToProps ,which is giving the error
PhantomJS 2.1.1 (Mac OS X 0.0.0) <TestComponent Component /> - should display the proper label FAILED
Actions must be plain objects. Use custom middleware for async actions.
Related
I am using jest and react-testing-library to write tests for my react application. In the application, I have a context provider that contains the state across most of the application. In my tests, I need to mock one state variable in context provider and leave one alone, but I'm not sure how to do this.
AppContext.tsx
const empty = (): void => {};
export const emptyApi: ApiList = {
setStep: empty,
callUsers: empty,
}
export const defaultState: StateList = {
userList = [],
step = 0,
}
const AppContext = createContext<ContextProps>({ api: emptyApi, state: defaultState });
export const AppContextProvider: React.FC<Props> = props => {
const [stepNum, setStepNum] = React.useState(0);
const [users, setUsers] = useState<User[]>([]);
const api: ApiList = {
setStep: setStepNum,
callUsers: callUsers,
}
const state: Statelist = {
userList: users,
step: stepNum,
}
const callUsers = () => {
const usersResponse = ... // call users api - invoked somewhere else in application
setUsers(userResponse);
}
return <AppContext.Provider value={{ api, state}}>{children}</AppContext.Provider>;
}
export default AppContext
In _app.tsx
import { AppContextProvider } from '../src/context/AppContext';
import { AppProps } from 'next/app';
import { NextPage } from 'next';
const app: NextPage<AppProps> = props => {
const { Component, pageProps } = props;
return (
<React.Fragment>
<AppContextProvider>
<Component {...pageProps}
</AppContextProvider>
</React.Fragment>
)
}
export default app
the component that uses AppContext
progress.tsx
import AppContext from './context/AppContext';
const progress: React.FC<Props> = props => {
const { state: appState, api: appApi } = useContext(AppContext);
const { userList, step } = appState;
const { setStep } = appApi;
return (
<div>
<Name />
{step > 0 && <Date /> }
{step > 1 && <UserStep list={userList} /> }
{step > 2 && <Address />
</div>
)
}
export default progress
users is data that comes in from an API which I would like to mock. This data is shown in progress.tsx.
stepNum controls the display of subcomponents in progress.tsx. It is incremented after a step is completed, once incremented, the next step will show.
In my test, I have tried the following for rendering -
progress.test.tsx
import progress from './progress'
import AppContext, { emptyApi, defaultState } from './context/AppContext'
import { render } from "#testing-library/react"
describe('progress', () => ({
const api = emptyApi;
const state = defaultState;
it('should go through the steps', () => ({
state.usersList = {...}
render(
<AppContext.Provider value={{api, state}}>
<progress />
</AppContext.Provider>
)
// interact with screen...
// expect(...)
})
})
However, when I set up the context provider like that in the test, I can set the userList to whatever I want, but it'll also override the setStep state hook so in the component, it won't update the state.
Is it possible to mock only the users variable inside of AppContext with jest, but leave users hook alone?
I am trying to the test that an action have dispatched inside getServerSiderProps function.
I am using next + react + Redux
The test fails claiming timesActionDispatched equal 0 instead of 1
How i can I fix the test?
I tested the page manually and everything works so the problem is 100% in the test + the test was successful when the action been dispatched on the client side.
thanks to juliomalves for the solution
The test
jest.mock('../../../redux/actions/recipe', () => ({
loadRecipeListAction: jest.fn()
}));
test('should dispatch loadRecipeListAction', async () => {
const listOfRecipes = (await getStaticProps()).props.listOfRecipes;
render(
<Provider store={store}>.
<RecipeList listOfRecipes={listOfRecipes} />
</Provider>
);
const timesActionDispatched = loadRecipeListAction.mock.calls.length;
expect(timesActionDispatched).toBe(1);
});
The page
import DisplayRecipes from '../../components/recipes/DisplayRecipes';
import React from 'react';
import { loadRecipeListAction } from '../../redux/actions/recipe';
import { store } from '../../redux/store';
const RecipeList = ({ listOfRecipes }) => {
return (
<main data-testid='recipeList'>
{listOfRecipes ? <DisplayRecipes recipesToDisplay={listOfRecipes} /> : null}
</main>
);
};
export async function getStaticProps() {
await store.dispatch(loadRecipeListAction());
const { listOfRecipes } = await store.getState().recipeReducer;
return { props: { listOfRecipes }, revalidate: 10 };
}
export default RecipeList;
Here is my test
const initialRootState = {
accounts: [mockAccounts],
isLoading: false
}
describe('Account Dashboard', () => {
let rootState = {
...initialRootState
}
const mockStore = configureStore()
const store = mockStore({ ...rootState })
const mockFunction = jest.fn()
jest.spyOn(Redux, 'useDispatch').mockImplementation(() => mockFunction)
jest
.spyOn(Redux, 'useSelector')
.mockImplementation((state) => state(store.getState()))
afterEach(() => {
mockFunction.mockClear()
// Reseting state
rootState = {
...initialRootState
}
})
it('renders correctly', () => {
const wrapper = mount(
<TestWrapper>
<AccountDashboard />
</TestWrapper>
)
console.log(wrapper)
})
})
In my component I am mapping accounts from the state. In my test I am getting the following error TypeError: Cannot read property 'map' of undefined
I would like to test an if statement I am using in my component to ensure it's returning the proper view based on the number of accounts I am receiving.
However, when I console.log(store.getState()) it is printing correctly. What am I doing wrong?
If you're going to test a Redux connected component, I'd recommend steering away from mocking its internals and instead to test it as if it were a React component connected to a real Redux store.
For example, here's a factory function for mounting connected components with enzyme:
utils/withRedux.jsx
import * as React from "react";
import { createStore } from "redux";
import { Provider } from "react-redux";
import { mount } from "enzyme";
import rootReducer from "../path/to/reducers";
/*
You can skip recreating this "store" by importing/exporting
the real "store" from wherever you defined it in your app
*/
export const store = createStore(rootReducer);
/**
* Factory function to create a mounted Redux connected wrapper for a React component
*
* #param {ReactNode} Component - the Component to be mounted via Enzyme
* #function createElement - Creates a wrapper around the passed in component with incoming props so that we can still use "wrapper.setProps" on the root
* #returns {ReactWrapper} - a mounted React component with a Redux store.
*/
export const withRedux = Component =>
mount(
React.createElement(props => (
<Provider store={store}>
{React.cloneElement(Component, props)}
</Provider>
)),
options
);
export default withRedux;
Now, using the above factory function, we can test the connected component by simply using store.dispatch:
tests/ConnectedComponent.jsx
import * as React from "react";
import withRedux, { store } from "../path/to/utils/withRedux";
import ConnectedComponent from "../index";
const fakeAccountData = [{...}, {...}, {...}];
describe("Connected Component", () => {
let wrapper;
beforeEach(() => {
wrapper = withRedux(<ConnectedComponent />);
});
it("initially shows a loading indicator", () => {
expect(wrapper.find(".loading-indicator")).exists().toBeTruthy();
});
it("displays the accounts when data is present", () => {
/*
Ideally, you'll be dispatching an action function for simplicity
For example: store.dispatch(setAccounts(fakeAccountData));
But for ease of readability, I've written it out below.
*/
store.dispatch({ type: "ACCOUNTS/LOADED", accounts: fakeAccountData }));
// update the component to reflect the prop changes
wrapper.update();
expect(wrapper.find(".loading-indicator")).exists().toBeFalsy();
expect(wrapper.find(".accounts").exists()).toBeTruthy();
});
});
This vastly simplifies not having to mock the store/useSelector/useDispatch over and over when you start to test other Redux connected components.
On a side note, you can skip this entirely if you use react-redux's connect function while exporting the unconnected component. Instead of importing the default export, you can import the unconnected component within your test...
Example component:
import * as React from "react";
import { connect } from "react-redux";
export const Example = ({ accounts, isLoading }) => { ... };
const mapStateToProps = state => ({ ... });
const mapDispatchToProps = { ... };
export default connect(mapStateToProps, mapDispatchToProps)(Example);
Example test:
import * as React from "react";
import { mount } from "enzyme";
import { Example } from "../index";
const initProps = {
accounts: [],
isLoading: true
};
const fakeAccountData = [{...}, {...}, {...}];
describe("Unconnected Example Component", () => {
let wrapper;
beforeEach(() => {
wrapper = mount(<Example {...initProps } />);
});
it("initially shows a loading indicator", () => {
expect(wrapper.find(".loading-indicator")).exists().toBeTruthy();
});
it("displays the accounts when data is present", () => {
wrapper.setProps({ accounts: fakeAccountData, isLoading: false });
wrapper.update();
expect(wrapper.find(".loading-indicator")).exists().toBeFalsy();
expect(wrapper.find(".accounts").exists()).toBeTruthy();
});
});
I figured out that my test was working incorrectly due to my selector function in my component being implement incorrectly. So the test was actually working properly!
Note: My team is currently using mocked data(waiting for the API team to finish up endpoints).
Originally the useSelector function in my component(that I was testing) looked like:
const { accounts, isLoading } = useSelector(
(state: RootState) => state.accounts,
shallowEqual
)
When I updated this to:
const { accounts, isAccountsLoading } = useSelector(
(state: RootState) => ({
accounts: state.accounts.accounts,
isAccountsLoading: state.accounts.isLoading
}),
shallowEqual
)
My tests worked - here are my final tests:
describe('App', () => {
let rootState = {
...initialState
}
const mockStore = configureStore()
const store = mockStore({ ...rootState })
jest.spyOn(Redux, 'useDispatch').mockImplementation(() => jest.fn())
jest
.spyOn(Redux, 'useSelector')
.mockImplementation((state) => state(store.getState()))
afterEach(() => {
jest.clearAllMocks()
// Resetting State
rootState = {
...initialState
}
})
it('renders correctly', () => {
const wrapper = mount(
<TestWrapper>
<Dashboard />
</TestWrapper>
)
expect(wrapper.find('[data-test="app"]').exists()).toBe(true)
expect(wrapper.find(verticalCard).exists()).toBe(false)
expect(wrapper.find(horizontalCard).exists()).toBe(true)
})
it('renders multiple properly', () => {
rootState.info = mockData.info
const wrapper = mount(
<TestWrapper>
<Dashboard />
</TestWrapper>
)
expect(wrapper.find(verticalCard).exists()).toBe(true)
expect(wrapper.find(horizontalCard).exists()).toBe(false)
})
})
I have a react context like this
UserContext.js
export const UserContext = createContext({
createUser: async () => {
...execute some xhr requests and manipulateData
},
getUser: () => {},
});
const UserProvider ({ children }) => {
const context = {
createUser,
getUser,
};
return (
<UserContext.Provider value={context}>{children}</UserContext.Provider>
);
}
And the following unit test
UserContext.spec.js
import { render } from '#testing-library/react';
import UserProvider, { UserContext } from '#my-contexts/user';
...
let createUser = () => {};
render(
<UserProvider>
<UserContext.Consumer>
{value => {
createUser = value.createUser;
return null;
}}
</UserContext.Consumer>
</UserProvider>
);
await createUser(data);
expect(data).toEqual({ status: 200 });
I am not sure if this way
let createUser = () => {};
render(
<UserProvider>
<UserContext.Consumer>
{value => {
createUser = value.createUser;
return null;
}}
</UserContext.Consumer>
</UserProvider>
);
it is the better technique for "extract" the inner methods exposed in the UserContext (in this case createUser method)
In my App code I use this context like following:
import { useContext } from 'react';
import { UserContext } from '#my-contexts/user';
const someComponent = (props) => {
...
const { createUser } = useContext(UserContext);
const handleCreate = (e) => {
createUser(form);
};
return (
<form>
<label>Username</label>
<input value={form.userName} />
<button type="submit" onClick={handleCreate}> create user </button>
</form>
)
};
But the problem is that I can get the exposed methods from context only if I have a component and get the method using useContext hook.
If I want to test the createUser method in isolation:
Exists a better way for get the methods exposed in context without rendering a provider and consumer and "manually extract" him?
I'm having trouble checking if my mock action was called from an onSubmit on a form:
Login Form:
class Login extends Component {
constructor(props) {
super(props);
this.state = {
email: "",
password: "",
};
}
handleSubmit = e => {
e.preventDefault();
this.props.loginUser(this.state.email, this.state.password);
};
handleChange = event => {
const { value } = event.target;
const { name } = event.target;
this.setState({
[name]: value,
});
};
render() {
const { t } = this.props;
return (
<div className="login-container>
<form data-test-id="login-form" onSubmit={this.handleSubmit}>
<TextComponent
label="Email"
testId="login-email"
value={this.state.email}
handleChange={this.handleChange}
/>
<div className="my-3" />
<TextComponent
label="Password"
testId="login-password"
value={this.state.password}
handleChange={this.handleChange}
/>
<button action="submit" data-test-id="login-button">
{t("en.login")}
</button>
</form>
</div>
);
}
}
function mapStateToProps(state) {
return {
authenticated: state.login.authenticated,
};
}
function mapDispatchToProps(dispatch) {
return bindActionCreators({ loginUser }, dispatch);
}
export default connect(mapStateToProps, mapDispatchToProps)(withTranslation()(Login));
Test:
import { mount, shallow, render } from "enzyme";
import React from "react";
import { Provider } from "react-redux";
import { I18nextProvider } from "react-i18next";
import { Router } from "react-router-dom";
import { findByTestAtrr, testStore } from "../../test/utils/utils";
import Login from "../../src/Login/index";
import i18n from "../../src/i18n";
import history from "../../src/history";
const setUp = loginUser => {
const initialState = {
login: {
authenticated: true,
},
};
const store = testStore(initialState);
const wrapper = mount(
<I18nextProvider i18n={i18n}>
<Provider store={store}>
<Router history={history}>
<Login onSubmit={loginUser} />
</Router>
</Provider>
</I18nextProvider>
);
return wrapper;
};
describe("submit button", () => {
let emailInput;
let passwordInput;
let submitButton;
let newWrapper;
let loginAction;
beforeEach(() => {
loginAction = jest.fn();
newWrapper = setUp(loginAction);
emailInput = findByTestAtrr(newWrapper, "login-email");
emailInput.instance().value = "email.com";
emailInput.simulate("change");
passwordInput = findByTestAtrr(newWrapper, "login-password");
passwordInput.instance().value = "password";
passwordInput.simulate("change");
submitButton = findByTestAtrr(newWrapper, "login-button");
});
it("login action is called", () => {
console.log(submitButton.debug());
submitButton.simulate("submit");
expect(loginAction).toHaveBeenCalledTimes(1);
});
});
});
I'm able to simulate adding values to the email and password but I can't simulate the onClick to work. Am I testing the submit function incorrectly?
This is my submit button when I console.log
console.log __tests__/integration_tests/login.test.js:97
<button action="submit" data-test-id="login-button">
Log in
</button>
error:
expect(jest.fn()).toHaveBeenCalledTimes(expected)
Expected number of calls: 1
Received number of calls: 0
As raised from 2016 on github: Simulated click on submit button in form does not submit form #308, this issue still persists on my project, using Jest + Enzyme (2020).
There are some workarounds such as using tape or sino.
The other solution is to get the DOM element and trigger the click without using simulate (go to given link, scroll down the thread to learn more).
If anyone ever found a solution to simulate form submit, kindly tag me here. Thank you.
As explained in the below mentioned link, you can use done and setTimeout.
https://github.com/ant-design/ant-design/issues/21272#issuecomment-628607141
it('login action is called', (done) => {
submitButton.simulate("submit");
setTimeout(() => {
expect(loginAction).toHaveBeenCalledTimes(1);
done();
}, 0);
});
it('should call onSubmit method', ()=>{
const spy = jest.spyOn(wrapper.instance(), "onSubmit");
wrapper.find('button[type="submit"]').simulate('click');
expect(spy).toHaveBeenCalledTimes(1);
});
Check the name of method to be called on simulate click. In my case it was onSubmit.
No need to import sinon or spy from sinon jest does everything.