I'm working on a React Native application that also use Redux and I want to write tests with Jest. I'm not able to mock the "navigation" prop that is added by react-navigation.
Here is my component:
import React from 'react';
import { connect } from 'react-redux';
import PropTypes from 'prop-types';
import { Text, View } from 'react-native';
const Loading = (props) => {
if (props.rehydrated === true) {
const { navigate } = props.navigation;
navigate('Main');
}
return (
<View>
<Text>Loading...</Text>
</View>
);
};
Loading.propTypes = {
rehydrated: PropTypes.bool.isRequired,
navigation: PropTypes.shape({
navigate: PropTypes.func.isRequired,
}).isRequired,
};
const mapStateToProps = state => ({
rehydrated: state.rehydrated,
});
export default connect(mapStateToProps)(Loading);
The Loading component is added as a screen to a DrawerNavigator.
And here is the test:
import React from 'react';
import renderer from 'react-test-renderer';
import mockStore from 'redux-mock-store';
import Loading from '../';
describe('Loading screen', () => {
it('should display loading text if not rehydrated', () => {
const store = mockStore({
rehydrated: false,
navigation: { navigate: jest.fn() },
});
expect(renderer.create(<Loading store={store} />)).toMatchSnapshot();
});
});
When I run the test, I get the following error:
Warning: Failed prop type: The prop `navigation` is marked as required in `Loading`, but its value is `undefined`.
in Loading (created by Connect(Loading))
in Connect(Loading)
Any idea on how to mock the navigation property?
Try to pass navigation directly via props:
it('should display loading text if not rehydrated', () => {
const store = mockStore({
rehydrated: false,
});
const navigation = { navigate: jest.fn() };
expect(renderer.create(<Loading store={store} navigation={navigation} />)).toMatchSnapshot();
});
Related
I am new to React Native Testing Library. For my React native app I am using styled components and Typescript. I fetched the data and pass to my flatList. In Flat-list's render item I have created one global component where it display all the data which is wrap with one touchable container. When user will click the that touchable opacity it will go to single product details screen.
For testing the component I have created one mock container. And I wrap my touchable opacity component. I followed this article to create mocked navigator. I want to test the touchable opacity component and it navigate to the next screen. But I am getting error and it says:
The action 'NAVIGATE' with payload
{"name":"ProductDetails","params":{"product":{"__typename":"Product","id":"6414893391048","ean":"6414893391048","name":"T
shirt","brandName":"Denim","price":13.79 }} was not handled by any
navigator.
This my component
const navigation = useNavigation();
const onPress = () => {
trackProductView({
id: item.id ,
name: item.name,
});
navigation.navigate(Routes.ProductDetails, { product: item });
};
return (
<TouchableOpacity
accessible={true}
{...a11y({
role: 'menuitem',
label: item.name,
})}
onPress={onPress} // This is my onPress function
>
<ItemContainer>
<ProductTitleText ellipsizeMode={'tail'} numberOfLines={2}>
{item.name}
</ProductTitleText>
<QuantityModifierWrapper>
<QuantityModifier item={item!} />
</QuantityModifierWrapper>
</ItemContainer>
</TouchableOpacity>
);
This is my mocked container
import React from 'react';
import { NavigationContainer } from '#react-navigation/native';
import { createStackNavigator } from '#react-navigation/stack';
import 'react-native-gesture-handler';
import { MockedProvider } from '#apollo/client/testing';
type Props = {
screen: any;
params?: any;
};
const Stack = createStackNavigator();
const MockedNavigator = ({ screen, params = {} }: Props) => {
return (
<MockedProvider>
<NavigationContainer>
<Stack.Navigator>
<Stack.Screen
name='MockedScreen'
component={screen}
initialParams={params}
/>
</Stack.Navigator>
</NavigationContainer>
</MockedProvider>
);
};
export default MockedNavigator;
This is my mocked screen
import React from 'react';
import { View } from 'react-native';
type Props = {
children: React.ReactNode;
};
const MockedScreen = ({ children }: Props) => {
return <View>{children}</View>;
};
export default MockedScreen;
This is my test suite where I am getting failed test
import React from 'react';
import { fireEvent, render, cleanup } from 'skm/utils/testing_utils';
import Touchablecomponent from './Touchable';
import MockedNavigator from './MockNav';
import MockedScreen from './Mockscreen';
describe('<Touchablecomponent/> ', () => {
test("render with invalid data", async () => {
const screenName = 'ProductDetails';
const component = (
<MockedNavigator
screen={() => (
<MockedScreen>
<ProductItemSmall item={mockData} />
</MockedScreen>
)}
// params={{data: mockData }}
/>
);
const { getByA11yRole, debug, toJSON } = render(component);
const item = getByA11yRole('menuitem');
console.log(fireEvent.press(item));
});
})
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} />;
}
I have combined my react redux.
Here is my App.js
import React from 'react';
import ReduxThunk from 'redux-thunk';
import { Provider } from 'react-redux';
import { compose, createStore, applyMiddleware } from 'redux';
import reducers from './src/reducers';
import AppContainer from './src/navigator'
const composeEnhancers = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose;
const App: () => React$Node = () => {
const store = createStore(reducers, {}, composeEnhancers(applyMiddleware(ReduxThunk)));
return (
<Provider store={store}>
<AppContainer />
</Provider>
);
};
export default App;
src/reducers/index.js
import { combineReducers } from 'redux';
import LoginReducer from './LoginReducer';
export default combineReducers({
LoginRedux: LoginReducer
});
If I use my action login(), I can see login action start, but I can't see dispatch start
import React from 'react';
import {
Text,
View,
TouchableOpacity,
} from 'react-native';
import { connect } from 'react-redux';
import { login } from '../actions';
const LoginScreen = ({ navigation }) => {
// console.log('see my test value', testValue)
return (
<View>
<TouchableOpacity
onPress={() => {
login();
}
}>
<View>
<Text>LOGIN</Text>
</View>
</TouchableOpacity>
</View>
</View>
);
}
const mapStateToProps = (state) => {
const { testValue } = state.LoginRedux;
console.log('mapStateToProps testValue =>', testValue);
return { testValue };
};
export default connect(mapStateToProps, { login })(LoginScreen);
If I console.log(dispatch), it will show dispatch is not defined.
import { LOGIN } from './types';
export const login = () => {
console.log('login action start')
return (dispatch) => {
console.log('dispatch start');
// console.log(dispatch);
dispatch({ type: LOGIN, testValue: 'I am test' });
};
};
src/reducers/LoginReducer.js
import { LOGIN } from '../actions/types';
const INITIAL_STATE = {
testValue: ''
};
export default (state = INITIAL_STATE, action) => {
console.log('reducer =>', action); // I can't see the console.log
switch (action.type) {
case LOGIN:
return {
...state,
testValue: action.testValue
};
default:
return state;
}
};
I have no idea why my action dispatch is not working. Do I set something wrong ?
Any help would be appreciated.
According to Zaki Obeid help, I update like this:
the action code:
export const login = () => {
console.log('login !');
return { type: LOGIN };
};
the function component code:
import { login } from '../../actions';
export const SettingScreen = ({ navigation, login }) => {
// return view code
}
const mapDispatchToProps = dispatch => ({
// you will use this to pass it to the props of your component
login: () => dispatch(login),
});
connect(null, mapDispatchToProps)(SettingScreen);
In LoginScreen component
you will need to add mapDispatchToProps
const mapDispatchToProps = dispatch => ({
// you will use this to pass it to the props of your component
login: () => dispatch(login()),
});
export default connect(mapStateToProps, mapDispatchToProps)(LoginScreen);
Then
you will need to destructure from the props as:
const LoginScreen = ({ navigation, login }) => {
// your code
}
In actions.js
the way you use dispatch here requires a library redux-thunk and it's used for async calls.
and the normal action should do the job for you:
export const login = () => ({
type: LOGIN,
testValue: 'I am test'
})
I hope this is useful and will solve your problem,
Have a good day.
In a react-redux app, you obtain the dispatch function either from getting a hold of the store object directly (store.dispatch), or via the react-redux connect function, which will provide dispatch as an argument to a function you write and then later hook up to a component
import { connect } from 'react-redux';
const mapStateToProps = ...
const mapDispatchToProps = (dispatch) => {
return {
someHandle: () => dispatch(myActionCreator())
}
}
export const connect(mapStateToProps, mapDispatchToProps)(MyComponent)
You can't just call dispatch out of thin air -- it's not a global function.
It seems you are using the login function directly. you will have to use the props. Just change the name for confusing and use through props.
import { combineReducers } from 'redux';
import LoginReducer from './LoginReducer';
export default combineReducers({
LoginRedux: LoginReducer
});
If I use my action login(), I can see login action start, but I can't see dispatch start
import React from 'react';
import {
Text,
View,
TouchableOpacity,
} from 'react-native';
import { connect } from 'react-redux';
import { login } from '../actions';
const LoginScreen = ({ navigation, userLogin }) => {
// console.log('see my test value', testValue)
return (
<View>
<TouchableOpacity
onPress={() => {
userLogin();
}
}>
<View>
<Text>LOGIN</Text>
</View>
</TouchableOpacity>
</View>
</View>
);
}
const mapStateToProps = (state) => {
const { testValue } = state.LoginRedux;
console.log('mapStateToProps testValue =>', testValue);
return { testValue };
};
export default connect(mapStateToProps, { userLogin:login })(LoginScreen);
Problem:
I have very simple todo app. There is one action - add todo. When I add a task, I simulate sending it to the server using a setTimeout.
When I receive a response from the server, I immediately check to see if there is an error to avoid further action. In stateful component, everything works, and in stateless component it doesn't.
See the code to better understand the problem.
Environment:
"react": "16.8.6",
"react-native": "0.60.5",
"react-redux": "^7.1.1",
"redux": "^4.0.4",
"redux-logger": "^3.0.6",
"redux-thunk": "^2.3.0"
№ 1. Stateful component:
import React, {Component} from 'react';
import {View, Button, ActivityIndicator} from 'react-native';
import {connect} from 'react-redux';
import {addTodo as addTodoAction} from '../redux/reducer';
class MainScreen extends Component {
todoGenerator = () => ({
id: new Date().getTime(),
text: 'Pls help me ' + new Date().getTime(),
});
sendTodoToServer = async () => {
const todo = this.todoGenerator();
const {addTodo} = this.props;
await addTodo(todo);
// this
const {error} = this.props;
if (error) {
console.log('error', error);
}
};
render() {
const {isLoading} = this.props;
return (
<View>
<Button title="Generate todo" onPress={this.sendTodoToServer} />
{isLoading && <ActivityIndicator />}
</View>
);
}
}
export default connect(
state => ({
todos: state.todos,
error: state.error,
isLoading: state.isLoading,
}),
{
addTodo: addTodoAction,
},
)(MainScreen);
№ 1. Stateful component. Console:
As you can see,
const {error} = this.props;
if (error) {
console.log('error', error);
}
it's work. Okay, let's move on to functional components
№ 2. Stateless component with redux connect:
import React from 'react';
import {ActivityIndicator, Button, View} from 'react-native';
import {connect} from 'react-redux';
import {addTodo as addTodoAction} from '../redux/reducer';
const MainScreenFC = ({isLoading, addTodo, error}) => {
const todoGenerator = () => ({
id: new Date().getTime(),
text: 'Pls help me ' + new Date().getTime(),
});
const sendTodoToServer = async () => {
const todo = todoGenerator();
await addTodo(todo);
if (error) {
console.log('error', error);
}
};
return (
<View>
<Button title="Generate todo" onPress={sendTodoToServer} />
{isLoading && <ActivityIndicator />}
</View>
);
};
export default connect(
state => ({
todos: state.todos,
error: state.error,
isLoading: state.isLoading,
}),
{
addTodo: addTodoAction,
},
)(MainScreenFC);
№ 2. Stateless component with redux connect. Console:
The error did not display in the console, although it is in the reducer
№ 3. Stateless component with redux HOOKS:
import React from 'react';
import {ActivityIndicator, Button, View} from 'react-native';
import {connect, shallowEqual, useDispatch, useSelector} from 'react-redux';
import {addTodo as addTodoAction} from '../redux/reducer';
const MainScreenReduxHooks = () => {
const todos = useSelector((state: AppState) => state.todos, shallowEqual);
const error = useSelector((state: AppState) => state.error, shallowEqual);
const isLoading = useSelector(
(state: AppState) => state.isLoading,
shallowEqual,
);
const dispatch = useDispatch();
const todoGenerator = () => ({
id: new Date().getTime(),
text: 'Pls help me ' + new Date().getTime(),
});
const sendTodoToServer = async () => {
const todo = todoGenerator();
await dispatch(addTodoAction(todo));
if (error) {
console.log('error', error);
}
};
return (
<View>
<Button title="Generate todo" onPress={sendTodoToServer} />
{isLoading && <ActivityIndicator />}
</View>
);
};
export default connect(
state => ({
todos: state.todos,
error: state.error,
isLoading: state.isLoading,
}),
{
addTodo: addTodoAction,
},
)(MainScreenReduxHooks);
№ 3. Stateless component with redux HOOKS. Console:
It's the same here, as in the second example.
Questions:
Can redux be connected to a stateless component?
How do you make the second and third example work the same way as the first?
Other code:
App.js
import React from 'react';
import {Provider} from 'react-redux';
import {MainScreen, MainScreenFC, MainScreenReduxHooks} from './src/screens';
import store from './src/redux';
const App = () => {
return (
<Provider store={store}>
<MainScreenFC />
</Provider>
);
};
export default App;
store.js:
import {applyMiddleware, createStore} from 'redux';
import thunk from 'redux-thunk';
import logger from 'redux-logger';
import rootReducer from './reducer';
export default createStore(rootReducer, applyMiddleware(thunk, logger));
reducer.js:
const ADD_TODO_REQUEST = 'ADD_TODO_REQUEST';
const ADD_TODO_SUCCESS = 'ADD_TODO_SUCCESS';
const ADD_TODO_FAILURE = 'ADD_TODO_FAILURE';
const initialState = {
todos: [],
isLoading: false,
error: undefined,
};
export const addTodo = data => async dispatch => {
dispatch({
type: ADD_TODO_REQUEST,
payload: {
isLoading: true,
},
});
try {
const todo = await new Promise((resolve, reject) => {
setTimeout(() => {
reject('Ooops, error');
}, 3000);
});
dispatch({
type: ADD_TODO_SUCCESS,
payload: {
todo,
isLoading: false,
},
});
} catch (e) {
dispatch({
type: ADD_TODO_FAILURE,
payload: {
isLoading: false,
error: e,
},
});
}
};
export default function(state = initialState, {type, payload}) {
switch (type) {
case ADD_TODO_REQUEST: {
return {
...state,
isLoading: true,
};
}
case ADD_TODO_SUCCESS: {
return {
...state,
isLoading: false,
todos: [...state.todos, payload.todo],
};
}
case ADD_TODO_FAILURE: {
return {
...state,
isLoading: false,
error: payload,
};
}
default:
return state;
}
}
I think the problem with your code is that you try to wait for a response in the component, it's a bad idea for both stateful and stateless components. What would I recommend you to do, is to handle error somewhere in your middleware (redux-thunk, redux-saga, etc). In this case, your component should just represent data, and if you have to display an error, just take it from props, I believe it is stored somewhere in redux.
In any way, stateless components shouldn't be an async function, because the result of an async function is a promise but not a component. There are some libraries like async-reactor, but personally, I prefer to go with another approach.
If you tell me your use case, what would you like to do once you got an error, I'll give you a more useful answer.
Update:
export const addTodo = data => dispatch => {
// tell your application that request is sending,
// so you can handle it in UI (show a progress indicator)
dispatch({
type: ADD_TODO_REQUEST,
payload: data
});
try {
const response = await createTodo(data);
dispatch({
type: ADD_TODO_SUCCESS,
payload: response
});
// here you can dispatch navigation action as well
} catch (error) {
dispatch({
type: ADD_TODO_FAILURE,
error
});
// and here you can dispatch action with a toast
// to notify users that something went wrong
}
};
i am using react-navigation and redux in react-native.when i just used redux, the states can send to child by redux. but it don't work when i add react-navigation.
my navigation.js
import {StackNavigator} from 'react-navigation';
import Home from './modules/home/views/Home'
export const StackRouter = StackNavigator({
Main: {screen: Home},
initialRouteName: {screen: Home}
});
const firstAction = StackRouter.router.getActionForPathAndParams('Main');
const tempNavState = StackRouter.router.getStateForAction(firstAction);
const initialNavState = StackRouter.router.getStateForAction(
firstAction,
tempNavState
);
//navigationreducer
export const stackReducer = (state=initialNavState,action) => {
debugger
const newState = StackRouter.router.getStateForAction(action, state);
return newState || state;
};
my store.js
import React, {Component} from 'react';
import { connect, Provider } from 'react-redux';
import {createStore, combineReducers, bindActionCreators, applyMiddleware } from 'redux';
import {addNavigationHelpers} from 'react-navigation'
import thunk from 'redux-thunk'
import PropTypes from 'prop-types'
import {StackRouter, stackReducer} from './router'
import home from './modules/home';
//routerConmponent
class RouterAppWithState extends Component {
constructor(props){
super(props)
}
render() {
return (
<StackRouter navigation={addNavigationHelpers({
dispatch: this.props.dispatch,
state: this.props.router,
})} />
);
}
}
//all reducer
const reducer = combineReducers({
home: home.reducer,
router: stackReducer,
});
const mapActionToProps = (dispatch) => {
return {
home_actions: bindActionCreators(home.actions, dispatch)
}
};
const mapStateToProps = (state) => {
debugger;
return state
};
//connect redux and navigation
const StoreApp = connect(mapStateToProps, mapActionToProps)(RouterAppWithState);
const store = applyMiddleware(thunk)(createStore)(reducer);
export default class RootApp extends Component{
render() {
return(
<Provider store={store}>
<StoreApp />
</Provider>
)
}
}
my home.js ,just import actions and reducers, and export the module
import actions from './store/actions';
import reducer from './store/reducer'
export default {
actions,
reducer
}
my reducers
export default(state=states, action) => {
let newState = {...state};
switch (action.type){
case TYPES.ADD_COUNT: newState.count = state.count+1;break;
default: break;
}
return newState
};
my actions
const add_count = () => {
console.log('add');
return async (dispatch) => {
dispatch({type: TYPES.ADD_COUNT});
await new Promise((resolve) => {setTimeout(() => {resolve()} , 3000)});
dispatch({type: TYPES.ADD_COUNT});
};
};
export default {
add_count
}
my view Home.js
import React, {Component, StyleSheet} from 'react';
import {View, Text, Button} from 'react-native';
export default class Home extends Component{
constructor(props){
super(props)
}
// static contextTypes = {
// navigation: React.PropTypes.Object
// };
render() {
debugger
const {add_count} = this.props.home_actions;
const {count} = this.props.home;
return (
<View>
<Text >{count}</Text>
<Button onPress={add_count} title={'add count'}/>
{/*<Button onPress={() => {navigation.navigate('live')}} title={'live'}/>*/}
</View>
)
}
}
In Home.js function render, this.props is
{
navigation: Object,
screenProps: undefined
}
has not states in redux. but without react-navigation, this.props is
{
home: Object,
home_actions: Object
}
thanks
The reason is because now the StackRouter is controlling the rendering of your Home page. The router only pass down a prop called navigation and whatever you pass down from screenProps.
There are two ways that you can achieve a similar behavior as before:
1) In RouterAppWithState's render function, pass down this.props into screenProps like this
<StackRouter navigation={addNavigationHelpers({
dispatch: this.props.dispatch,
state: this.props.router,
})}
screenProps={this.props}
/>
2) (recommended) The other way is to directly use connect on your home page to connect your Home component to the store and access home specific part of the store. i.e. connect(mapStateToProps)(Home)