Can a component method which modifies its navigation prop be unit tested? - react-native

I need to figure out how to test a component method that doesn't return a value and doesn't change any of the states of its component, all it does is push another screen.
I'm using jest and enzyme to access a class methods and states.
This is the method I want to test (if possible):
signUp() {
this.props.navigation.push('Signup');
}

Yep, just pass in a mock for the navigation prop:
import * as React from 'react';
import { shallow } from 'enzyme';
class SimpleComponent extends React.Component {
signUp() {
this.props.navigation.push('Signup');
}
render() { return null; }
}
test('signUp', () => {
const navigationMock = { push: jest.fn() };
const wrapper = shallow(<SimpleComponent navigation={navigationMock}/>);
wrapper.instance().signUp();
expect(navigationMock.push).toHaveBeenCalledWith('Signup'); // Success!
});

Related

How to use useNavigation() Hook in Redux actions React Native

I want to navigate user from my redux actions. For example when they
click on login, they navigate from action
.
Two ways i have tried.
1.pass navigation prop from component to action. (it works fine.)
2. use useNavigation() hook in redux actions. (it is not working. (Hooks can only be called inside of the body of a function component)).
Here is my code
action.js
export const registerUser = (data) => {
const navigation = useNavigation()
return async dispatch => {
dispatch(authLoading());
try {
const res = await axios.post(
`${BASE_URL}/mobilesignup`,
data,
{
headers: {
Accept: 'application/json',
'Content-Type': 'application/json',
},
},
);
console.log(res);
dispatch(registerSuccess(res));
navigation.navigate('dashboard')
} catch (err) {
dispatch(authFailed(err));
}
};
};
This code is not working
Error (Hooks can only be called inside of the body of a function
component)
Can anybody help me how can i use useNavigation() in redux actions ?
Thanks
You will have to use the Navigation ref which is there for purposes like calling from the reducer
The idea is to create a navigation.js and set the reference of navigation container and use it.
Code would be like below. (A sample from documentation)
//App.js
import { NavigationContainer } from '#react-navigation/native';
import { navigationRef } from './RootNavigation';
export default function App() {
return (
<NavigationContainer ref={navigationRef}>{/* ... */}</NavigationContainer>
);
}
// RootNavigation.js
import * as React from 'react';
export const navigationRef = React.createRef();
export function navigate(name, params) {
navigationRef.current?.navigate(name, params);
}
You can simply import the navigation js anywhere and call navigate
Documentation
https://reactnavigation.org/docs/navigating-without-navigation-prop/#handling-initialization
According to the documentation of react-navigation v6.x
Define your rootNavigation module as followed:
// RootNavigation.ts
import { createNavigationContainerRef } from "#react-navigation/native";
const navigationRef = createNavigationContainerRef();
export class RootNavigation {
static navigate(name: string, params: any = {}) {
if (navigationRef.isReady()) {
navigationRef.navigate(name, params);
}
}
static get ref():any {
return navigationRef;
}
}
Pass the reference to NavigationContainer located at the root of your App.
// App.js
import { NavigationContainer } from '#react-navigation/native';
import { navigationRef } from './RootNavigation';
export default function App() {
return (
<NavigationContainer ref={RootNavigation.ref}>{/* ... */}</NavigationContainer>
);
}
Then simply use it at an action creator
// any js module
// ...
RootNavigation.navigate('ChatScreen', { userName: 'Lucy' });

React navigation- import file navigate page

This file import
class Footer extends Component {
_notifications = () => {
const { navigate } = this.props.navigation;
navigate('Ntf', {});
}
render() {
return (<TouchableHighlight onPress={() => this._notifications()} ></TouchableHighlight>);
}
}
This file main ( React-Navigation - NavigationDrawerStructure ).
import { Footer } from './Footer';
export default class HomePage extends Component {
render() {
return (<View><Footer/></View>);
}
Click _notifications button after error : undefined is an object c.props.navigation.navigate
Help me please
Only the components defined in routes has access to the navigation props not the child of those components!.
Solution:-
import file
class Footer extends Component {
_notifications = () => {
this.props.NavigateToNTF()
}
render() {
return (<TouchableHighlight onPress={() => this._notifications()} ></TouchableHighlight>);
}
}
main file:-
import { Footer } from './Footer';
export default class HomePage extends Component {
render() {
return (<View><Footer NavigateToNTF={()=> this.props.navigation.navigate('Ntf', {}) } /></View>);
}
Navigation props are not available in child component that's why you are getting undefined when you call navigation props but with this solution we are sending props from the parent file (main file) to the child (export file) so that way it will work!.
If it helps! make sure to motivate me 😜 you know what i mean!.

React native reload child component

I have 2 screen A and Screen B. Screen A has a sub component SomeList which loads data and i just pass the action to the subcomponent and it does the rest.Currently by design of react-navigation no action on revisit. I googled and came across https://reactnavigation.org/docs/en/navigation-events.html but not sure how to use it to reload SomeList when i navigate from ScreenB to ScreenA.
ScreenA
class ScreenA extends Component {
render() {
return (
<SomeList
loadData={this.props.actions.getAlllist}
/>
)
}
ScreenB
Class ScreenB extends Component {
someAction = () => {
this.props.navigation.navigate("ScreenA")
}
}
Due to react-native's design, I think you'd be better off passing data to your child Component.
Here's how you can do this:
class ScreenA extends Component {
constructor(props) {
super(props)
this.state = {
data: [],
}
this.props.navigation.addListener('didFocus', this.load)
this.props.navigation.addListener('willBlur', this.unmount)
}
load = ()=>{
const _data = this.props.actions.getAlllist;
this.setState({data:_data})
}
unmount = ()=>{
this.setState({data:[]})
}
render() {
return (
<SomeList
dataArray ={this.state.data}
/>
)
}
};
If you want to fetch data each time you render something, you can use componentDidMount lifecycle method
class ScreenA extends Component {
state = {
data: [];
}
componentDidMount() {
this.setState({ data: this.props.actions.getAlllist });
}
render() {
return <SomeList loadData={this.state.data}/>;
}
So every time the ScreenA screen it's rendered it will fetch the data.

How to Re-render a component that was already rendered after activating a props in other component

I'm setting up an application in React-native where I have a:
Component A : a search component with 2 fields
Component B : a button on this page where I click on it, the 3rd field appears
This components are only linked with react-navigation
In my case, the component B is a component where I can buy premium, and I want to update the component A when premium bought.
The problem : when I already rendered the Component A, and the I go to Component B, click the button, the Component A does not re-render, how can I do it ?
I'm looking for something like this :
class ComponentA extends PureComponent {
render() {
if (userHasNotClickedOnComponentB) {
return (
<SearchForm/>
)
} else {
return (
<SearchFormPremium/>
)
}
}
}
SearchForm and SearchFormPremium are two separated Component:
One with the Premium functionalities, the other one for normal users only
I already rendered ComponentA, and then I go to ComponentB and click the button
class ComponentB extends PureComponent {
render() {
return (
<Button onClick={() => setPremium()}/>
)
}
}
How can the ComponentA re-render so i can have the changes of ComponentB ?
Thanks
You may want to look into using Redux, or something of the like to keep a centralized store that all of your components can look at. There are plenty of Redux tutorials out there so I wont go into details, but essentially it will allow you to:
1) Create a data store accessible from any 'connected' component
2) Dispatch actions from any component to update the store
When you connect a component, the connected data becomes props. So, for example, if you connected component A and B to the same slice of your store, when component A updates it, component B will automatically re-render because its props have changed.
Redux github page
Okay, with Redux it worked !
Just connect both component. In ComponentA (the component that has to be automatically updated) use the function componentWillReceiveProps() and refresh it inside of it.
In Reducer :
const initialState = {premium: false};
const tooglePremiumReducer = (state = initialState, action) => {
switch (action.type) {
case "TOOGLE_PREMIUM":
return {
...state,
premium: action.payload.premium,
};
default:
return state;
}
};
export default tooglePremiumReducer;
In Action :
export const tooglePremiumAction = (premium) => {
return dispatch => {
dispatch({
type: "TOOGLE_PREMIUM",
payload: {
premium: premium
}
});
};
};
In ComponentB :
// Import tooglePremiumAction
class ComponentB extends PureComponent {
render() {
return (
<Button onClick={() => this.props.tooglePremiumAction(true)}/>
)
}
}
const actions = {
tooglePremiumAction
};
export default connect(
actions
)(ComponentB);
In ComponentA:
class ComponentA extends PureComponent {
componentWillReceiveProps(nextProps) {
if(this.props.premium !== nextProps.premium) {
//here refresh your component
}
}
render() {
if (!this.props.premium) {
return (
<SearchForm/>
)
} else {
return (
<SearchFormPremium/>
)
}
}
}
const mapStateToProps = state => {
const premium = state.premium.premium
return { premium };
};
export default connect(mapStateToProps)(ComponentA);

How to hoist a jest dependency mock over actual dependency?

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