I've been working around with unit tests using jest in React and HOC. Currently, I am facing an issue in accessing the state and method of my class. Check the sample code below
//Login.Container.js
import { connect } from 'react-redux';
import { withRouter } from 'react-router-dom';
class Login extends Component {
constructor(props) {
super(props);
this.state = {
isShowLoader: false,
};
this.handleSubmit = this.handleSubmit.bind(this);
}
handleSubmit() {
//sample code
}
render() {
return (
<LoginComponent />
);
}
function mapStateToProps(state) {
return {
task: state
};
}
function mapDispatchToProps(dispatch) {
return bindActionCreators(
Object.assign({},
actions),
dispatch);
}
export default withRouter(connect(mapStateToProps, mapDispatchToProps)(Login));
Login.displayName = 'Login';
Sample login component
//Login.Component.js
const LoginComponent = props => {
return (<div>hi</div>);
}
My sample test suite using jest and enzyme
//login.test.js
import { Provider } from 'react-redux';
import { history, store} from '../store';
import { ConnectedRouter } from 'react-router-redux';
describe('>>>Login --- Container', () => {
let wrapperInner
it('should perform login container by using ComponentWapper', async () => {
wrapperInner = mount(<Provider store={store}>
<ConnectedRouter history={history}>
<Login />
</ConnectedRouter>
</Provider>);
const instance = wrapperInner.instance();
expect(wrapperInner.state('isShowLoader')).toBe(true);
const responseJson = await instance.handleSubmit();
});
});
Finally, I found the solution for this issue.
wrapperInner.find("Login").instance() // to access the methods
wrapperInner.find("Login").instance().state // to access the state
For more details check here: https://github.com/airbnb/enzyme/issues/361
Related
I learn ReactJs and have a design Composition question about ReactJs higher order component (HOC).
In the code below App.jsx I use this withAuthentication HOC that initializes app core processes. This HOC value is not used in the App.js. Therefore I must suppress all withAuthentication HOC render callbaks and I do that in the shouldComponentUpdate by returning false.
(I use this HOC in many other places to the get HOC's value but not in App.jsx)
File App.jsx:
import React, { Component } from 'react';
import { connect } from 'react-redux';
import { compose } from 'recompose';
import { getAlbumData } from './redux/albumData/albumData.actions';
import { getMetaData } from './redux/albumMetaData/albumMetaData.actions';
import Header from './components/structure/Header';
import Content from './components/structure/Content';
import Footer from './components/structure/Footer';
import { withAuthentication } from './session';
import './styles/index.css';
class App extends Component {
componentDidMount() {
const { getMeta, getAlbum } = this.props;
getMeta();
getAlbum();
}
shouldComponentUpdate() {
// suppress render for now boilerplate, since withAuthentication
// wrapper is only used for initialization. App don't need the value
return false;
}
render() {
return (
<div>
<Header />
<Content />
<Footer />
</div>
);
}
}
const mapDispatchToProps = dispatch => ({
getMeta: () => dispatch(getMetaData()),
getAlbum: () => dispatch(getAlbumData()),
});
export default compose(connect(null, mapDispatchToProps), withAuthentication)(App);
The HOC rwapper WithAuthentication below is a standard HOC that render Component(App) when changes are made to Firebase user Document, like user-role changes, user auth-state changes..
File WithAuthentication .jsx
import React from 'react';
import { connect } from 'react-redux';
import { compose } from 'recompose';
import AuthUserContext from './context';
import { withFirebase } from '../firebase';
import * as ROLES from '../constants/roles';
import { setCurrentUser, startUserListener } from '../redux/userData/user.actions';
import { selectUserSlice } from '../redux/userData/user.selectors';
const WithAuthentication = Component => {
class withAuthentication extends React.Component {
constructor() {
super();
this.state = {
authUser: JSON.parse(localStorage.getItem('authUser')),
};
}
componentDidMount() {
const { firebase, setUser, startUserListen } = this.props;
this.authListener = firebase.onAuthUserListener(
authUser => {
this.setState({ authUser });
setUser(authUser);
startUserListen();
},
() => {
localStorage.removeItem('authUser');
this.setState({ authUser: null });
const roles = [];
roles.push(ROLES.ANON);
firebase
.doSignInAnonymously()
.then(authUser => {
if (process.env.NODE_ENV !== 'production')
console.log(`Sucessfully signed in to Firebase Anonymously with UID: ${firebase.getCurrentUserUid()}`);
firebase.doLogEvent('login', { method: 'Anonymous' });
firebase
.userDoc(authUser.user.uid)
.set({
displayName: `User-${authUser.user.uid.substring(0, 6)}`,
roles,
date: firebase.fieldValue.serverTimestamp(),
})
.then(() => {
console.log('New user saved to Firestore!');
})
.catch(error => {
console.log(`Could not save user to Firestore! ${error.code}`);
});
})
.catch(error => {
console.error(`Failed to sign in to Firebase: ${error.code} - ${error.message}`);
});
},
);
}
componentWillUnmount() {
this.authListener();
}
render() {
const { currentUser } = this.props;
let { authUser } = this.state;
// ALl changes to user object will trigger an update
if (currentUser) authUser = currentUser;
return (
<AuthUserContext.Provider value={authUser}>
<Component {...this.props} />
</AuthUserContext.Provider>
);
}
}
withAuthentication.whyDidYouRender = true;
const mapDispatchToProps = dispatch => ({
setUser: authUser => dispatch(setCurrentUser(authUser)),
startUserListen: () => dispatch(startUserListener()),
});
const mapStateToProps = state => {
return {
currentUser: selectUserSlice(state),
};
};
return compose(connect(mapStateToProps, mapDispatchToProps), withFirebase)(withAuthentication);
};
export default WithAuthentication;
My question is will this hit me later with problems or is this ok to do it like this?
I know a HOC is not suppose to be used like this. The WithAuthentication is taking care of Authentication against Firebase and then render on all user object changes both local and from Firestore listener snapshot.
This HOC is used in many other places correctly but App.jsx only need to initialize the HOC and never use it's service.
My question is will this hit me later with problems or is this ok to do it like this?
Testing with Jasmine and Enzyme and I've been trying to test a HOC which is connected to redux. Take for instance the following HOC:
import React, { Component } from 'react';
import { connect } from 'react-redux';
const WithAreas = (WrappedComponent) => {
class WithAreasComponent extends Component {
constructor(props) {
super(props);
}
shouldRenderWrappedComponent(userObj) {
return !!userObj.areas;
}
render() {
const { userObj } = this.props;
return shouldRenderWrappedComponent(userObj)
? <WrappedComponent {...this.props} />
: null;
}
}
function mapStateToProps(state) {
const { info } = state;
const { userObj } = info.userObj;
return { userObj };
}
return connect(mapStateToProps)(WithAreasComponent);
};
export default WithAreas;
Let's say I want to test this HOC in order to check if the wrapped component is being render according to the userObj. I thought about doing a mock component and pass it to the HOC, but this is not working.
Test.js File:
import React from 'react';
import { shallow } from 'enzyme';
import jasmineEnzyme from 'jasmine-enzyme';
import WithAreas from './';
class MockComponent extends React.Component {
render() {
return(
<div> MOCK </div>
);
}
}
function setup(extraProps) {
const props = {
info: {
userObj: {
id: 'example1'
}
},
};
Object.assign(props, extraProps);
const WithAreasInstance = WithAreas(MockComponent);
const wrapper = shallow(<WithAreasInstance {...props} />);
return {
props,
wrapper
};
}
fdescribe('<WithAreas />', () => {
beforeEach(() => {
jasmineEnzyme();
});
it('should render the Mock Component', () => {
const { wrapper } = setup();
expect(wrapper.find(MockComponent).exists()).toBe(true);
});
});
But it gives me this error:
TypeError: (0 , _.WithAreas) is not a function
at setup (webpack:///src/user/containers/WithAreas/test.js:20:47 <- src/test_index.js:9:18060087)
at UserContext.<anonymous> (webpack:///src/user/containers//WithAreas/test.js:34:24 <- src/test_index.js:9:18060822)
What am I doing wrong? Or what approach might you recommend?
Thanks for any given help.
You're not importing the HOC in test correctly. You've set it as the default export, but are using named export deconstruction.
Try this in your test file:
import WithAreas from './';
Also, don't forget to pass the store to your component in test, otherwise the connect HOC won't work as expected.
shallow(
<Provider store={store}>
<WithAreasInstance ...>
</Provider>)
I'm trying to lazy load routes in React by implementing the AsyncCompoment class as documented here Code Splitting in Create React App. Below is the es6 asyncComponent function from the tutorial:
import React, { Component } from "react";
export default function asyncComponent(importComponent) {
class AsyncComponent extends Component {
constructor(props) {
super(props);
this.state = {
component: null
};
}
async componentDidMount() {
const { default: component } = await importComponent();
this.setState({
component: component
});
}
render() {
const C = this.state.component;
return C ? <C {...this.props} /> : null;
}
}
return AsyncComponent;
}
I've written this function in typescript and can confirm that components are indeed being loaded lazily. The issue I face is that they are not being rendered. I was able to determine that the component object is always undefined in the componentDidMount hook:
//AsyncComponent.tsx
async componentDidMount() {
const { default: component } = await importComponent();
this.setState({
component: component
});
}
The object being returned from the importComponent function has the following properties:
{
MyComponent: class MyComponent: f,
__esModule: true
}
I modified the componentDidMount method to take the first property of this object, which is the MyComponent class. After this change my project is now lazy loading the components and rendering them properly.
async componentDidMount() {
const component = await importComponent();
this.setState({
component: component[Object.keys(component)[0]]
});
}
My best guess is that I have not written this line properly in typescript:
const { default: component } = await importComponent();
I'm calling the asyncComponent method like so:
const MyComponent = asyncComponent(()=>import(./components/MyComponent));
Anyone know how to implement the AsyncComponent in typescript? I'm not sure if simply getting the 0 index on the esModule object is the correct way to do it.
// AsyncComponent.tsx
import * as React from "react";
interface AsyncComponentState {
Component: null | JSX.Element;
};
interface IAsyncComponent {
(importComponent: () => Promise<{ default: React.ComponentType<any> }>): React.ComponentClass;
}
const asyncComponent: IAsyncComponent = (importComponent) => {
class AsyncFunc extends React.PureComponent<any, AsyncComponentState> {
mounted: boolean = false;
constructor(props: any) {
super(props);
this.state = {
Component: null
};
}
componentWillUnmount() {
this.mounted = false;
}
async componentDidMount() {
this.mounted = true;
const { default: Component } = await importComponent();
if (this.mounted) {
this.setState({
component: <Component {...this.props} />
});
}
}
render() {
const Component = this.state.Component;
return Component ? Component : <div>....Loading</div>
}
}
return AsyncFunc;
}
export default asyncComponent;
// Counter.tsx
import * as React from 'react';
import { RouteComponentProps } from 'react-router';
interface CounterState {
currentCount: number;
}
class Counter extends React.Component<RouteComponentProps<{}>, CounterState> {
constructor() {
super();
this.state = { currentCount: 0 };
}
public render() {
return <div>
<h1>Counter</h1>
<p>This is a simple example of a React component.</p>
<p>Current count: <strong>{this.state.currentCount}</strong></p>
<button onClick={() => { this.incrementCounter() }}>Increment</button>
</div>;
}
incrementCounter() {
this.setState({
currentCount: this.state.currentCount + 1
});
}
}
export default Counter;
//routes.tsx
import * as React from 'react';
import { Route } from 'react-router-dom';
import { Layout } from './components/Layout';
import { Home } from './components/Home';
import asyncComponent from './components/AsyncComponent';
const AsyncCounter = asyncComponent(() => import('./components/Counter'));
export const routes = <Layout>
<Route exact path='/' component={Home} />
<Route path='/counter' component={AsyncCounter} />
</Layout>;
Im trying to create a React Component using Redux and with functions
userSignUpRequest I follow a tutorial but still getting the error:
I think is possible becouse I use module.exports to export my component instead of export by default but im not sure how to fix it.
My Store:
const middleware = routerMiddleware(hashHistory);
const store = createStore(
reducers,
applyMiddleware(middleware)
);
render(
<Provider store={store}>
<Router
onUpdate={scrollToTop}
history={history}
routes={rootRoute}
/>
</Provider>,
document.getElementById('app-container')
);
This is my Component:
import React from 'react';
import PropTypes from 'prop-types';
import { connect } from 'react-redux'; //to pass functions
import { userSignUpRequest } from '../../../actions/loginAxios';
class Login extends React.Component {
constructor() {
super();
this.state = {
{** initial state **}
};
}
onSubmit(e) {
e.preventDefault();
if(this.isValid()){
//reset errros object and disable submit button
this.setState({ errors: {}, isLoading: true });
//we store a function in the props
this.props.userSignUpRequest(this.state).then(
(response) => {
//succes redirect
this.context.router.history.push('/');
},
(error) => {
console.log("error");
this.setState({ errors: error.response.data, isLoading: false });
});
} else {
console.log(this.state.errors);
}
}
onChange(e) {
this.setState({ [e.target.name]: e.target.value });
}
render() {
const { errors } = this.state; //inicializate an get errors
const { userSignUpRequest } = this.props;
return (
<div className="body-inner">
{*** My React Login React Form ***}
</div>
);
}
}
const Page = () => (
<div className="page-login">
<div className="main-body">
<QueueAnim type="bottom" className="ui-animate">
<div key="1">
<Login />
</div>
</QueueAnim>
</div>
</div>
);
//To get the main Actions
Login.propTypes = {
userSignUpRequest: PropTypes.func.isRequired
};
function mapStateToProps(state) {
//pass the providers
return {
}
}
module.exports = connect(mapStateToProps, { userSignUpRequest })(Page);
This is my function "loginAxios.js"
import axios from "axios";
export function userSignUpRequest(userData) {
return dispatch => {
return axios.post("/api/users", userData);
}
}
I am new to React so I would greatly appreciate your support!! Thanks
You explicitly use propTypes and even use isRequired, which is exactly what it sounds like.
Login.propTypes = {
userSignUpRequest: PropTypes.func.isRequired
};
But in your Page component, you don't provide a prop name userSignUpRequest. You just have it as this:
<Login/>
If userSignUpRequest is fundamentally needed for that component to work, you need to pass it in.
<Login userSignUpRequest={() => myFunction()}/>
If it's not, then just delete your propTypes declaration.
You are importing the module userSignUpRequest. It doesn't appear you are passing it through as a prop. Try just calling your module without the "this.props"
In my case, I have resolved the Warning by using below syntax
parentRoute={`${parentRoute}`}
Instead of
parentRoute={parentRoute}
Hope it helps.
I've tried a few ways of going about this where in my action is do the dispatch(push) :
import LoginService from '../../services/login-service';
export const ActionTypes = {
SET_LOGIN: 'SET_LOGIN',
}
export function getLogin(data, dispatch) {
LoginService.getLoginInfo(data).then((response) => {
const login = response.data;
dispatch({
type: ActionTypes.SET_LOGIN,
login
})
// .then((res) => {
// dispatch(push('/'));
// })
})
}
I even saw something about using the render property on the route for react router.
Now I am trying to use renderIf based on whether my state is true or false but I can't seem to get props on this page successfully :
import React from 'react';
import renderIf from 'render-if';
import { Redirect } from 'react-router-dom';
import LoginHeader from '../../components/header/login-header';
import LoginPage from './login-page';
const LoginHome = props => (
<div>
{console.log(props)}
{renderIf(props.loginReducer.status === false)(
<div>
<LoginHeader />
<LoginPage />}
</div>)}
{renderIf(props.loginReducer.status === true)(
<Redirect to="/" />,
)}
</div>
);
export default LoginHome;
Here is loginPage:
import React from 'react';
import { form, FieldGroup, FormGroup, Checkbox, Button } from 'react-bootstrap';
import { Field, reduxForm, formValueSelector } from 'redux-form';
import { connect } from 'react-redux';
import { getLogin } from './login-actions';
import LoginForm from './form';
import logoSrc from '../../assets/images/logo/K.png';
import './login.scss'
class LoginPage extends React.Component {
constructor(props) {
super(props);
this.state = {
passwordVisible: false,
}
this.handleSubmit= this.handleSubmit.bind(this);
this.togglePasswordVisibility= this.togglePasswordVisibility.bind(this);
}
togglePasswordVisibility() {
this.setState((prevState) => {
if (prevState.passwordVisible === false) {
return { passwordVisible: prevState.passwordVisible = true }
}
else if (prevState.passwordVisible === true) {
return { passwordVisible: prevState.passwordVisible = false }
}
})
}
handleSubmit(values) {
this.props.onSubmit(values)
}
render () {
return (
<div id="background">
<div className="form-wrapper">
<img
className="formLogo"
src={logoSrc}
alt="K"/>
<LoginForm onSubmit={this.handleSubmit} passwordVisible={this.state.passwordVisible}
togglePasswordVisibility={this.togglePasswordVisibility}/>
</div>
</div>
)
}
}
function mapStateToProps(state) {
return {
loginReducer: state.loginReducer,
};
}
function mapDispatchToProps(dispatch) {
return {
onSubmit: (data) => {
getLogin(data, dispatch);
},
};
}
export default connect (mapStateToProps, mapDispatchToProps)(LoginPage);
Let me know if anything else is needed
Use the withRouter HoC from React Router to provide { match, history, location } to your component as follows;
import { withRouter } from "react-router";
...
export default withRouter(connect(mapStateToProps, mapDispatchToProps)(LoginPage));
Modify getLogin to accept history as a third argument; export function getLogin(data, dispatch, history) { ... }
Now in the then method of your async action you can use history.push() or history.replace() to change your current location. <Redirect /> uses history.replace() internally.
You can read about the history module here. This is what is used internally by React Router.
EDIT: In response to your comment...
With your current setup you will need to pass history into getLogin through your mapDispatchToProps provided onSubmit prop.
function mapDispatchToProps(dispatch) {
return {
onSubmit: (data, history) => {
getLogin(data, dispatch, history);
},
};
}
Your handleSubmit method will also need updated.
handleSubmit(values) {
this.props.onSubmit(values, this.props.history)
}
Modified answer depending on how Router was set up I had to use hashHistory: react-router " Cannot read property 'push' of undefined"