How to write unit test cases for navigation stack in react native using enzyme jest? - react-native

I am new to react native and trying to write unit test cases. I am able to write snapshot test cases but not sure how to write unit test cases for navigation stack.
is there any way to write unit test cases for navigation?
navigator.js
import { createStackNavigator } from 'react-navigation-stack';
import { createAppContainer } from 'react-navigation';
import HomeScreen from '../component/HomeComponent/home';
import ThumbnailView from '../component/ThumbnailComponent/thumbnailView';
import AlbumDetailsView from '../component/AlbumDetailsComponent/albumDetailsView';
const MainNavigator = createStackNavigator({
HomeScreen: { screen: HomeScreen },
ThumbnailViewScreen: { screen: ThumbnailView },
AlbumDetailsViewScreen: { screen: AlbumDetailsView },
},
{
defaultNavigationOptions: {
headerTintColor: '#fff',
headerStyle: {
backgroundColor: '#0c82f3',
},
headerTitleStyle: {
fontWeight: 'bold',
},
},
});
const NavigationApp = createAppContainer(MainNavigator);
export default NavigationApp;

You can test navigate function in your components like this:
import HomeScreen from '../component/HomeComponent/home';
import { shallow, ShallowWrapper } from "enzyme";
import React from "react";
import { View } from "react-native";
const createTestProps = (props) => ({
navigation: {
navigate: jest.fn()
},
...props
});
describe("HomeScreen", () => {
describe("rendering", () => {
let wrapper;
let props;
beforeEach(() => {
props = createTestProps({});
wrapper = shallow(<HomeScreen {...props} />);
});
it("should render a <View /> and go to ThumbnailViewScreen", () => {
expect(wrapper.find(View)).toHaveLength(1); // Some other tests
expect(props.navigation.navigate).toHaveBeenCalledWith('ThumbnailViewScreen'); // What you want
});
});
});
HomeScreen.js :
import React, { Component } from "react";
import { Text, View } from "react-native";
export class HomeScreen extends Component {
componentDidMount = () => {
this.props.navigation.navigate("ThumbnailViewScreen");
};
render() {
return (
<View>
<Text>This is the HomeScreen.</Text>
</View>
);
}
}
export default HomeScreen;
You can find more details here

Related

Attempts to access this ref will fail. Did you mean to use React.forwardRef()? with jest

The warning doesn't happen during debug or release mode builds but when testing App.js:
App.js
import React from 'react';
import { View, AppRegistry } from 'react-native';
import { gestureHandlerRootHOC } from 'react-native-gesture-handler';
import { LocalizationProvider } from 'library/localization';
import NavigationService from 'library/NavigationService';
import AppNavigator from 'library/AppNavigator';
const App = () => {
return (
<>
<LocalizationProvider>
<AppNavigator
ref={(navigatorRef) => {
NavigationService.setTopLevelNavigator(navigatorRef);
}}
/>
</LocalizationProvider>
</>
);
};
AppRegistry.registerComponent(appName, () => gestureHandlerRootHOC(App));
export default App;
the error doesn't appear if I remove the ref from AppNavigator.
AppNavigator.js
const guestUserNavigation = createStackNavigator(
{
Login: {
screen: LoginScreen
},
Logout: {
screen: LogoutActivity
}
},
{
headerMode: 'none',
initialRouteName: 'Login'
}
);
const userNavigation = createStackNavigator(
{
Home: {
screen: HomeScreen
}
},
{
headerMode: 'none',
initialRouteName: 'Home'
}
);
const App = createSwitchNavigator({
loader: {
screen: AuthLoadingScreen
},
Auth: {
screen: guestUserNavigation
},
App: {
screen: userNavigation
}
});
const AppNavigator = createAppContainer(App, {
initialRouteName: 'loader'
});
export default AppNavigator;
App.test.js
import React from 'react';
import { render } from 'react-native-testing-library';
import App from './App';
describe('App', () => {
it('should render the App', () => {
const result = render(<App />).toJSON();
expect(result).toMatchSnapshot();
});
});
mocks/react-navigation.js
jest.mock('react-navigation', () => ({
createAppContainer: jest
.fn()
.mockReturnValue(function NavigationContainer(props) {
return null;
}),
createSwitchNavigator: jest.fn(),
NavigationActions: {
navigate: jest.fn().mockImplementation((x) => x)
}
}));
now the test passes with the following warning that is bugging me and I cant seem to get it to work:
Warning: Function components cannot be given refs. Attempts to access this ref will fail. Did you mean to use React.forwardRef()?
Check the render method of `App`.
in NavigationContainer (at App.js:29)
in LocalizationProvider (at App.js:28)
in App (at App.test.js:7)
using forwardRef works but then the app doesn't render.
AppNavigator shouldn't return null because this makes the mock useless to test App.
In order to receive a ref, AppNavigator should be class or forwardRef component. createAppContainer arguments can be optionally represented in element hierarchy in order to be testable by a snapshot:
...
createAppContainer: jest.fn((Comp, options) => React.forwardRef((props, ref) => {
return <div data-testid="AppContainer" ref={ref}>
<div data-testid="createAppContainer component">
{Comp.name}
</div>
<div data-testid="createAppContainer options">
{JSON.stringify(options)}
</div>
{children}
</div>;
})),
...

How can I pass the navigator prop from react-native-navigation v2 to my splash screen via Navigation.setRoot

I am trying to migrate from react-native-navigation v1 to react-native-navigation v2. I am struggling to move from
Navigation.startSingleScreenApp
to
Navigation.setRoot
When I switch from Navigation.startSingleScreenApp (v1) to Navigation.setRoot (v2), I no longer have the navigator prop that I was relying on to navigate around the application.
I have copy and pasted all relevant code below
RegisterScreens
import { Navigation } from 'react-native-navigation';
import SplashScreenScreen from './components/SplashScreen';
import { Provider } from 'react-redux';
import React from "react";
import SCREEN from './screenNames';
export default function registerScreens(store) {
Navigation.registerComponent(
SCREEN.SPLASH_SCREEN,
() => props => (<Provider store={store}><SplashScreenScreen {...props} /></Provider>), () => SplashScreenScreen);
App
import { Platform } from 'react-native';
import { Navigation } from 'react-native-navigation';
import registerScreens from './registerScreens';
import { Colors, Fonts } from './themes';
import { store } from './configureStore';
import NavigationListener from './NavigationEventListener';
import configureNotification from './configureNotification';
import SCREEN from './screenNames';
import Reactotron from 'reactotron-react-native';
const navBarTranslucent = Platform.OS === 'ios';
configureNotification();
registerScreens(store);
new NavigationListener(store);
const STARTING_SCREEN = SCREEN.SPLASH_SCREEN;
Navigation.events().registerAppLaunchedListener(() => {
Reactotron.log('5');
Navigation.setRoot({
root: {
stack: {
children: [{
component: {
id: STARTING_SCREEN,
name: STARTING_SCREEN
}
}],
}
},
layout: {
orientation: 'portrait',
},
});
});
SplashScreen
import React from 'react';
import { View, StyleSheet, Text } from 'react-native';
import { connect } from 'react-redux';
import PropTypes from 'prop-types';
import { PersistGate } from 'redux-persist/es/integration/react';
import { navigateToFirstScreen } from '../redux/splash';
import { Colors, Fonts, Metrics } from '../themes';
import { persistor } from '../configureStore';
export class SplashScreen extends React.Component {
navigateTo = (screen) =>
this.props.navigator.push({
screen,
overrideBackPress: true,
backButtonHidden: true,
animated: false,
navigatorStyle: {
disabledBackGesture: true,
},
});
render() {
const { dispatchNavigateToFirstScreen } = this.props;
return (
<PersistGate
persistor={persistor}
onBeforeLift={() => setTimeout(() => dispatchNavigateToFirstScreen(this.navigateTo), 2000)}><View style={styles.bodyContainer}
>
<Text>Jono</Text>
</View>
</PersistGate>
);
}
}
const styles = StyleSheet.create({
bodyContainer: {
flex: 1,
justifyContent: 'center',
alignItems: 'center',
backgroundColor: Colors.splashScreen,
},
appTitleText: {
fontSize: Fonts.size.splashScreenTitle,
fontFamily: Fonts.type.extraBold,
lineHeight: Metrics.lineHeight.appTitle,
textAlign: 'center',
color: Colors.textLightColor,
},
});
SplashScreen.propTypes = {
navigator: PropTypes.shape({
push: PropTypes.func.isRequired,
}).isRequired,
dispatchNavigateToFirstScreen: PropTypes.func.isRequired,
};
const mapDispatchToProps = (dispatch) => {
return {
dispatchNavigateToFirstScreen: (navigateTo) =>
dispatch(navigateToFirstScreen(navigateTo)),
};
};
export default connect(null, mapDispatchToProps)(SplashScreen);
I spent multiple hours trying to solve this problem so I am going to post my conclusion as an answer.
this.props.navigator is not used anymore in 2.x.
You need to use Navigation
This dude had the same problem and reached the same conclusion: https://github.com/wix/react-native-navigation/issues/3795

How to navigate to another screen from a Redux form with React navigation?

Basically I want that within a props.handleSubmit function you can navigate to another screen.
Here is my code :
import React, { Component } from "react";
import { createStackNavigator, createAppContainer } from 'react-navigation';
import { Provider } from 'react-redux';
import Store from './Store/Store'
import ScreenSignUp from './Navigation/ScreenSignUp';
import ScreenSignIn from './Navigation/ScreenSignIn';
import ScreenHome from "./Navigation/ScreenHome";
class App extends Component {
render() {
return (
<Provider store={Store}>
<AppContainer />
</Provider>
);
}
}
export default App;
const RootStack = createStackNavigator(
{
Home: ScreenHome,
SignUp: ScreenSignUp,
SignIn: ScreenSignIn,
},
{
initialRouteName: 'SignIn',
defaultNavigationOptions: {
header: null,
},
},
);
const AppContainer = createAppContainer(RootStack);
you can do it like this
export default reduxForm({
form: 'SignInForm',
onSubmitSuccess : (result, dispatch, props) => {
props.navigation.navigate('Register');
},
onSubmitFail : () => {
Toast.show({
text: "Enter Passport Number",
duration: 2500,
position: "top",
textStyle: { textAlign: "center" }
});
}
validate,
})(SignInForm);

React Navigation with Redux - Drawer close error

I'am using react navigation with redux and after redux integration, i got some errors on drawer close.
import React from "react";
. . .
import { NavigationActions } from "react-navigation";
import { StackNavigator, DrawerNavigator } from 'react-navigation';
import { addListener } from "./components/common/utils";
import Dashboard from './components/pages/Dashboard';
. . .
const MainNavigator = StackNavigator({
Dashboard : {
screen : Dashboard,
},
. . .
})
export const AppNavigator = DrawerNavigator(
{
Main: { screen: MainNavigator }
}, {
contentComponent: Menu,
drawerWidth: 300,
headerMode: 'screen',
drawerPosition: 'left',
drawerOpenRoute: 'DrawerOpen',
drawerCloseRoute: 'DrawerClose',
drawerToggleRoute: 'DrawerToggle',
}
)
class AppWithNavigationState extends React.Component {
constructor (props) {
super(props)
this.onBackPress = this.onBackPress.bind(this)
}
componentDidMount () {
BackHandler.addEventListener('hardwareBackPress', this.onBackPress)
}
componentWillUnmount () {
BackHandler.removeEventListener('hardwareBackPress', this.onBackPress)
}
onBackPress () {
...
}
render() {
const { dispatch, nav } = this.props;
return (
<AppNavigator
navigation={{
dispatch,
state: nav,
addListener,
}}
/>
);
}
}
const mapStateToProps = state => ({
nav: state.nav,
});
AppWithNavigationState.propTypes = {
dispatch: PropTypes.func.isRequired,
nav: PropTypes.object.isRequired,
};
export default connect(mapStateToProps)(AppWithNavigationState);
Here is my reducer:
import { fromJS } from 'immutable';
import { NavigationActions } from "react-navigation";
import { combineReducers } from "redux";
import { AppNavigator } from "../../App";
import {...} from './constants';
import { ToastAndroid } from 'react-native';
const mainAction = AppNavigator.router.getActionForPathAndParams('Main');
const initialNavState = AppNavigator.router.getStateForAction(mainAction);
function nav(state = initialNavState, action) {
let nextState;
switch (action.type) {
case 'Reports':
nextState = AppNavigator.router.getStateForAction(
NavigationActions.back(),
state
);
break;
default:
nextState = AppNavigator.router.getStateForAction(action, state);
break;
}
return nextState || state;
}
const initialState = fromJS({
isLoading: true,
...
});
function store(state = initialState, action) {
switch (action.type) {
case SET_IS_LOADING:
return state.set('isLoading', action.value);
...
default:
return state;
}
}
const AppReducer = combineReducers({
nav,
store,
});
export default AppReducer;
and file i call DraweOpen:
import React from "react";
import PropTypes from "prop-types";
import { TouchableOpacity } from "react-native";
import { createStructuredSelector } from 'reselect';
import { connect } from 'react-redux';
import { bindActionCreators } from 'redux';
import Icon from "react-native-vector-icons/dist/MaterialIcons";
import { DrawerBurger } from "../common/styles";
import { navigate } from "../store/actions";
const drawerButton = (props) => (
<DrawerBurger>
<TouchableOpacity
onPress={() => props.navigate("DrawerOpen")}
>
<Icon name="menu" size={30} color="white" />
</TouchableOpacity>
</DrawerBurger>
);
drawerButton.propTypes = {
navigate: PropTypes.func.isRequired,
};
const mapStateToProps = createStructuredSelector({});
const mapDispatchToProps = dispatch => (
(
bindActionCreators({
navigate,
}, dispatch)
)
);
export default connect(mapStateToProps, mapDispatchToProps)(drawerButton);
and i call drawerButton component on navigation options like:
...
class Dashboard extends Component {
static navigationOptions = () => ({
headerTitle: <Header dashboard />,
headerStyle: { backgroundColor: '#2c4e0f' },
headerLeft: <DrawerButton />,
});
...
I followed instructions on reactnavigation.org, also read some example code to build navigator.
Actually there was no error before redux integration and the navigator structure was same except BackHandling.
Here is my actions.js:
import { NavigationActions } from "react-navigation";
import {...} from './constants';
...
export const navigate = routeName => NavigationActions.navigate({ routeName });
My environment is:
react-navigation: 1.5.11
react-native: 0.53.0
react-navigation-redux-helpers: 1.0.5
react-redux: 5.0.7
redux: 3.7.2
node: 8.9.4
npm: 5.6.0
Thank for your help.
According to the redux integration docs, it seems you've missed one step.
You need to add addNavigationHelpers from React Navigation
Usage
import {addNavigationHelpers} from 'react-navigation';
<AppNavigator navigation={addNavigationHelpers({
dispatch,
state: nav,
addListener,
})} />

How to detect when the drawer is opened in react-navigation?

I'm using react-navigation's DrawerNavigator in my app. I would like to detect when a user drags open the side menu so that i can perform a certain action, e.g dismiss an opened Keyboard.
How can i do this? i can't seem to find a solution in the docs. Thank you.
Here is my code
import React from 'react';
import { Dimensions } from 'react-native';
import { Icon } from 'react-native-elements';
import { DrawerNavigator, StackNavigator, addNavigationHelpers } from 'react-navigation';
//redux related imports
import { createStore, combineReducers } from 'redux';
import { Provider, connect } from 'react-redux';
import Attendance from './containers/pages/Attendance';
import Fees from './containers/pages/Fees';
import Exams from './containers/pages/Exams';
import InitializeUser from './containers/pages/InitializeUser';
import Landing from './Landing';
import Login from './containers/pages/Login';
import Search from './containers/pages/Search';
import Staff from './containers/pages/Staff';
import Stats from './containers/pages/Stats';
import Students from './containers/pages/Students';
import Verification from './containers/pages/verify';
import ProfileDetail from './components/pages/ProfileDetail';
import FeesDetail from './containers/pages/FeesDetails';
import MainReport from './containers/pages/Reports/Main_Report';
import AcademicDetails from './containers/pages/Reports/Student_Academic_Details';
import { Constants } from './config';
import ResultsLanding from './containers/pages/Reports/ResultsLanding';
import { CustomDrawerContentComponent } from '../src/components/base/SideMenu';
const screenWidth = Dimensions.get('window').width;
const MainPages = DrawerNavigator({
StudentsPage: {
path: '/Students',
screen: Students
},
SearchPage: {
path: '/Seacrh',
screen: Search
},
Staff: {
path: '/Staff',
screen: Staff
},
Fees: {
path: '/Fees',
screen: Fees
},
Stats: {
path: '/Stats',
screen: Stats
},
Results: {
screen: ResultsLanding,
navigationOptions: {
tintColor: 'red',
flex: 1,
drawerIcon: ({ tintColor }) => (
<Icon
name="content-paste"
color={tintColor}
/>
)
}
},
Attendance:
{
path: '/Attendance',
screen: Attendance
},
},
{
initialRouteName: 'StudentsPage',
contentOptions: {
activeTintColor: Constants.ui.THEME,
activeBackgroundColor: 'rgba(0,0,0,0.1)',
inactiveTintColor: 'rgba(0,0,0,0.6)',
labelStyle: {
fontSize: 12,
marginLeft: 10,
},
},
drawerWidth: screenWidth > 320 ? 300 : 250,
contentComponent: CustomDrawerContentComponent
});
export const Navigator = StackNavigator({
LandingPage: {
screen: Landing,
navigationOptions: {
header: null
}
},
LoginPage: {
screen: Login,
navigationOptions: {
header: null
},
},
ProfileDetailPage: {
screen: ProfileDetail,
headerMode: 'screen',
navigationOptions: {
header: null
}
},
FeesDetailPage:
{
screen: FeesDetail,
navigationOptions: {
header: null
},
},
VerifyPage: {
screen: Verification,
navigationOptions: {
header: null
},
},
InitUserPage: {
screen: InitializeUser,
navigationOptions: {
header: null
},
},
MainPages: {
screen: MainPages,
navigationOptions: {
header: null
}
},
MainReportPage: {
screen: MainReport,
navigationOptions: {
header: null
}
},
ExamsMainPage: {
screen: Exams,
navigationOptions: {
header: null
}
},
AcademicDetailsPage: {
screen: AcademicDetails,
navigationOptions: {
header: null
}
},
});
const initialState = MainPages.router.getStateForAction(
MainPages.router.getActionForPathAndParams('StudentsPage'));
const navReducer = (state = initialState, action) => {
const nextState = MainPages.router.getStateForAction(action, state);
return nextState || state;
};
const appReducer = combineReducers({
nav: navReducer
});
class App extends React.Component {
componentWillReceiveProps(nextProps) {
console.warn('nextProps: ', JSON.stringify(nextProps, null, 4));
}
render() {
return (
<MainPages
navigation={addNavigationHelpers({
dispatch: this.props.dispatch,
state: this.props.nav,
})} />
);
}
}
const mapStateToProps = (state) => ({
nav: state.nav
});
const AppWithNavigationState = connect(mapStateToProps)(App);
const store = createStore(appReducer);
class Root extends React.Component {
render() {
return (
<Provider store={store}>
<AppWithNavigationState />
</Provider>
);
}
}
Here is a screenshot of the error i get when i run the code
Simply, if you use react-navigation v5 -
import { useIsDrawerOpen } from '#react-navigation/drawer'
const isOpen: boolean = useIsDrawerOpen()
then you have to subscribe on "isOpen" flag by hook useEffect:
useEffect(() => {
if (!isOpen) {
// Your dismiss logic here
}
}, [isOpen])
Your custom Drawer can look like DrawerContent
Hope it helped
I was able to detect the DrawerNavigator's open and close side menu actions by following the Redux Integration guide and modifying it to use a DrawerNavigator instead of StackNavigator. Here is what I have inside my index.ios.js file. Near the bottom within the App class I use componentWillReceiveProps which displays a warning every time the drawer opens or closes.
import React from 'react';
import {
AppRegistry,
Image,
Text,
View,
ScrollView
} from 'react-native';
import {DrawerNavigator, DrawerItems, addNavigationHelpers } from 'react-navigation';
import { Provider, connect } from 'react-redux'
import { createStore, combineReducers } from 'redux'
class MyHomeScreen extends React.Component {
static navigationOptions = {
drawerLabel: 'Home',
drawerIcon: ({ tintColor }) => (
<Image
source={require('./images/Groups.png')}
style={{tintColor: tintColor, width: 26, height: 26}}/>
),
};
render() {
return (
<View>
<Text>Home Screen</Text>
</View>
);
}
}
class MyNotificationsScreen extends React.Component {
static navigationOptions = {
drawerLabel: 'Notifications',
drawerIcon: ({ tintColor }) => (
<Image
source={require('./images/Events.png')}
style={{tintColor: tintColor, width: 26, height: 26}}/>
),
};
render() {
return (
<View>
<Text>Notifications Screen</Text>
</View>
);
}
}
const NavDemo = DrawerNavigator({
Home: { screen: MyHomeScreen },
Notifications: { screen: MyNotificationsScreen }
}, {
tabBarOptions: {
activeTintColor: '#e91e63',
},
drawerWidth: 200,
drawerPosition: 'left',
contentComponent: props => <ScrollView><DrawerItems {...props} /></ScrollView>
});
const initialState = NavDemo.router.getStateForAction(NavDemo.router.getActionForPathAndParams('Home'));
const navReducer = (state = initialState, action) => {
const nextState = NavDemo.router.getStateForAction(action, state);
return nextState || state;
};
const appReducer = combineReducers({
nav: navReducer
});
class App extends React.Component {
componentWillReceiveProps(nextProps) {
console.warn('nextProps: ' + JSON.stringify(nextProps, null, 4))
}
render() {
return (
<NavDemo navigation={addNavigationHelpers({
dispatch: this.props.dispatch,
state: this.props.nav,
})} />
);
}
}
const mapStateToProps = (state) => ({
nav: state.nav
});
const AppWithNavigationState = connect(mapStateToProps)(App);
const store = createStore(appReducer);
class Root extends React.Component {
render() {
return (
<Provider store={store}>
<AppWithNavigationState />
</Provider>
);
}
}
AppRegistry.registerComponent('NavDemo', () => Root);
When I open the drawer and expand the warning nextProps looks like this:
And then after I close the drawer, the new warning appears like this:
nextProps.nav is an object with two keys, routes and index. When the drawer opens, index becomes 1, and when it closes, index becomes 0.
Using that information, you can add an if statement to perform your necessary actions when the drawer opens.
This is an old thread, but I wanted to share how I managed to do this.
Add the following code inside your app (preferably where you create the DraweStack.)
const defaultGetStateForAction = DrawerStack.router.getStateForAction;
DrawerStack.router.getStateForAction = (action, state) => {
switch (action.type) {
case "Navigation/DRAWER_OPENED":
case "Navigation/MARK_DRAWER_ACTIVE":
Keyboard.dismiss();
break;
}
return defaultGetStateForAction(action, state);
};
There are various action.types. For instance:
Navigation/MARK_DRAWER_ACTIVE is called when the drawer starts to open
Navigation/MARK_DRAWER_SETTLING is called when the drawer has opened.
etc.
Cheers.
this.props.navigation.state.isDrawerOpen => true/false
In navigation 6x, you can do the following to get the drawer status.
import { useDrawerStatus } from '#react-navigation/drawer';
const isDrawerOpen = useDrawerStatus() === 'open';
here is the full documentation. https://reactnavigation.org/docs/drawer-navigator/#events