How to hoist a jest dependency mock over actual dependency? - react-native

I'm testing a react-native Component which imports a Class LanguageStore. Currently the test fails because the component is instantiating this class which calls a private setter that is undefined in the scope of the test:
FAIL src\modules\languageProvider\__tests__\LanguageProvider-test.js
● renders correctly
TypeError: _this.strings.setLanguage is not a function
at LanguageStore.setLanguage (src\modules\languageProvider\LanguageStore.js:25:15)
at new LanguageProvider (src\modules\languageProvider\LanguageProvider.js:30:16)
Question:
How to hoist a jest dependency mock over actual dependency?
In order to resolve this I called a jest.mock according my to this answer How can I mock an ES6 module import using Jest?. But I get the same error as before because the test is calling the implementation of LanguageStore rather than the mock I created below - _this.strings.setLanguage is not a function:
import { View } from 'react-native';
import React from 'react';
import { shallow } from 'enzyme';
import renderer from 'react-test-renderer';
import connect from '../connect.js';
import LanguageProvider from '../LanguageProvider';
import LanguageStore from '../LanguageStore';
it('renders correctly', () => {
const TestComponent = connect(Test);
const strings = { test: 'Test' };
const language = "en"
const stringsMock = {
setLanguage: jest.fn()
};
const mockSetLanguage = jest.fn();
jest.mock('../LanguageStore', () => () => ({
language: language,
strings: stringsMock,
setLanguage: mockSetLanguage,
}));
const wrapper = shallow(<LanguageProvider strings={strings} language="en"><Test /></LanguageProvider>);
expect(wrapper.get(0)).toMatchSnapshot();
});
class Test extends React.Component {
constructor(props) {
super(props);
}
render() {
return <View />;
}
}
This is a link to the test and related components and classes under test:
https://github.com/BrianJVarley/react-native-prototyping/blob/i18nProvider-feature/src/modules/languageProvider/tests/LanguageProvider-test.js

Calling jest.mock within a test doesn't work.
You'll need to move your mock outside of the test and make sure your factory function doesn't have any external dependencies.
Something like this:
import { View } from 'react-native';
import React from 'react';
import { shallow } from 'enzyme';
import connect from '../connect.js';
import LanguageProvider from '../LanguageProvider';
import LanguageStore from '../LanguageStore';
jest.mock('../LanguageStore', () => {
const language = "en"
const stringsMock = {
setLanguage: jest.fn()
};
const mockSetLanguage = jest.fn();
return () => ({
language,
strings: stringsMock,
setLanguage: mockSetLanguage,
})
});
it('renders correctly', () => {
const TestComponent = connect(Test);
const strings = { test: 'Test' };
const wrapper = shallow(<LanguageProvider strings={strings} language="en"><Test /></LanguageProvider>);
expect(wrapper.get(0)).toMatchSnapshot();
});
class Test extends React.Component {
constructor(props) {
super(props);
}
render() {
return <View />;
}
}

Related

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} />;
}

How to pass functions from parent to any children and sub-children using Navigator and WithSecurityScreen in React Native?

I am quite new in React Native and I am trying to understand how function written in a parent Component could be passed (inherited) to any children and sub-children. In particular I am using a library to internationalise my App using:
import * as RNLocalize from 'react-native-localize'
import i18n from 'i18n-js'
But I noticed that I have to implement the translate(...) function for each Component of the whole project and this seems to be exaggerated because it requires a lot of work to implement the translation feature (I followed this tutorial).
Please, note that I have a basic understanding how to pass a function or some data using this.props, so I am not asking how props works from a parent to a single child. What I am asking is: how to avoid to repeat the code from //BEGIN ... to //END... (please see WithSecurityScreen file) and to avoid to repeat the implementation of handleLocalizationChange, RNLocalize.addEventListener, RNLocalize.removeEventListener and translate.
Please also note that the translation library works, test is provided at following line of WithSecurityScreen:
const SecurityScreen = () => <View><Text>{translate('USER_SURNAME')}πŸ˜‚</Text></View>;
But I am not be able to pass translate(...) function to each components of the whole project.
The project structure is:
App.js (wraps SecureApp.js)
SecureApp.js (wrapped in App.js and runs WithSecurityScreen.js)
WithSecurityScreen.js (wraps routes to views, e.g. Welcome.js)
Welcome.js (main view)
App.js
import { withSecurityScreen } from './src/components/withSecurityScreen'
import App from "./SecureApp.js"
export default withSecurityScreen(App);
SecureApp.js
const MainNavigator = createStackNavigator({
Home: {
screen: Welcome,
navigationOptions: {
headerShown: false
}
},
UserProfile: {
screen: CoreApp,
navigationOptions: {
headerShown: false
}
},
NumPad: {
screen: NumPad,
navigationOptions: {
header: 'PIN Creation',
headerShown: false
}
}, /* , navigationOptions: {headerLeft: () => null} */
QrScan: {
screen: QrScan, navigationOptions: {
header: 'QR Scan',
headerShown: false
}
},
...
});
export default createAppContainer(MainNavigator);
WithSecurityScreen.js
// START: https://heartbeat.fritz.ai/how-to-use-react-native-localize-in-react-native-apps-3bb3d510f801
import * as RNLocalize from 'react-native-localize'
import i18n from 'i18n-js'
import memoize from 'lodash.memoize'
const translationGetters = {
en: () => require('./../../assets/locales/en/en.json'),
it: () => require('./../../assets/locales/it/it.json')
};
const translate = memoize(
(key, config) => i18n.t(key, config),
(key, config) => (config ? key + JSON.stringify(config) : key)
)
const setI18nConfig = () => {
const fallback = { languageTag: 'en' }
const { languageTag } =
RNLocalize.findBestAvailableLanguage(Object.keys(translationGetters)) ||
fallback
translate.cache.clear()
i18n.translations = { [languageTag]: translationGetters[languageTag]() }
i18n.locale = languageTag
}
// END: https://heartbeat.fritz.ai/how-to-use-react-native-localize-in-react-native-apps-3bb3d510f801
const SecurityScreen = () => <View><Text>{translate('USER_SURNAME')}πŸ˜‚</Text></View>;
const showSecurityScreenFromAppState = appState =>
['background', 'inactive'].includes(appState);
const withSecurityScreenIOS = Wrapped => {
return class WithSecurityScreen extends React.Component {
constructor(props) {
super(props)
setI18nConfig()
}
state = {
showSecurityScreen: showSecurityScreenFromAppState(AppState.currentState)
};
componentDidMount() {
AppState.addEventListener('change', this.onChangeAppState)
RNLocalize.addEventListener('change', this.handleLocalizationChange)
}
componentWillUnmount() {
AppState.removeEventListener('change', this.onChangeAppState)
RNLocalize.removeEventListener('change', this.handleLocalizationChange)
}
handleLocalizationChange = () => {
setI18nConfig()
.then(() => this.forceUpdate())
.catch(error => {
console.error(error)
})
}
onChangeAppState = nextAppState => {
const showSecurityScreen = showSecurityScreenFromAppState(nextAppState);
this.setState({showSecurityScreen})
};
render() {
return this.state.showSecurityScreen
? <SecurityScreen/>
: <Wrapped {...this.props} />
}
}
};
const withSecurityScreenAndroid = Wrapped => Wrapped;
export const withSecurityScreen = Platform.OS === 'ios'
? withSecurityScreenIOS
: withSecurityScreenAndroid;
Welcome.js
export default class Welcome extends Component {
let username = 'UserName';
render() {
return (
<View style={styles.container}>
<LinearGradient colors={globalStyles.colors.gradientGreen} style={{flex: 1}}>
<View style={styles.upperView}><Text style={styles.upperViewText}>{this.props.translate('WELCOME_TEXT')}{this.username}</Text>
</View>
</LinearGradient>
</View>
);
}
}
I get following error:
First of all in your case you can declare translate function in separate js file locale.js and can declare all your translation logic in that file and export the functions translate and setI18nConfig
local.js
import * as RNLocalize from 'react-native-localize'
import i18n from 'i18n-js'
import memoize from 'lodash.memoize'
const translationGetters = {
en: () => require('./../../assets/locales/en/en.json'),
it: () => require('./../../assets/locales/it/it.json')
};
export const translate = memoize(
(key, config) => i18n.t(key, config),
(key, config) => (config ? key + JSON.stringify(config) : key)
)
export const setI18nConfig = () => {
const fallback = { languageTag: 'en' }
const { languageTag } =
RNLocalize.findBestAvailableLanguage(Object.keys(translationGetters)) ||
fallback
translate.cache.clear()
i18n.translations = { [languageTag]: translationGetters[languageTag]() }
i18n.locale = languageTag
}
and import this functions in your components where you want to use this like
App.js
import React, { Component } from 'react';
import { View, Text } from 'react-native';
import { setI18nConfig, translate } from './locale';
export default class App extends Component {
constructor(props) {
super(props);
this.state = {};
}
render() {
setI18nConfig(); // This should be called only once through out the app at the time of changing locale.
return (
<View>
<Text> translate ('key_from_json_to_label') </Text>
</View>
);
}
}
For more details you can refer this repo where I have implemented the same.
Hope this will help.
i use import i18n from "i18next", for translation.
Here is the config file:
//config/i18n
import i18n from "i18next";
import { reactI18nextModule, initReactI18next } from "react-i18next";
import translationFR from '../translation/fr/translation.json';
import translationEN from '../translation/en/translation.json';
import DeviceInfo from 'react-native-device-info';
let locale = DeviceInfo.getDeviceLocale().substring(0, 2);
if (locale != 'fr') {
locale = 'en';
}
// the translations
const resources = {
en: translationEN,
fr: translationFR,
};
i18n
.use(reactI18nextModule) // passes i18n down to react-i18next
.init({
resources: resources,
lng: locale,
fallbackLng: ['en', 'fr'],
keySeparator: false, // we do not use keys in form messages.welcome
interpolation: {
escapeValue: false // react already safes from xss
}
});
export default i18n;
One of the uses is to
import i18n from 'config/i18n'
and use translate in file like this
i18n.t('bottomTab:home_tab')
You can also wrap component with withNamespaces from 'react-i18next', like this:
export default withNamespaces([], {wait: true})(Welcome)
and then access translation with:
this.props.t('welcome_message')

Invariant Violation: Could not find β€œstore” React Native

I get the following error using redux in native react
Invariant Violation: Invariant Violation: Could not find "store" in
the context of "Connect(Lista)". Either wrap the root component in a
, or pass a custom React context provider to and
the corresponding React context consumer to Connect(Lista) in connect
options.
My code is the following
SettingStore
import {createStore, applyMiddleware} from 'redux';
import Reducers from './Reducer'
import thunk from 'redux-thunk'
export default SettingStore = () => {
let store = createStore(Reducers, applyMiddleware(thunk))
return store
}
my index reducer
import {combineReducers} from 'redux';
import loginreducer from './login.reducer'
export default combineReducers({
login: loginreducer,
})
my index action
import {FETCHING_GETDATA_PARVU} from '../Constante'
import {GetParv} from '../Api/Parvularia.api'
export const getParvuSuccess = (data) => {
return {type: FETCHING_GETDATA_PARVU, data}
}
export const GetParvu = () => {
return (dispatch) => {
dispatch(getData())
GetParv(1)
.then(([response, json]) => {
dispatch(getParvuSuccess(json))
})
.catch((error) => console.log(error))
}
}
This is the maintab.reducer of my reducer
import {FETCHING_GETDATA_PARVU} from '../Constante'
const initialState = {
data: [],
isFeching: false,
error: false
}
export default dataReducer = (state = initialState, action) => {
switch(action.type) {
case FETCHING_GETDATA_PARVU:
return {
...state,
data: action.data,
isFeching: false
}
default:
return state
}
}
and let's say this is my app.js of the redux structure
import React, { Component } from 'react';
import { Platform, StyleSheet, Text, View } from 'react-native';
import {Provider} from 'react-redux';
import Lista from '../screens/Educadora/Lista';
import SettingStore from './SettingStore'
let store = SettingStore()
const Ap = () => (
<Provider store={store}>
<Lista/>
</Provider>
)
export default Ap;
This last part is the one that generates me more doubt, I think that this is my error but I do not know why I am new in react native
Edit
This is where I want to show the query made with redux
import { connect } from 'react-redux'
import {GetParvu} from '../../Redux/Actions'
class Lista extends React.Component {
componentWillMount() {
this.props.GetParvu()
render(){
return(algoaca)
}
}
}
const mapStateToProps = state => {
return {
parvu: state.data
}
}
const mapDispatchToProps = dispatch => {
return {
GetParvu: () => {
return dispatch(GetParvu(1))
}
}
}
AppRegistry.registerComponent('EqualsMobile', () => Lista);
export default connect(mapStateToProps, mapDispatchToProps)(Lista);
You have connecting your component in wrong way. connect() is high order component that take mapStateToProps as an argument to map store to your component.
You can use it in your component like this.
function mapStateToProps(state){
return {state}
}
export default connect(mapStateToProps)(your_component_name);

Error in react-native with expo: Could not find "store" in the context of "Connect(App)"

I'm building my first react native app and I encountered a problem to connect to redux store (I also do not have much experience with redux yet). I am using expo.
The error is:
Invariant Violation: Could not find "store" in the context of "Connect(App)". Either wrap the root component in a , or pass a custom React context provider to and the corresponding React context consumer to Connect(App) in connect options.
This error is located at:
in Connect(App) (at withExpoRoot.js:22)
(...)
Here is my code:
Could you please help?
// App.js
import React, { Component } from "react";
import AppStackNav from "./navigators/AppStackNav";
import { Provider, connect } from 'react-redux';
import { createStore } from 'redux';
import guestsReducer from "./reducers/GuestsReducer";
const store = createStore(guestsReducer);
class App extends Component {
constructor(props) {
super(props);
}
addGuest = (index) => {
// ...
}
render() {
return (
<Provider store={store}>
<AppStackNav
screenProps={{
currentGuests: this.state.currentGuests,
possibleGuests: this.state.possibleGuests,
addGuest: this.addGuest
}}
/>
</Provider>
)
}
}
const mapStateToProps = state => {
return {
currentGuests: this.state.current,
possibleGuests: this.state.possible,
addGuest: this.addGuest
};
}
export default connect(mapStateToProps)(App);
// GuestsReducer.js
import { combineReducers } from 'redux';
const INITIAL_STATE = {
current: 10,
possible: [
'Guest1',
'Guest2',
'Guest3',
'Guest4',
],
};
const guestsReducer = (state = INITIAL_STATE, action) => {
switch (action.type) {
default:
return state
}
};
export default combineReducers({
guests: guestsReducer,
});
// AppStackNav.js
import { createStackNavigator, createAppContainer } from "react-navigation";
import Home from "../screens/Home";
import Dashboard from "../screens/Dashboard";
import Project from "../screens/Project";
import Placeholder from "../screens/Placeholder";
const AppStackNav = createStackNavigator({
// ...
});
export default createAppContainer(AppStackNav);
First Issue
const mapStateToProps = ({ guests }) => {
return {
currentGuests: guests.current,
possibleGuests: guests.possible
};
}
Second Issue
You wire redux store to your upper level component which is the App component ... and then use connect and mapStateToProps to access redux store in the children of this upper level component (App) ... I mean you connect your store via mapStateToProps to your AppStackNav component not the App component
const AppStackNav = ({ currentGuests, possibleGuests }) => {
const Stack = createStackNavigator({...});
return <Stack />;
};
const mapStateToProps = ({ guests }) => {
return {
currentGuests: guests.current,
possibleGuests: guests.possible
};
}
// react-navigation v2 is needed for this to work:
export default connect(mapStateToProps)(AppStackNav);
App.js
class App extends Component {
constructor(props) {
super(props);
}
addGuest = (index) => {
// ...
}
render() {
return (
<Provider store={store}>
<AppStackNav />
</Provider>
)
}
}
export default App;
you can't use 'this' keyword outside the class as It wont be able to understand the context for that particular method.
you need to simply remove this keyword from mapStateToProps
like this:
const mapStateToProps = state => {
return {
currentGuests: state.current,
possibleGuests: state.possible
};
}

Async call with react native and redux , thunk

I have been following this tutorial to integrate redux into my react native app.
https://github.com/jlebensold/peckish
On my Home view, I'm not able to call the functions from my action folder.
One difference is that I'm using react-navigation in my app. Wonder if I need to integrate redux with react navigation to be able to use redux for all data?
Below is the full implementation code I have been doing.
On the Home screen, I call the fetchSite function on ComponentDidMount to launch an async call with axios. But I can't even access to this function.
Sorry for this long post but I can't figure out how to make this work so quite difficult to make a shorter code sample to explain the structure of my app.
Let me know if any question.
index.ios.js
import React from 'react'
import { AppRegistry } from 'react-native'
import { Provider } from 'react-redux'
import { createStore, applyMiddleware, compose} from 'redux'
import thunkMiddleware from 'redux-thunk'
import { createLogger } from 'redux-logger'
import reducer from './app/reducers'
import AppContainer from './app/index'
// middleware that logs actions
const loggerMiddleware = createLogger({ predicate: (getState, action) => __DEV__ });
function configureStore(initialState) {
const enhancer = compose(
applyMiddleware(
thunkMiddleware, // lets us dispatch() functions
loggerMiddleware,
),
);
return createStore(reducer, initialState, enhancer);
}
const store = configureStore({});
const App = () => (
<Provider store={store}>
<AppContainer />
</Provider>
);
AppRegistry.registerComponent('Appero', () => App;
reducers/index.js
import { combineReducers } from 'redux';
import * as sitesReducer from './sites'
export default combineReducers(Object.assign(
sitesReducer,
));
reducers/sites.js
import createReducer from '../lib/createReducer'
import * as types from '../actions/types'
export const searchedSites = createReducer({}, {
[types.SET_SEARCHED_SITES](state, action) {
let newState = {};
action.sites.forEach( (site) => {
let id = site.id;
newState[id] = Object.assign({}, site, { id });
});
return newState;
},
});
../lib/createReducer
export default function createReducer(initialState, handlers) {
return function reducer(state = initialState, action) {
if (handlers.hasOwnProperty(action.type)) {
return handlers[action.type](state, action)
} else {
return state
}
}
}
../actions/types
export const SET_SEARCHED_SITES = 'SET_SEARCHED_SITES';
AppContainer in ./app/index
import React, { Component } from 'react';
import { connect } from 'react-redux';
import { bindActionCreators } from 'redux';
import { ActionCreators } from './actions';
console.log(ActionCreators); //Properly gathered the functions from the actions folder
import { Root } from './config/router';
window.store = require('react-native-simple-store');
window.axios = require('axios');
class App extends Component {
render() {
return (
<Root />
)
}
}
function mapDispatchToProps(dispatch) {
return bindActionCreators(ActionCreators, dispatch);
}
export default connect(mapDispatchToProps)(App);
ActionCreators in './actions';
import * as SiteActions from './sites'
export const ActionCreators = Object.assign({},
SiteActions,
);
Actions in './actions/sites'
import * as types from './types' //See above
export function fetchSites(token) {
return (dispatch, getState) => {
let instance = axios.create({
baseURL: url + 'api/',
timeout: 10000,
headers: {'Accept' : 'application/json', 'Authorization' : 'Bearer ' + token}
});
instance.get('/sites?page=1')
.then(response => {
console.log(response.data.data);
dispatch(setSearchedSites({sites: response.data.data}));
}).catch(error => {
console.log(error);
});
}
}
export function setSearchedSites({ sites }) {
return {
type: types.SET_SEARCHED_SITES,
sites,
}
}
Root file for navigation based on react-navigation
I made it as simple as possible for this example.
import React from 'react';
import {StackNavigator} from 'react-navigation';
import Home from '../screens/Home';
export const Root = StackNavigator({
Home: {
screen: Home,
}
});
And finally my Home screen
import React, {Component} from 'react';
import { connect } from 'react-redux';
import {Text, View} from 'react-native';
class Home extends Component {
componentDidMount()
{
let token = "12345678" //Just for this example
this.props.fetchSites(token).then( (response) => {
console.log(response);
});
}
render() {
return (
<View>
<Text>This is the Home view</text>
</View>
);
}
}
function mapStateToProps(state) {
return {
searchedSites: state.searchedSites
};
}
export default connect(mapStateToProps)(Home);
To use action methods you need to connect in home screen like this
import { fetchSites } from '<your-path>'
// your Home's other code.
const mapDispatchToProps = (dispatch) => {
return{
fetchSites:dispatch(fetchSites())
}
}
export default connect(mapStateToProps,mapDispatchToProps)(Home);
after that you can use fetchSites as this.props.fetchSites whenever you want.