How to implement jest unit test with react-navigation - react-native

I currently working on adding jest unit test for the react-navigation, for example:
My StackNavigator
const Nav = StackNavigator({
Home: {
screen: Home,
},
Second: {
screen: Second,
}
});
export default class App extends Component<{}> {
render() {
return (
<Nav/>
);
}
}
My Home component
export default class Home extends Component<{}> {
_goToNextPage = () => {
this.props.navigation.navigate('Second');
}
render() {
return (
<View>
<Text>Home</Text>
<Button
onPress={this._goToNextPage}
title="Go to Second Page"
>Click to next page</Button>
</View>
);
}
}
My Second component
export default class Second extends Component<{}> {
render() {
return (
<View>
<Text>Second</Text>
</View>
);
}
}
How should I write jest unit test to test "when I click the GoToNextPage button, and the Second Component should be rendered correctly ?"
I do not find any useful info about jest with react-navigation, Any help will be much appreciated!!!
Thanks a lot~

I personally like #testing-library/react for this because of their philosophy around testing from the user perspective. I'd imagine your test would look something like the following:
it('shows the second component when the next button is clicked', async () => {
// Renders your app
const { getByText, findByText } = render(<App />);
// Clicks your button
fireEvent.click(getByText('Click to next page'));
// Waits for the 'Second' text to be visible, could be async or sync, in this
// case I'm using the async expectation style
await wait(() => expect(getByText('Second')));
});
I would test this in a way that separates the concerns of the click of the button and the validation that it renders properly by simply rendering the second component and checking it against your snapshot.
// inside your second component test - still using testing-library
it('matches the snapshot', () => {
expect(render(<Second />).container).toMatchSnapshot();
});

Related

React-Navigation passing functions to screenProps

I am creating an app using React Native, Expo and React Navigation, and I don't know the correct method for passing props and functions around.
I have copied the Expo method for navigators that I will build on, but right now I just have App.js calling the following
AppNavigator -> MainSwitchNavigator -> HomeScreen
I have then wrapped the main exported App that Expo expects with the Amazon AWS Amplify HOC withAuthenticator. Now I can log in to my app and show hello world securely using Amazon's cognito service.
If I want to log out I can use the signout function bellow that I currently have on the props of App.
class App extends React.Component {
constructor(props) {
super(props);
this.signOut = this.signOut.bind(this);
}
signOut() {
Auth.signOut().then(() => {
this.props.onStateChange('signedOut', null);
console.log("signed out");
}).catch(e => {
console.log(e);
});
}
render () {
return (
<View style={styles.container}>
<AppNavigator screenProps={{
signOut: () => {this.signOut}
}}/>
</View>
);
}
}
export default withAuthenticator(App)
For now I just want to pass this function down to my Home Screen so I can add a button and log out.
AppNavigator
export default createAppContainer(
createSwitchNavigator({
// You could add another route here for authentication.
// Read more at https://reactnavigation.org/docs/en/auth-flow.html
Main: { screen: MainSwitchNavigator, params: { signOut: this.props.screenProps.signOut } },
},
{
initialRouteName: 'Main'
})
);
MainSwitchNavigator
export default createSwitchNavigator({
// Home: { screen: HomeScreen }
Home: { screen: HomeScreen, params: { signOut: this.props.navigation.params.signOut } }
},{
initialRouteName: 'Home'
});
HomeScreen
class HomeScreen extends Component {
render () {
return (
<View>
<Text>Main App Here</Text>
<Button
onPress={this.props.navigation.params.signOut()}
title="Sign Out"
/>
</View>
)};
}
export default HomeScreen;
At the moment I get the error
undefined is not an object (evaluating 'this.props.navigation')
<unknown>
MainSwitchNavigator.js
8:62
Which puts its at the point I'm trying to read the props passed in to MainSwitchNavigator.
So my question is, what is good practice on sharing functions and state with screens below the main App and how do I pass the signOut function down to the rest of my components?
=======================================================================
EDIT
I have since worked out how to use screenProps correctly for variables but not for functions. screenProps is passed down through navigators automatically to the screen. So I only have to pass it to the AppNavigator component once and I can access it in HomeScreen. However any functions are not passed down.
E.g for variables, if I modify App.js to pass text to the variable signOut and pass that to screenProps
class App extends React.Component {
constructor(props) {
super(props);
this.signOut = this.signOut.bind(this);
}
signOut() {
Auth.signOut().then(() => {
this.props.onStateChange('signedOut', null);
console.log("signed out");
}).catch(e => {
console.log(e);
});
}
render () {
return (
<View style={styles.container}>
<AppNavigator screenProps={{signOut: "some text"}}/>
</View>
);
}
}
HomeScreen will then show this as part of the this.props object.
class HomeScreen extends Component {
render () {
return (
<View>
<Text>Main App Here</Text>
<Button
onPress={console.log(JSON.stringify(this.props))}
// onPress={alert(this.props.screenProps.var3)}
title="Sign Out"
/>
</View>
)};
}
export default HomeScreen;
However if I pass a function to AppNavigator instead and do this
<AppNavigator screenProps={{signOut: () => {this.signOut}}}/>
screenProps does not get set and I can't access the function signOut.
The only thing I can get to work is this
<AppNavigator screenProps={this.signOut}/>
But what I need is to create a property of screenProps and pass this down. Any thoughts?
passing a function as a screenProp should be
<AppNavigator screenProps={{signOut: this.signOut}} />
then to use do
this.props.screenProps.signOut()
MainSwitchNavigator
export default createSwitchNavigator({
// Home: { screen: HomeScreen }
Home: { screen: HomeScreen }
},{
initialRouteName: 'Home'
});
to send data on navigate
this.props.navigation.navigate('Signout' , { name: 'John Doe', email: 'john#doe.com' })

Use a method in another class and navigation

I am coding an application that is neccessary to login users and I am experiencing a problem.
I have in my App component a getUser () method that I would like to call from other components. I also have a Login () method that allows you to navigate to another page. Only the problem is that I can not export the StackNavigator and App at the same time. Would there be a way to export the StackNavigator and my function? Or an other solution that would solve my problem?
class App extends Component<{}> {
//get the user and return it
_getUser() {
return user
}
//authentify the user and go to is profil
_userLogin() {
navigate('Profil');
}
render() {
const { navigate } = this.props.navigation;
return (
<View>
<Button
title="Login"
onPress={() => this.Login() }
/>
</View>
);
};
}
export default StackNavigator({
Home: { screen: App },
Profil: { screen: secondScreen },
});
Thanks for your help.
Finally i found a solution, I ll store the user in an asynchron storage.

Where to initialize data loading with react-navigation

I'm using react-navigation and here is my structure :
The root stack navigator :
export const Root = StackNavigator({
Index: {
screen: Index,
navigationOptions: ({ navigation }) => ({
}),
},
Cart: {
screen: Cart,
navigationOptions: ({ navigation }) => ({
title: 'Votre panier',
drawerLabel: 'Cart',
drawerIcon: ({ tintColor }) => <Icon theme={{ iconFamily: 'FontAwesome' }} size={26} name="shopping-basket" color={tintColor} />
}),
},
...
My structure looks like this :
StackNavigator (Root)
DrawerNavigator (Index)
TabNavigator
MyPage
MyPage (same page formatted with different datas)
...
So my question is, where do I load my data, initialize my application ? I need somewhere called once, called before the others pages.
The first page displayed in my application is the MyPage page. But as you can see, because of the TabNavigator, if I put my functions inside, it will be called many times.
Some will says in the splashscreen, but I'm using the main splashscreen component and I don't have many controls over it.
I thought about my App.js where we create the provider, but I don't think this is a good idea ?
const MyApp = () => {
//TODO We're loading the data here, I don't know if it's the good decision
ApplicationManager.loadData(store);
SplashScreen.hide();
return (
<Provider store={store}>
<Root/>
</Provider>
);
};
What is the good way to do it ?
class MyApp extends Component {
state = {
initialized: false
}
componentWillMount() {
// if this is a promise, otherwise pass a callback to call when it's done
ApplicationManager.loadData(store).then(() => {
this.setState({ initialized: true })
})
}
render() {
const { initialized } = this.state
if (!initialized) {
return <SplashScreen />
}
return (
<Provider store={store} >
<Root />
</Provider>
);
}
}
TabNavigator by default renders/loads all its child components at the same time, but if you set property lazy: true components will render only if you navigate. Which means your functions will not be called many times.
const Tabs = TabNavigator(
{
MyPage : {
screen: MyPage
},
MyPage2 : {
screen: MyPage,
}
}
},
{
lazy: true
}
);
If you use this structure and call fetching data inside of MyPage you can add logic in componentWillReceiveProps that will check is data already in store and/or is it changed before fetching new data. Calling your fetch functions from MyPage gives you the ability to pull fresh data on every page/screen visit or do "pull to refresh" if you need one.
You could also pull initial data in splashscreen time, I would just not recommend pulling all your app data, data for all screens, at that time since you probably don't need it all at once. You can do something like:
class MyApp extends Component {
state = {
initialized: false
}
componentWillMount() {
// if this is a promise, otherwise pass a callback to call when it's done
ApplicationManager.loadData(store).then(() => {
this.setState({ initialized: true })
})
}
render() {
const { initialized } = this.state
if (!initialized) {
return null
}
return (
<Provider store={store} >
<Root />
</Provider>
);
}
}
class Root extends Component {
componentDidMount() {
SplashScreen.hide();
}
...
}
You should do it in App.js or where you initialize your StackNavigator. If I were you, I would put a loading screen, which would get replaced by the StackNavigator structure once the data is ready.
I wouldn't do it in the App because you lose control. Sadly I haven't used react-navigation or redux but I see that the TabNavigator has a tabBarOnPress method, which I would use to trigger the loading. You can load every page data on demand.
https://reactnavigation.org/docs/navigators/tab#tabBarOnPress

React Native Redux store dispatches reducers correctly, but doesn't update UI component

Working on a cancer treatment app in react native:
Current functionality: when I move the sliders and change the date on my app, it dispatches changes to the redux store successfully. Unfortunately, my UI doesn't update, even though I am calling the same store from the presentational components that I called for dispatch.
That results in this:
GIF of redux store changing while UI is static
Printing via
store.getState();
store.subscribe(() =>
console.log(store.getState())
);
I tried using subscription, but it seems like this isn't the right way to go about this. Thoughts?
snippets from my code (all in one small file, linked below)
Action
//action
function set_num_treatments(num) {
return {
type: SET_NUM_TREATMENTS,
num: num
}
}
setting the title
SET_NUM_TREATMENTS = "SET_NUM_TREATMENTS"
main reducer
function main_reducer(state = initialState, action) {
switch (action.type) {
case SET_PAGE_VIEW:
return Object.assign({}, state, {
current_page: action.page_of_interest
})
case SET_NUM_TREATMENTS:
return Object.assign({}, state, {
num_treatments: action.num
})
case SET_INTER_TREATMENT_INTERVAL:
return Object.assign({}, state, {
inter_treatment_interval: action.weeks_between_treatments
})
case SET_TREATMENT_START_DATE:
return Object.assign({}, state, {
treatment_start_date: action.date
})
default:
return state
}
return state
}
Here's where I start the store & produce the printing functionality
let store = createStore(main_reducer);
store.getState();
store.subscribe(() =>
console.log(store.getState())
);
here's the presentational components
class TreatmentSettings extends React.Component {
constructor(props){
super(props)
}
render() {
const props = this.props
const {store} = props
const state = store.getState()
return(
<View style={styles.treatment_option_slider_card}>
<Text style={styles.my_font, styles.tx_settings_header}>{state.num_treatments} Treatments</Text>
<Slider step={1} minimumValue={1} maximumValue={20} value={12}
onValueChange={(num_treatments) => {store.dispatch(set_num_treatments(num_treatments))}} />
<Text style={styles.my_font, styles.tx_settings_header}>X Weeks Between Treatments</Text>
<Slider step={1} minimumValue={1} maximumValue={4} value={2} style={{marginBottom:60}}
onValueChange={(value) => {store.dispatch(set_inter_treatment_interval(value))}}
/>
</View>
)
}
}
These final two components hold the main containers for the app
export default class App extends React.Component {
constructor(props) {
super(props);
}
render() {
return (
<Provider store={createStore(main_reducer)}>
<AppContainer />
</Provider>
);
}
}
class AppContainer extends React.Component {
constructor(props){
super(props)
}
render(){
return(
<View style={styles.container}>
<TreatmentSettings store={store} />
<Text>footertext</Text>
</View>
)
}
}
the one gist file is here if you want to see it all: https://github.com/briancohn/learning-redux/blob/navigation_addn/App.js
I really appreciate the help—
Thanks in advance!
-Brian
I think the way you are updating the store is fine but there’s something wrong with how your components are listening to the changes.
It seems you meant to use connect from react-redux for the containers to connect to the store. Then you can use mapStateToProps to get the data from the store to pass into the components as props. Check https://github.com/reactjs/react-redux/blob/master/docs/api.md#connectmapstatetoprops-mapdispatchtoprops-mergeprops-options for example.

React Native Change Page after 5 seconds

I'm newbie in react native and I don't know how to change page after 5 seconds.
I create an android.index.js file that will navigate to LandingPage.js. What I want to do is, when the LandingPage being loaded, it will wait for 5 seconds and then redirect / navigate to another page.
index.android.js
export default class DefaultProject extends Component {
render() {
return (
<Navigator
renderScene={(route, navigator) =>
<LandingPage/>
}
/>
)
LandingPage.js
export default class LandingPage extends Component {
render() {
return (
<Image source={require('./images/event3.jpeg')}
style={styles.container} />
//How to redirect to another page from here after 5 secs?
);
}
}
You can use a simple setTimeout, as you would in a standard JS setup:
export default class LandingPage extends Component {
componentDidMount(){
// Start counting when the page is loaded
this.timeoutHandle = setTimeout(()=>{
// Add your logic for the transition
}, 5000);
}
componentWillUnmount(){
clearTimeout(this.timeoutHandle); // This is just necessary in the case that the screen is closed before the timeout fires, otherwise it would cause a memory leak that would trigger the transition regardless, breaking the user experience.
}
render() {
return (
<Image source={require('./images/event3.jpeg')}
style={styles.container} />
//How to redirect to another page from here after 5 secs?
);
}
}
I'm using lodash for this:
export default class Splash extends React.Component {
constructor(props) {
super(props);
}
async componentWillMount() {
_.delay(() => this.props.navigator.replace({ component: 'login' }), 1000);
}
render() {
return (
...
);
}
}
This will only work if you have a Navigator set up. Check this article:
React Native Navigator — Navigating Like A Pro in React Native