Issue with unit testing of react component that uses "Material UI" using "Enzyme" - testing

I have created two components LoginView and Login Form. LoginView uses LoginForm component for rendering. I am writing test cases for LoginView and LoginForm separately. The issue is when I import the LoginView and LoginForm component in a single spec, it works fine. But as I am trying to import same either components it raises an error
Invariant Violation: addComponentAsRefTo(...): Only a ReactOwner can have refs. You might be adding a ref to a component that was not created inside a component's render method, or you have multiple copies of React loaded.
The error says that
I might be using multiple copies of React, but I am using
react#15.3.2 and material-ui#0.15.4
The use of 'ref', I have not used 'ref' but Material-ui uses ref for
their component.
I am unable to solve issue, any help/ suggestions are appreciated.
Node v6.6.0
NPM 3.10.3
Files are:
LoginView:
import React from 'react';
import { FlatButton, Dialog } from 'material-ui';
import BaseComponent from './BaseComponent';
import LoginForm from './LoginForm';
export default class LoginView extends BaseComponent {
// statements
render(
<LoginForm onSubmit={this.handleSubmit} loading={loading} />
<br />
<FlatButton
secondary
onClick = { this.navToRegister }
label = {__('No Account? Create one.')}
/>
<br />
<FlatButton
secondary
onClick = { this.navToForgotPassword }
label = {__('Forgot your password?')}
/>
<br />
)
}
LoginForm:
import React from 'react';
import { TextField, RaisedButton } from 'material-ui';
export default class LoginForm extends React.Component {
// Statements
return (
<form onSubmit={this.handleSubmit}>
<TextField
type = "text"
/>
<br />
<TextField
type= "password"
/>
<br />
<RaisedButton
type = "submit"
/>
</form>
);
}
Spec Files:
Login Form:
import React from 'react';
import { mount } from 'enzyme';
import getMuiTheme from 'material-ui/styles/getMuiTheme';
import LoginForm from '../../src/components/LoginForm';
import baseTheme from '../../src/style/muiThemes/neo';
describe('Login ', () => {
describe('LoginForm test case ', () => {
const context = { muiTheme: getMuiTheme(baseTheme) };
const email = 'test#mail.com';
const pass = 'testPassword';
let form = null;
let textField = null;
let emailInput = null;
let passwordInput = null;
let submitButton = null;
let submitSpy = null;
let wrapper = null;
beforeEach(() => {
submitSpy = chai.spy.on(LoginForm.prototype, 'handleSubmit');
wrapper = mount(<LoginForm onSubmit={submitSpy} />, { context });
form = wrapper.find('LoginForm');
textField = wrapper.find('TextField');
emailInput = textField.get(0);
passwordInput = textField.get(1);
submitButton = wrapper.find('button[type="submit"]');
});
// Some test scenarios
}
Login View:
import React from 'react';
import { mount } from 'enzyme';
import getMuiTheme from 'material-ui/styles/getMuiTheme';
import LoginView from '../../src/components/LoginView'; // Error while importing this file.
import baseTheme from '../../src/style/muiThemes/neo';
describe('LoginView test cases', () => {
const context = { muiTheme: getMuiTheme(baseTheme) };
let submitSpy = null;
let wrapper = null;
let loginForm = null;
beforeEach(() => {
submitSpy = chai.spy.on(LoginView.prototype, 'handleSubmit');
wrapper = mount(<LoginView onSubmit={submitSpy} />, { context });
loginForm = wrapper.find('LoginForm');
});
it('calls componentDidMount', () => {
const spy = chai.spy.on(LoginView.prototype, 'componentDidMount');
wrapper = mount(<LoginView />, { context });
expect(spy).to.have.been.called.once;
});
it('Access the dom', () => {
expect(loginForm).not.to.equal(undefined);
});
});

Related

handleClick() in LogoutButton.js is not working

I've integrated MSAL.js 2.0 with react-admin in order to use Azure Active Directory as Auth Provider. Based on react-admin Auth providers samples and links (https://github.com/victorp13/react-admin-msal) I've implemented login. Works great, my react-admin frontend is correctly protected.
But I cannot succeeded to implement logout. If I follow react-admin documentation (https://marmelab.com/react-admin/Authentication.html#uselogout-hook), my LogoutButton.js code is ignored.
Index.js
import React from "react";
import ReactDOM from "react-dom";
import "./index.css";
import App from "./App";
import reportWebVitals from "./reportWebVitals";
import { PublicClientApplication, InteractionType } from "#azure/msal-browser";
import { MsalProvider, MsalAuthenticationTemplate } from "#azure/msal-react";
import { msalConfig } from "./authConfig";
const msalInstance = new PublicClientApplication(msalConfig);
ReactDOM.render(
<React.StrictMode>
<MsalProvider instance={msalInstance}>
<MsalAuthenticationTemplate interactionType={InteractionType.Redirect}>
<App />
</MsalAuthenticationTemplate>
</MsalProvider>
</React.StrictMode>,
document.getElementById("root")
);
reportWebVitals();
App.js
import * as React from "react";
import { Admin, Resource, ListGuesser, fetchUtils } from "react-admin";
import dataProvider from "./dataProvider";
import LogoutButton from './LogoutButton';
const App = () => (
<Admin dataProvider={dataProvider} logoutButton={LogoutButton}>
<Resource name="users" list={ListGuesser} />
</Admin>
);
export default App;
LogoutButton.js
import * as React from 'react';
import { forwardRef } from 'react';
import { useLogout } from 'react-admin';
import MenuItem from '#material-ui/core/MenuItem';
import ExitIcon from '#material-ui/icons/PowerSettingsNew';
import { PublicClientApplication } from "#azure/msal-browser";
import { msalConfig } from "./authConfig";
const LogoutButton = forwardRef((props, ref) => {
const logout = useLogout();
const handleClick = () => {
console.log('Logout button never clicked!');
const msalInstance = new PublicClientApplication(msalConfig);
msalInstance.logoutRedirect();
logout();
};
return (
<MenuItem
onClick={handleClick}
ref={ref}
>
<ExitIcon /> Disconnect
</MenuItem>
);
});
export default LogoutButton;
Any ideas to help? Thanks!
Regards
You need to use an authProvider in order to properly use the auth functions given by react-admin. If you can share your console errors when clicking, I think I could have more insight. Still, first thing I would do is override the logout function from the authProvider , the following is the interface for writing your own. What you want to do is take out all the logic from your handleClick function, and only leave the logout() callback. Then the msal logic, you want to put it inside logout, something like this.
import { AuthProvider } from 'react-admin';
const authProvider = {
// authentication
login: ({ username, password }) => { /* ... */ },
checkError: (error) => { /* ... */ },
checkAuth: () => { /* ... */ },
logout: () => {
console.log('Logout button never clicked!');
const msalInstance = new PublicClientApplication(msalConfig);
msalInstance.logoutRedirect();
},
getIdentity: () => { /* ... */ },
// authorization
getPermissions: (params) => { /* ... */ },
}.

How to test a custom react hook which uses useNavigation from 'react-navigation-hooks' using Jest?

I have a custom react hook 'useSample' which uses useNavigation and useNavigationParam
import { useContext } from 'react'
import { useNavigation, useNavigationParam } from 'react-navigation-hooks'
import sampleContext from '../sampleContext'
import LoadingStateContext from '../LoadingState/Context'
const useSample = () => {
const sample = useContext(sampleContext)
const loading = useContext(LoadingStateContext)
const navigation = useNavigation()
const Mode = !!useNavigationParam('Mode')
const getSample = () => {
if (Mode) {
return sample.selectors.getSample(SAMPLE_ID)
}
const id = useNavigationParam('sample')
sample.selectors.getSample(id)
navigation.navigate(SAMPLE_MODE_ROUTE, { ...navigation.state.params}) // using navigation hook here
}
return { getSample }
}
export default useSample
I need to write unit tests for the above hook using jest and I tried the following
import { renderHook } from '#testing-library/react-hooks'
import sampleContext from '../../sampleContext'
import useSample from '../useSample'
describe('useSample', () => {
it('return sample data', () => {
const getSample = jest.fn()
const sampleContextValue = ({
selectors: {
getSample
}
})
const wrapper = ({ children }) => (
<sampleContext.Provider value={sampleContextValue}>
{children}
</sampleContext.Provider>
)
renderHook(() => useSample(), { wrapper })
})
})
I got the error
'react-navigation hooks require a navigation context but it couldn't be found. Make sure you didn't forget to create and render the react-navigation app container. If you need to access an optional navigation object, you can useContext(NavigationContext), which may return'
Any help would be appreciated!
versions I am using
"react-navigation-hooks": "^1.1.0"
"#testing-library/react-hooks":"^3.4.1"
"react": "^16.11.0"
You have to mock the react-navigation-hooks module.
In your test:
import { useNavigation, useNavigationParam } from 'react-navigation-hooks';
jest.mock('react-navigation-hooks');
And it's up to you to add a custom implementation to the mock. If you want to do that you can check how to mock functions on jest documentation.
for me, soved it by usingenter code here useRoute():
For functional component:
import * as React from 'react';
import { Button } from 'react-native';
import { useNavigation } from '#react-navigation/native';
function MyBackButton() {
const navigation = useNavigation();
return (
<Button
title="Back"
onPress={() => {
navigation.goBack();
}}
/>
);
}
For class component:
class MyText extends React.Component {
render() {
// Get it from props
const { route } = this.props;
}
}
// Wrap and export
export default function(props) {
const route = useRoute();
return <MyText {...props} route={route} />;
}

Redux: mapStateToProps is not being called

I understand this kind of question was already asked several times here at StackOverflow. But I tried all the recommended solutions and nothing works for me. I'm running out of ideas.
The problem is with a React Native application for Android. Basically, the app provides a search bar to search an underlying database. The search results should be put into the store.
I use Redux v4.0.5, React-Redux v7.1.3, React v16.12.0 and React Native v0.61.5. For debugging, I use React Native Debugger in the latest version.
Now the simplified code. First, the component with the search bar. Here, mapStateToProps() is called. User makes an input and useEffect() immediately runs the database query, which should result in immediately calling mapStateToProps().
import React, {useEffect, useRef, useState} from 'react';
import {connect} from 'react-redux';
import {RootState} from '../../../rootReducer/rootReducer';
import {setResultValueSearchBar} from '../../../store/searchBar/actions';
imports ...
type Props = {};
const SearchBar: React.FC<Props> = () => {
const [returnValue, setReturnValue] = useState('');
const [inputValue, setInputValue] = useState('');
useEffect(() => {
// get query results
// logic to finally get a result string that should be put into the store
const resultNames: string = resultNamesArray.toString();
// method to set local and Redux state
const sendReturnValueToReduxStore = (resultNames: string) => {
setReturnValue(resultNames);
setResultValueSearchBar({resultValue: resultNames});
console.log('result value sent to store ', resultNames);
};
// call above method
sendReturnValueToReduxStore(resultNames);
}, [inputValue, returnValue]);
return (
<View>
<ScrollView>
<Header searchBar>
<Item>
<Input
placeholder="Search"
onChangeText={text => setInputValue(text)}
value={inputValue}
/>
</Item>
</Header>
</ScrollView>
</View>
);
};
function mapStateToProps(state: RootState) {
console.log("map state to props!", state); // is only called one time, initially
return {
resultValue: state.searchBarResult.resultValue,
};
}
const mapDispatchToProps = {
setResultValueSearchBar,
};
export default connect(mapStateToProps, mapDispatchToProps)(SearchBar);
Here is the rootReducer:
import {combineReducers} from 'redux';
import searchBarResultReducer from '../store/searchBar/reducers';
import reducer2 from '../store/reducer2example/reducers';
const rootReducer = combineReducers({
searchBarResult: searchBarResultReducer,
reducer2Result: reducer2,
});
export type RootState = ReturnType<typeof rootReducer>;
Here is the searchBarResultReducer in reducers.ts file:
import {
SearchBarResultState,
SET_RESULT_VALUE_SEARCHBAR,
ResultValueType,
} from './types';
const initialState: SearchBarResultState = {
resultValue: 'No results',
};
// take state and action and then return a new state
function searchBarResultReducer(
state = initialState,
action: ResultValueType,
): SearchBarResultState {
console.log('invoked result: ', action.type); // called only initially
if (action.type === 'SET_RESULT_VALUE_SEARCHBAR') {
return {
...state,
...action.payload,
};
} else {
return state;
}
}
export default searchBarResultReducer;
And the corresponding types.ts ...
export const SET_RESULT_VALUE_SEARCHBAR = 'SET_RESULT_VALUE_SEARCHBAR';
export interface SearchBarResultState {
resultValue: string;
}
interface ResultValueAction {
type: typeof SET_RESULT_VALUE_SEARCHBAR;
payload: SearchBarResultState;
}
export type ResultValueType = ResultValueAction
... and the actions.ts:
import {SET_RESULT_VALUE_SEARCHBAR, ResultValueType, SearchBarResultState} from './types'
export const setResultValueSearchBar = (resultValue: SearchBarResultState): ResultValueType => ({
type: SET_RESULT_VALUE_SEARCHBAR,
payload: resultValue,
});
And index.js:
import React from 'react';
import {AppRegistry} from 'react-native';
import {createStore, applyMiddleware, compose} from 'redux';
import {Provider} from 'react-redux';
import App from './App';
import {name as appName} from './app.json';
import rootReducer from './src/rootReducer/rootReducer';
import Realm from 'realm';
import { composeWithDevTools } from 'redux-devtools-extension';
import invariant from 'redux-immutable-state-invariant';
const composeEnhancers = composeWithDevTools({});
const store = createStore(
rootReducer,
composeEnhancers(applyMiddleware(invariant()))
);
const Root = () => {
Realm.copyBundledRealmFiles();
return (
<Provider store={store}>
<App />
</Provider>
);
};
AppRegistry.registerComponent(appName, () => Root);
To summarize: Whenever the database query succeeds, the result value should be sent to the store. But in the React Native Debugger/Redux Devtools, the reducer/mapStateToProps() is called only once and only, as shown by the console.log s in the code.
What is going on here?
Solved! As stated by Hemant in this Thread, you also have to pass the action that you import as props into the component. Works like a charm now :)

How to start with left menu collapsed

Is there an easy was to start with the left menu collapsed or do I need to update the layout ?
I'd like to have the menu collapsed by default with only the icons visible.
Thank you.
If you mean Sidebar when saying "left menu", you can hide it by turning on the user saga (the toggle action will continue to work):
// closeSidebarSaga.js
import {
put,
takeEvery,
} from 'redux-saga/effects'
import {
REGISTER_RESOURCE, // React-admin 3.5.0
setSidebarVisibility,
} from 'react-admin'
function* closeSidebar(action) {
try {
if (action.payload) {
yield put(setSidebarVisibility(false))
}
} catch (error) {
console.log('closeSidebar:', error)
}
}
function* closeSidebarSaga() {
yield takeEvery(REGISTER_RESOURCE, closeSidebar)
}
export default closeSidebarSaga
// App.js:
import closeSidebarSaga from './closeSidebarSaga'
<Admin customSagas={[ closeSidebarSaga ]} ... }>
...
</Admin>
In the react-admin library itself, apparently a bug, at some point in time after the login, action SET_SIDEBAR_VISIBILITY = true is called!
You can set the initial state when loading Admin, then there will be no moment when the Sidebar is visible at the beginning, and then it is hidden:
https://marmelab.com/react-admin/Admin.html#initialstate
const initialState = {
admin: { ui: { sidebarOpen: false, viewVersion: 0 } }
}
<Admin
initialState={initialState}
...
</Admin>
To hide the left sideBar divider we need to dispatch setSidebarVisibility action .
This is an example to hide the sideBar by using useDispatch react-redux hook &
setSidebarVisibility action inside the layout file (layout.tsx):
import React from 'react';
/**
* Step 1/2 :Import required hooks (useEffect & useDispatch)
*/
import { useEffect } from 'react';
import { useSelector,useDispatch } from 'react-redux';
import { Layout, Sidebar ,setSidebarVisibility} from 'react-admin';
import AppBar from './AppBar';
import Menu from './Menu';
import { darkTheme, lightTheme } from './themes';
import { AppState } from '../types';
const CustomSidebar = (props: any) => <Sidebar {...props} size={200} />;
export default (props: any) => {
const theme = useSelector((state: AppState) =>
state.theme === 'dark' ? darkTheme : lightTheme
);
/**
* Step 2/2 : dispatch setSidebarVisibility action
*/
const dispatch = useDispatch();
useEffect(() => {
dispatch(setSidebarVisibility(false));
});
return (
<Layout
{...props}
appBar={AppBar}
sidebar={CustomSidebar}
menu={Menu}
theme={theme}
/>
);
};
Since Redux is not used anymore by ReactAdmin, you need to adapt the local storage instead to hide the sidebar. Call this in your App.tsx class:
const store = localStorageStore();
store.setItem("sidebar.open", false);

undifined is not a function- react redux

I am having some issues in this code which i'm following a tutorial react-native-redux.I had to change some build.gradle compiler versions to suit according to my Android SDK Version.Please can anyone point out where did i do the mistake?
import React, { Component } from 'react';
import { View, Text } from 'react-native';
import firebase from 'firebase';
import connect from 'react-redux';
import {emailChanged} from '../actions';
import { Spinner, Header, Button, Card, CardSection, Input } from './common';
class LoginForm extends Component {
// constructor() {
// super();
// this.onEmailChange = this.onEmailChange.bind(this);
// }
onEmailChange(text) {
//step 1 : trigger the action with new text
this.props.emailChanged(text);
}
render() {
return (
<Card>
<CardSection>
<Header text="Please Login" />
</CardSection>
<CardSection>
<Input placeholder="example#user.com"
labelText="e Mail"
onChangeText={this.onEmailChange.bind(this)}
//set the value with previous text
value={this.props.email}
/>
</CardSection>
<CardSection>
<Input encrypt={true} labelText="Password" />
</CardSection>
<CardSection>
<Button>Login Here</Button>
</CardSection>
</Card>
);
}
};
// get the state(session) and assign to props
const mapStateToProps = (state) => {
return {
//return empty objetc with assigned session.reducerName.propertyName as props
email : state.auth.email
};
};
export default connect(mapStateToProps,{emailChanged})(LoginForm);
Actions
import Types from './types';
export const emailChanged = (text) => {
return {
type : Types.EMAIL_CHANGED ,
payload : text
};
};
Reducer
import Types from '../actions/types';
const INITIAL_STATE = { email: '' };
export default (state = INITIAL_STATE, action) => {
switch (action.type) {
case Types.EMAIL_CHANGED:
//need a brand new object when returning - Theory
// return an empty object with all properties of state with email updated with action.payload
return { ...state, email: action.payload };
default:
return state;
}
}
The connect function is not the default export of react-redux. You have to import it like this:
import {connect} from 'react-redux';