unit test react container component - javascript

I am trying to unit test React container component using chai/sinon. This is how the component is defined:
import { connect } from 'react-redux';
import React, { Component } from 'react';
class TestComponent extends Component {
componentDidMount() {
const { loadData } = this.props;
loadData();
}
render() {
const { links, heading } = this.props;
return (
<p>
<h1>{heading}</h1>
<div>{links.map(link => link.label)}</div>
</p>
);
}
}
const mapStateToProps = state => ({
heading: state.heading,
links: state.links
});
const mapDispatchToProps = dispatch => ({
loadData: dispatch('LOAD_DATA')
});
export default connect(mapStateToProps, mapDispatchToProps)(TestComponent);
I am writing a unit test to assert that container component has the props loadData, heading and links. Below is the unit test:
import configureStore from 'redux-mock-store';
import { Provider } from 'react-redux';
import React from 'react';
import { shallow } from 'enzyme';
import TestContainer from './TestContainer';
const mockStore = configureStore({});
describe('TestContainer', () => {
const initialState = {
heading: 'A test component',
links: []
};
const store = mockStore(initialState);
it(
'renders correctly',
sinon.test(() => {
const component = shallow(
<Provider store={store}>
<TestContainer />
</Provider>
);
console.log(component.prop('heading')); // prints undefined
expect(component.prop('heading')).to.equal('A test component'); // gives AssertionError: expected undefined to equal 'A test component'
})
);
});
Above unit test fails and I get all props as undefined. Can somebody explain what am I doing wrong OR how a container component props should be asserted?
Thanks.

Related

How to test mapStateToProps using React Redux and Jest?

When I create a test for my connected React component where I want to test the mapStateToProps logic I run into a problem which I'm not sure how to solve.
Error message
Expected: 1
Received: undefined
24 | it('should show previously rolled value', () => {
25 | // test that the state values were correctly passed as props
> 26 | expect(wrapper.props().lastRolledNumber).toBe(1);
When I check the wrapper.props() it only returns the store object and no properties.
To make sure it's not my code I have found an example that should work to simplify it, but I get the same problem when using that exact version in my app (option #2, https://jsramblings.com/2018/01/15/3-ways-to-test-mapStateToProps-and-mapDispatchToProps.html)
I think it might have to do with the React 16+ version, which I found mentioned here: https://airbnb.io/enzyme/docs/api/ReactWrapper/props.html
.props() => Object
Returns the props object for the root node of the wrapper. It must be
a single-node wrapper. This method is a reliable way of accessing the
props of a node; wrapper.instance().props will work as well, but in
React 16+, stateless functional components do not have an instance.
See .instance() => ReactComponent
Does anyone know how to test this in a good way to see that the logic is working as expected without exporting the private mapStateToProps function directly?
Component
import React from 'react';
import PropTypes from 'prop-types';
import { connect } from 'react-redux';
// Component 1 - "Base component"
// Exporting it is a good practice for testing its own logic
export const Dice = ({ lastRolledNumber, onRollDice }) => (
<div>
<p>The last rolled number was {lastRolledNumber}.</p>
<button onClick={onRollDice}>Roll dice</button>
</div>
);
Dice.propTypes = {
lastRolledNumber: PropTypes.number.isRequired,
onRollDice: PropTypes.func.isRequired
}
const mapStateToProps = (state) => ({
lastRolledNumber: state.lastRolledNumber
});
const mapDispatchToProps = (dispatch) => ({
onRollDice: () => dispatch({ type: 'ROLL_DICE' })
});
// Component 2 - Container component
// Export it as a default export
export default connect(mapStateToProps, mapDispatchToProps)(Dice);
Test
import React from 'react';
import { shallow } from 'enzyme';
import '../test-config'; // Setup Enzyme & Adapter
import DiceContainer from './Dice';
// Create the mock store
import configureMockStore from 'redux-mock-store';
const mockStore = configureMockStore();
describe('Dice', () => {
let wrapper, store;
beforeEach(() => {
const initialState = {
lastRolledNumber: 1
};
store = mockStore(initialState);
// Shallow render the container passing in the mock store
wrapper = shallow(
<DiceContainer store={store} />
);
});
it('should show previously rolled value', () => {
// test that the state values were correctly passed as props
expect(wrapper.props().lastRolledNumber).toBe(1);
});
it('should roll the dice again when button is clicked', () => {
// test that the component events dispatch the expected actions
wrapper.simulate('rollDice');
const actions = store.getActions();
expect(actions).toEqual([ { type: 'ROLL_DICE' } ]);
});
});
You are almost there. You need to call .dive() method so that you can get the Dice component rather than the DiceContainer component which is wrapped by the connect HOC of react-redux module.
Short answer:
wrapper = shallow(<DiceContainer store={store} />).dive();
A Completed working example:
index.jsx:
import React from 'react';
import PropTypes from 'prop-types';
import { connect } from 'react-redux';
export const Dice = ({ lastRolledNumber, onRollDice }) => (
<div>
<p>The last rolled number was {lastRolledNumber}.</p>
<button onClick={onRollDice}>Roll dice</button>
</div>
);
Dice.propTypes = {
lastRolledNumber: PropTypes.number.isRequired,
onRollDice: PropTypes.func.isRequired,
};
const mapStateToProps = (state) => ({
lastRolledNumber: state.lastRolledNumber,
});
const mapDispatchToProps = (dispatch) => ({
onRollDice: () => dispatch({ type: 'ROLL_DICE' }),
});
export default connect(mapStateToProps, mapDispatchToProps)(Dice);
index.test.jsx:
import React from 'react';
import { shallow } from 'enzyme';
import configureMockStore from 'redux-mock-store';
import DiceContainer from '.';
const mockStore = configureMockStore();
describe('Dice', () => {
let wrapper;
let store;
beforeEach(() => {
const initialState = {
lastRolledNumber: 1,
};
store = mockStore(initialState);
wrapper = shallow(<DiceContainer store={store} />).dive();
});
it('should show previously rolled value', () => {
expect(wrapper.props().lastRolledNumber).toBe(1);
});
it('should roll the dice again when button is clicked', () => {
wrapper.simulate('rollDice');
const actions = store.getActions();
expect(actions).toEqual([{ type: 'ROLL_DICE' }]);
});
});
Unit test results:
PASS src/stackoverflow/59771991/index.test.jsx (9.645s)
Dice
✓ should show previously rolled value (19ms)
✓ should roll the dice again when button is clicked (2ms)
Test Suites: 1 passed, 1 total
Tests: 2 passed, 2 total
Snapshots: 0 total
Time: 11.505s
Test coverage html report:
In my case, I was able to write the test with render by this -
import React from 'react';
import '#testing-library/jest-dom';
import {
screen, cleanup, render,
} from '#testing-library/react';
import { Provider } from 'react-redux';
import configureMockStore from 'redux-mock-store';
const store = configureMockStore()({
headerState: {
showBack: false,
},
});
const mockProps = { ... };
describe('Component', () => {
beforeAll(() => {
render(
<Provider store={store}>
<Component {...mockProps} />
</Provider>,
);
});
afterAll(cleanup);
test('Test', () => {
screen.debug();
});
});

How to pass custom props to Single SPA child React apps?

I want to start my React microapp with props I'm passing from Single SPA (customProps). The only way I've figured out is:
import React from 'react';
import ReactDOM from 'react-dom';
import singleSpaReact from 'single-spa-react';
import App from './where/my/root/is.js';
function domElementGetter() {
return document.getElementById("mounting-node")
}
let EnhancedRootComponent = App; /* 1 */
const reactLifecycles = singleSpaReact({
React,
ReactDOM,
rootComponent: EnhancedRootComponent, /* 1 */
domElementGetter,
})
export const bootstrap = [
(args) => {
/* 2 */ EnhancedRootComponent = () => <App myArgs={args.thePropsIWannaPass} />;
return Promise.resolve();
},
reactLifecycles.bootstrap,
];
export const mount = [reactLifecycles.mount];
export const unmount = [reactLifecycles.unmount];
This does work (I can see and use the passed props in my component) but I'm not completely OK with the fact that the root component changes in between calling singleSpaReact (1) and calling bootstrap(2). Would there be side effects to this that I'm not seeing now? Does anyone know a better approach for this?
You have this value inside the props variable without this reassign.
Check this out:
Root-config.js, file responsible for passing prop to microfrontend
import { registerApplication, start } from 'single-spa';
import * as isActive from './activity-functions';
registerApplication('#company/micro2', () => System.import('#company/micro2'), isActive.micro2);
registerApplication('#company/micro1', () => System.import('#company/micro1'), isActive.micro1, { "authToken": "test" });
start();
micro1 Root.tsx
import React from 'react';
export default class Root extends React.Component {
constructor(props: any){
super(props)
}
state = {
hasError: false,
};
componentDidCatch() {
this.setState({ hasError: true });
}
render() {
console.log(this.props)
return (
<div>test</div>
);
}
}
console.log output:
props:
authToken: "test" <---- props which you pass
name: "#company/micro1"
mountParcel: ƒ ()
singleSpa: {…}
__proto__: Object
for more advance usage
const lifecycles = singleSpaReact({
React,
ReactDOM,
loadRootComponent: (props) =>
new Promise((resolve, reject) => resolve(() =>
<Root {...props} test2={'test2'}/>)),
domElementGetter,
});

Test React Component using Redux and react-testing-library

I am new to testing redux connected components in React and trying to figure out how to test them.
Currently I'm using react-testing-library and having trouble setting up the my renderWithRedux function to correctly setup redux.
Here is a sample component:
import React, { Component } from 'react'
import { connect } from 'react-redux'
class Sample extends Component {
constructor(props) {
super(props);
this.state = {
...
}
}
componentDidMount() {
//do stuff
console.log(this.props)
}
render() {
const { user } = this.props
return(
<div className="sample">
{user.name}
</div>
)
}
}
const mapStateToProps = state => ({
user: state.user
})
export default connect(mapStateToProps, {})(Sample);
Here is a sample test:
import React from 'react';
import { createStore } from 'redux'
import { Provider } from 'react-redux'
import { render, cleanup } from 'react-testing-library';
import Sample from '../components/sample/'
const user = {
id: 1,
name: "John Smith"
}}
function reducer(state = user, action) {
//dont need any actions at the moment
switch (action.type) {
default:
return state
}
}
function renderWithRedux(
ui,
{ initialState, store = createStore(reducer, initialState) } = {}
) {
return {
...render(<Provider store={store}>{ui}</Provider>),
store,
}
}
afterEach(cleanup)
test('<Sample> example text', () => {
const { getByTestId, getByLabelText } = renderWithRedux(<Sample />)
expect(getByText(user.name))
})
The user prop value always results as undefined. I have re-written this a couple of ways but can't seem to get it to work. If I pass the user data directly as a prop to Sample component in the test, it still resolves to be undefined.
I am learning from the tutorials and examples via the offical docs, like this one: https://github.com/kentcdodds/react-testing-library/blob/master/examples/tests/react-redux.js
Any pointers, tips or solutions would be greatly appreciated!
You should wrap the component inside Provider, here is the simple example
import React from 'react';
import { render } from '#testing-library/react';
import '#testing-library/jest-dom';
import { Provider } from "react-redux";
import configureMockStore from "redux-mock-store";
import TestedComponent from '../index';
const mockStore = configureMockStore();
const store = mockStore({});
const renderTestedComponent = () => {
return render(
<Provider store={store}>
<TestedComponent />
</Provider>
);
};
describe('test TestedComponent components', () => {
it('should be render the component correctly', () => {
const { container } = renderTestedComponent();
expect(container).toBeInTheDocument();
});
});
**Unable to Fire event using #testing-library**
// demo.test.js
import React from 'react'
import { Provider } from "react-redux";
import '#testing-library/react/cleanup-after-each'
import '#testing-library/jest-dom/extend-expect'
import { render, fireEvent } from '#testing-library/react'
// this is used to fire the event
// import userEvent from "#testing-library/user-event";
//import 'jest-localstorage-mock';
import ChangePassword from './ChangePassword';
import configureMockStore from 'redux-mock-store';
import thunk from 'redux-thunk';
const middlewares = [thunk];
const mockStore = configureMockStore(middlewares);
test('test 1-> Update User password', () => {
// global store
const getState = {
authUser :{
user : {
email: "test#gmail.com",
id: 0,
imageURL: null,
name: "test Solutions",
roleId: 1,
roleName: "testRole",
userName: "testUserName"
},
loading: false,
showErrorMessage: false,
errorDescription: ""
}
}; // initial state of the store
// const action = { type: 'LOGIN_USER' };
// const expectedActions = [action];
// const store = mockStore(getState, expectedActions);
const onSaveChanges = jest.fn();
const changePassword = jest.fn();
const store = mockStore(getState);
const { queryByText, getByLabelText, getByText , getByTestId , getByPlaceholderText, } = render(
<Provider store={store}>
<ChangePassword
onSaveChanges={onSaveChanges}
changePassword={changePassword}
/>
</Provider>,
)
// test 1. check the title of component
expect(getByTestId('updateTitle')).toHaveTextContent('Update your password');
// test 2. chekck the inputfile
expect(getByPlaceholderText('Old Password')) //oldpassword
expect(getByPlaceholderText('New Password')) //newpassword
expect(getByPlaceholderText('Confirm New Password')) //confpassword
// change the input values
fireEvent.change(getByPlaceholderText("Old Password"), {
target: { value: "theOldPasword" }
});
fireEvent.change(getByPlaceholderText("New Password"), {
target: { value: "#Ab123456" }
});
fireEvent.change(getByPlaceholderText("Confirm New Password"), {
target: { value: "#Ab123456" }
});
// check the changed input values
expect(getByPlaceholderText('Old Password').value).toEqual("theOldPasword");
expect(getByPlaceholderText('New Password').value).toEqual("#Ab123456");
expect(getByPlaceholderText('Confirm New Password').value).toEqual("#Ab123456");
expect(getByText('Save Changes')); // check the save change button
// calling onSave function
//fireEvent.click(getByTestId('savechange'))
// userEvent.click(getByText('Save Changes'));
})

Test connected component in React/Redux

I am trying test my connected component of my React/Redux app and I wrote some test case which actually throws the error:
App component › shows account info and debits and credits`
Invariant Violation: Could not find "store" in either the context or props of "Connect(AccountInfo)". Either wrap the root component in a <Provider>, or explicitly pass "store" as a prop to "Connect(AccountInfo)".
The test case which trow an error app.test.js is below. And my problem is that I don't understand what should I wrap here by Connect() because I didn't use AccountInfo here:
import React from 'react';
import { mount } from 'enzyme';
import { Provider } from 'react-redux';
import App from './App';
import * as actions from '../../actions';
function setup() {
const props = {
errorMessage: null,
actions
};
const enzymeWrapper = mount(<App {...props} />);
return {
props,
enzymeWrapper,
};
}
describe('App component', () => {
it('shows account info and debits and credits`', () => {
const {enzymeWrapper} = setup();
expect(enzymeWrapper.find('.account-info').exists()).toBe(true);
expect(enzymeWrapper.find('.debits-and-credits').exists()).toBe(true);
});
it('shows error message', () => {
const {enzymeWrapper} = setup();
enzymeWrapper.setProps({ errorMessage: 'Service Unavailable' });
expect(enzymeWrapper.find('.error-message').exists()).toBe(true);
});
});
My containers/app.js:
import { connect } from 'react-redux';
import { bindActionCreators } from 'redux';
import * as actions from '../actions';
import AppComponent from '../components/App/App';
const mapStateToProps = state => ({
isFetching: state.balance.isFetching,
errorMessage: state.errorMessage,
});
const mapDispatchToProps = dispatch => ({
actions: bindActionCreators(actions, dispatch),
});
const AppContainer = connect(mapStateToProps, mapDispatchToProps)(AppComponent);
export default AppContainer;
The component app.js:
import React, { Component } from 'react';
import ErrorMessage from '../../containers/ErrorMessage';
import AccountInfo from '../../containers/AccountInfo';
import DebitsAndCredits from '../../containers/DebitsAndCredits';
import './App.css';
const AppComponent = () =>
<div className="app">
<AccountInfo />
<DebitsAndCredits />
</div>;
export class App extends Component {
componentWillMount() {
const { actions } = this.props;
actions.fetchBalance();
}
render() {
const { errorMessage } = this.props;
return errorMessage ? <ErrorMessage /> : <AppComponent />;
}
}
export default App;
UPD:
I updated my test case and now it looks like:
import React from 'react';
import { mount } from 'enzyme';
import { Provider } from 'react-redux';
import configureMockStore from 'redux-mock-store';
import createSagaMiddleware from 'redux-saga';
import { initialState } from '../../reducers/balance/balance';
import App from './App';
import * as actions from '../../actions';
const middlewares = [createSagaMiddleware];
const mockStore = configureMockStore(middlewares);
const store = mockStore(initialState);
function setup() {
const props = {
errorMessage: null,
actions,
};
const enzymeWrapper = mount(
<Provider store={store}>
<App {...props} />
</Provider>
);
return {
props,
enzymeWrapper,
};
}
describe('App component', () => {
it('shows account info and debits and credits`', () => {
const { enzymeWrapper } = setup();
expect(enzymeWrapper.find('.account-info').exists()).toBe(true);
expect(enzymeWrapper.find('.debits-and-credits').exists()).toBe(true);
});
it('shows error message', () => {
const { enzymeWrapper } = setup();
enzymeWrapper.setProps({ errorMessage: 'Service Unavailable' });
expect(enzymeWrapper.find('.error-message').exists()).toBe(true);
});
});
And my error now is:
App component › shows account info and debits and credits`
TypeError: Cannot read property 'account' of undefined
UPD 2:
My initialState which I put when I create mocked store:
const initialState = {
isFetching: false,
account: {},
currency: '',
debitsAndCredits: [],
};
My AccountInfo component:
import React from 'react';
const AccountInfo = ({ account, currency }) =>
<header className="account-info">
<p>{account.name}</p>
<p>
IBAN: {account.iban}<br />
Balance: {account.balance}<br />
Currency: {currency}<br />
</p>
</header>;
export default AccountInfo;
For testing the connected component, you need to mock the provider as well, since the connect picks state variables from redux store.
Do this
const enzymeWrapper = mount (<Provider store={mockStore}><App {...props}/></Provider>)
You need to mock the redux store too.
Edit 1:
Just looking at your AccountInfo component it tells me that you are expecting account in the props here.
AccountInfo = ({account}) =>
So that means App.js has to pass down the accounts' value in the props. Same thing goes for currency.

TypeError: Cannot read property 'pathname' of undefined in react/redux testing

I'm testing some react components, a basic tests suite just to know if a component is rendering and their childs.
I'm using redux-mock-store to make the store and {mount} enzyme to mount the container in a provider, but even mocking the correct store this error is always fired:
TypeError: Cannot read property 'pathname' of undefined
Here is my very deadly basic test:
import React from 'react';
import { mount } from 'enzyme';
import configureStore from 'redux-mock-store';
import { Provider } from 'react-redux';
import App from '../containers/App.container';
describe('App', () => {
let wrapper;
const mockStore = configureStore([]);
const store = mockStore({
router: {
location: { pathname: '/home', query: {}, search: '' },
params: {}
}
});
console.log(store.getState());
beforeEach(() => {
wrapper = mount(
<Provider store={store}>
<App />
</Provider>
);
});
it('Should render app and container elements', () => {
expect(wrapper.find('.app').exists()).toBeTruthy();
expect(wrapper.find('.container').exists()).toBeTruthy();
});
it('Should render the navbar', () => {
expect(wrapper.find('nav').exists()).toBeTruthy();
});
});
And the (even more) simple component / container:
import React, { Component } from 'react';
import NavBar from '../components/Navbar';
class App extends Component {
render() {
const { location, logout} = this.props;
console.log(location);
return (
<section className='app'>
<NavBar location={location.pathname} onLogoutClick={logout}/>
<div className='container'>
{this.props.children}
</div>
</section>
);
}
}
export default App;
Container:
import { connect } from 'react-redux';
import { signOut } from '../actions/auth.actions'
import App from '../components/App';
const mapStateToProps = (state, ownProps) => {
return {
location: ownProps.location
}
}
const mapDispatchToProps = (dispatch, ownProps) => {
return {
logout: () => {
dispatch(signOut())
}
}
};
export default connect(mapStateToProps, mapDispatchToProps)(App);
I can't figure out the problem of the test, the mockStore is in the correct format:
Any idea?
Update:
Thinking about it, I have no reducer / prop in the rootReducer for the location, but, i just want to pass down through the children components the location object properties that react-redux-router make available in the ownProps argument.
Weird fact: logging the location property in the app returns me the correct object.
In the tests, is always undefined... (as the error shows).
Here is my rootReducer:
import { combineReducers } from 'redux';
import { reducer as formReducer } from 'redux-form';
import { routerReducer } from 'react-router-redux';
import authReducer from './auth.reducer';
import analysisReportsReducer from './AnalysisReports.reducer';
import titleAnalysisReducer from './TitleAnalysis.reducer';
import postsReportsReducer from './PostsReports.reducer';
const rootReducer = combineReducers({
form: formReducer,
routing: routerReducer,
auth: authReducer,
analysis: titleAnalysisReducer,
analysis_reports: analysisReportsReducer,
posts: postsReportsReducer
});
export default rootReducer;
It looks like your location object is scoped beneath the router.
Your test may be grabbing the window.location property, which your test suite may not replicate, assuming the test is cli and not in a browser.
Perhaps try:
<NavBar location={this.props.router.location.pathname} onLogoutClick={logout}/>

Categories

Resources