React Context testing - mocking Consumer in HOC - javascript

I am trying to test my component which is consuming data from context via HOC.
Here is setup:
Mocked context module /context/__mocks__
const context = { navOpen: false, toggleNav: jest.fn() }
export const AppContext = ({
Consumer(props) {
return props.children(context)
}
})
Higher OrderComponent /context/withAppContext
import React from 'react'
import { AppContext } from './AppContext.js'
/**
* HOC with Context Consumer
* #param {Component} Component
*/
const withAppContext = (Component) => (props) => (
<AppContext.Consumer>
{state => <Component {...props} {...state}/>}
</AppContext.Consumer>
)
export default withAppContext
Component NavToggle
import React from 'react'
import withAppContext from '../../../context/withAppContext'
import css from './navToggle/navToggle.scss'
const NavToggle = ({ toggleNav, navOpen }) => (
<div className={[css.navBtn, navOpen ? css.active : null].join(' ')} onClick={toggleNav}>
<span />
<span />
<span />
</div>
)
export default withAppContext(NavToggle)
And finally Test suite /navToggle/navToggle.test
import React from 'react'
import { mount } from 'enzyme'
beforeEach(() => {
jest.resetModules()
})
jest.mock('../../../../context/AppContext')
describe('<NavToggle/>', () => {
it('Matches snapshot with default context', () => {
const NavToggle = require('../NavToggle')
const component = mount( <NavToggle/> )
expect(component).toMatchSnapshot()
})
})
Test is just to get going, but I am facing this error:
Warning: Failed prop type: Component must be a valid element type!
in WrapperComponent
Which I believe is problem with HOC, should I mock that somehow instead of the AppContext, because technically AppContext is not called directly by NavToggle component but is called in wrapping component.
Thanks in advance for any input.

So I solved it.
There were few issues with my attempt above.
require does not understand default export unless you specify it
mounting blank component returned error
mocking AppContext with __mock__ file caused problem when I wanted to modify context for test
I have solved it following way.
I created helper function mocking AppContext with custom context as parameter
export const defaultContext = { navOpen: false, toggleNav: jest.fn(), closeNav: jest.fn(), path: '/' }
const setMockAppContext = (context = defaultContext) => {
return jest.doMock('../context/AppContext', () => ({
AppContext: {
Consumer: (props) => props.children(context)
}
}))
}
export default setMockAppContext
And then test file ended looking like this
import React from 'react'
import { shallow } from 'enzyme'
import NavToggle from '../NavToggle'
import setMockAppContext, { defaultContext } from '../../../../testUtils/setMockAppContext'
beforeEach(() => {
jest.resetModules()
})
describe('<NavToggle/>', () => {
//...
it('Should have active class if context.navOpen is true', () => {
setMockAppContext({...defaultContext, navOpen: true})
const NavToggle = require('../NavToggle').default //here needed to specify default export
const component = shallow(<NavToggle/>)
expect(component.dive().dive().hasClass('active')).toBe(true) //while shallow, I needed to dive deeper in component because of wrapping HOC
})
//...
})
Another approach would be to export the component twice, once as decorated with HOC and once as clean component and create test on it, just testing behavior with different props. And then test just HOC as unit that it actually passes correct props to any wrapped component.
I wanted to avoid this solution because I didn't want to modify project file(even if it's just one word) just to accommodate the tests.

Related

React: Class Based Context API to Hook Based context API

I am really new in Hooks and during learning faces many difficulties to switch from the old style.
My old code looks like:
context.js
import React from "react";
const SomeContext = React.createContext(null);
export const withContext = (Component) => (props) => (
<SomeContext.Consumer>
{(server) => <Component {...props} server={server} />}
</SomeContext.Consumer>
);
export default SomeContext;
main index.js
<SomeContext.Provider value={new SomeClass()}>
<App />
</SomeContext.Provider>
but when I want to access it through with export default withContext(SomeComponent) by this.props.server.someFunc() it showed props is undefined in the classless hook function.
how can I achieve this.props in react hook
Edit:
SomeClass is not React inheriting class and its look like it.
class SomeClass {
someFunc = (id) => axios('api endpoints')
}
SomeComponent
const SomeComponent = () => {
...
useEffect(() => {
this.props.server.someFunc(id).then(...).catch(...)
}, ...)
...
}
In the regular case, you need to export the Context, then import it and use it within useContext:
export const SomeContext = React.createContext(null);
// Usage
import { SomeContext } from '..';
const SomeComponent = () => {
const server = useContext(SomeContext);
useEffect(() => {
server.someFunc(id);
});
};
But in your case, since you using HOC, you have the server within the prop it self:
const SomeComponent = (props) => {
useEffect(() => {
props.server.someFunc(id);
});
};

"TypeError: dispatch is not a function" when using useReducer/useContext and React-Testing-Library

I'm having issues testing my components that use dispatch via useReducer with React-testing-library.
I created a less complex example to try to boil down what is going on and that is still having the same dispatch is not a function problem. When I run my tests, I am getting this error:
11 | data-testid="jared-test-button"
12 | onClick={() => {
> 13 | dispatch({ type: 'SWITCH' })
| ^
14 | }}
15 | >
16 | Click Me
Also, if I do a console.log(typeof dispatch) inside RandomButton, and I click on the button the output says function.
Here is the test in question.
import React from 'react'
import RandomButton from '../RandomButton'
import { render, fireEvent } from '#testing-library/react'
describe('Button Render', () => {
it('click button', () => {
const { getByTestId, queryByText } = render(<RandomButton />)
expect(getByTestId('jared-test-button')).toBeInTheDocument()
fireEvent.click(getByTestId('jared-test-button'))
expect(queryByText('My name is frog')).toBeInTheDocument()
})
})
Here is my relevant code:
RandomButton.js
import React, { useContext } from 'react'
import MyContext from 'contexts/MyContext'
const RandomButton = () => {
const { dispatch } = useContext(MyContext)
return (
<div>
<Button
data-testid="jared-test-button"
onClick={() => {
dispatch({ type: 'SWITCH' })
}}
>
Click Me
</Button>
</div>
)
}
export default RandomButton
MyApp.js
import React, { useReducer } from 'react'
import {myreducer} from './MyFunctions'
import MyContext from 'contexts/MyContext'
import RandomButton from './RandomButton'
const initialState = {
blue: false,
}
const [{ blue },dispatch] = useReducer(myreducer, initialState)
return (
<MyContext.Provider value={{ dispatch }}>
<div>
{blue && <div>My name is frog</div>}
<RandomButton />
</div>
</MyContext.Provider>
)
export default MyApp
MyFunctions.js
export const myreducer = (state, action) => {
switch (action.type) {
case 'SWITCH':
return { ...state, blue: !state.blue }
default:
return state
}
}
MyContext.js
import React from 'react'
const MyContext = React.createContext({})
export default MyContext
It is probably something stupid that I am missing, but after reading the docs and looking at other examples online I'm not seeing the solution.
I've not tested redux hooks with react-testing-library, but I do know you'll have to provide a wrapper to the render function that provides the Provider with dispatch function.
Here's an example I use to test components connected to a redux store:
testUtils.js
import React from 'react';
import { createStore } from 'redux';
import { render } from '#testing-library/react';
import { Provider } from 'react-redux';
import reducer from '../reducers';
// https://testing-library.com/docs/example-react-redux
export const renderWithRedux = (
ui,
{ initialState, store = createStore(reducer, initialState) } = {},
options,
) => ({
...render(<Provider store={store}>{ui}</Provider>, options),
store,
});
So, based upon what you've shared I think the wrapper you'd want would look something like this:
import React from 'react';
import MyContext from 'contexts/MyContext';
// export so you can test that it was called with specific arguments
export dispatchMock = jest.fn();
export ProviderWrapper = ({ children }) => (
// place your mock dispatch function in the provider
<MyContext.Provider value={{ dispatch: dispatchMock }}>
{children}
</MyContext.Provider>
);
and in your test:
import React from 'react';
import RandomButton from '../RandomButton';
import { render, fireEvent } from '#testing-library/react';
import { ProviderWrapper, dispatchMock } from './testUtils';
describe('Button Render', () => {
it('click button', () => {
const { getByTestId, queryByText } = render(
<RandomButton />,
{ wrapper: ProviderWrapper }, // Specify your wrapper here
);
expect(getByTestId('jared-test-button')).toBeInTheDocument();
fireEvent.click(getByTestId('jared-test-button'));
// expect(queryByText('My name is frog')).toBeInTheDocument(); // won't work since this text is part of the parent component
// If you wanted to test that the dispatch was called correctly
expect(dispatchMock).toHaveBeenCalledWith({ type: 'SWITCH' });
})
})
Like I said, I've not had to specifically test redux hooks but I believe this should get you to a good place.

How to test a connected component wrapped in another connected component?

I want to test my pure component so I'm doing this:
MyComp.js
export MyComp = props => {
return (
<Wrapper>Content</Wrapper>
)
}
const MyCompConn = connect()(MyComp);
export default MyCompConn;
Where <Wrapper>:
export Wrapper = ({children}) => {
return (
<div>{children}</div>
)
}
const WrapperConn = connect()(Wrapper);
export default WrapperConn;
MyComp.test.js
import React from 'react';
import renderer from 'react-test-renderer';
import { MyComp } from '../../MyComp';
describe('With Snapshot Testing', () => {
it('renders!"', () => {
const component = renderer.create(<Login />);
const tree = component.toJSON();
expect(tree).toMatchSnapshot();
});
});
Now, when I run yarn test I get this error:
Invariant Violation: Could not find "store" in either the context or props of "Connect(AppWrapper)". Either wrap the root component in a <Provider> or explicitly pass "store" as a prop to "Connect(AppWrapper)"
And this is happening because <Wrapper> is connected in my <MyComp> component, but I'm not sure how to test just the latter even if it's wrapped on a connected component.
To test our component without using mocked store, we can mock the connect of react-redux itself using Jest. PFB the example:
import React from 'react';
import renderer from 'react-test-renderer';
import { MyComp } from '../../MyComp';
jest.mock('react-redux', () => ({
connect: () => {
return (component) => {
return component
};
}
}));
describe('With Snapshot Testing', () => {
it('renders!"', () => {
const component = renderer.create(<Login />);
const tree = component.toJSON();
expect(tree).toMatchSnapshot();
});
});
Now, this will render the Wrapper component directly, and not the connected Wrapper component.
Try this:
import configureMockStore from 'redux-mock-store';
const store = mockStore({});
const component = renderer.create(<Provider store={store}><Login /></Provider>);

Print value from props, which is delivered to the component from redux by mapStateToProps

Problem:
I can't display the value from the state of redux, which is delivered by mapStateToProps function to the component.
Project structure:
Create-react-app CLi application built the project.
Inside of the src/ I have the following code structure
Necessary code:
The main page which we are interacting with looks like this:
Underneath it is planned to post the result of the clicking on the buttons.
So how do I bind the redux state and actions to those two components: Calculator and ResultLine?
Let me show the index.js code, where I create the store:
import React from "react";
import ReactDOM from "react-dom";
import { Provider } from "react-redux";
import { createStore } from "redux";
import reducers from './reducers/';
import App from './components/App';
ReactDOM.render(
<Provider store={createStore(reducers)}>
<App />
</Provider>,
document.getElementById("root")
);
There are only three actions:
import {CALCULATE, ERASE, PUT_SYMBOL} from "./types";
export const putSymbol = (symbol) => {
return {
type: PUT_SYMBOL,
payload: symbol
}
};
export const calculate = () => {
return {
type: CALCULATE
}
};
export const erase = () => {
return {
type: ERASE
}
};
And in the App.js I pass reducers, which are binded to those actions to the Calculator component:
import React, {Component} from 'react';
import Calculator from './Calculator';
import ResultLine from "./ResultLine";
import {calculate, erase, putSymbol} from "../actions/index";
import {connect} from "react-redux";
class App extends Component {
render() {
return (
<div>
<Calculator
onSymbolClick={this.props.onSymbolClick}
onEqualsClick={this.props.onEqualsClick}
onEraseClick={this.props.onEraseClick}/>
<br/>
<ResultLine result={this.props.result}/>
</div>
);
}
}
const mapStateToProps = (state) => {
console.log('mapState', state.calc.line);
return {
result: state.line
}
};
const mapDispatchToProps = {
onSymbolClick: putSymbol,
onEqualsClick: calculate,
onEraseClick: erase
};
export default connect(mapStateToProps, mapDispatchToProps)(App);
And that works fine. Whenever I click the button the state changes, and I observe it in the console log, called in mapStateToProps function.
So I expect, that I can deliver result prop to the Result line easily, and I pass it into the ResultLine component as a parameter. So, let's look at that element:
import React from 'react';
const ResultLine = ({result}) => {
return (
<p>{result}</p>
);
};
export default ResultLine;
And I can see no changes in a result line. Maybe, something wrong with the React/Redux lifecycle management and ResultLine component just does not update on changes in state?
There's an error on mapStateToProps.
Instead of:
const mapStateToProps = (state) => {
return {
result: state.line
}
}
Please use:
const mapStateToProps = (state) => {
return {
result: state.calc.line // calc was missing here
}
}

Why is this Jest/Enzyme setState test failing for my React app?

Expected:
Test runs and state is updated in the Login component, when then enables the Notification component (error message) to be found
Results:
Test fails, expected 1, received 0
Originally before I added redux and the store, thus needing to use the store and provider logic in my test, this Jest/Enzyme tests were passing.
The Login.test (updated current version)
import React from 'react'
import { Provider } from "react-redux"
import ReactTestUtils from 'react-dom/test-utils'
import { createCommonStore } from "../../store";
import { mount, shallow } from 'enzyme'
import toJson from 'enzyme-to-json'
import { missingLogin } from '../../consts/errors'
// import Login from './Login'
import { LoginContainer } from './Login';
import Notification from '../common/Notification'
const store = createCommonStore();
const user = {
id: 1,
role: 'Admin',
username: 'leongaban'
};
const loginComponent = mount(
<Provider store={store}>
<LoginContainer/>
</Provider>
);
const fakeEvent = { preventDefault: () => '' };
describe('<Login /> component', () => {
it('should render', () => {
const tree = toJson(loginComponent);
expect(tree).toMatchSnapshot();
});
it('should render the Notification component if state.error is true', () => {
loginComponent.setState({ error: true });
expect(loginComponent.find(Notification).length).toBe(1);
});
});
Login.test (previous passing version, but without the Redux store logic)
import React from 'react'
import ReactTestUtils from 'react-dom/test-utils'
import { mount, shallow } from 'enzyme'
import toJson from 'enzyme-to-json'
import { missingLogin } from '../../consts/errors'
import Login from './Login'
import Notification from '../common/Notification'
const loginComponent = shallow(<Login />);
const fakeEvent = { preventDefault: () => '' };
describe('<Login /> component', () => {
it('should render', () => {
const tree = toJson(loginComponent);
expect(tree).toMatchSnapshot();
});
it('should render the Notification component if state.error is true', () => {
loginComponent.setState({ error: true });
expect(loginComponent.find(Notification).length).toBe(1);
});
});
Your problem is that by mixing the redux store logic into the tests, the loginComponent variable no longer represents an instance of Login, but an instance of Provider wrapping and instance of Login.
Thus when you do this
loginComponent.setState({ error: true })
You're actually calling setState on the Provider instance.
I would recommend testing the LoginComponent you've wrapped with connect to produce LoginContainer separately from the store state. The Redux GitHub repo has a great article on testing connected components, which gives a general outline on how to do this.
To summarize what you need to do
Export both LoginComponent and LoginContainer separately
Test LoginComponent individually from the container, essentially doing what your previous working tests before mixing in redux store state did.
Write separate tests for LoginContainer where you test the mapStateToProps, mapDispatchToProps and mergeProps functionality.
Hope this helps!

Categories