React Native Test Button Press - react-native

I am trying to test calling a component method from a React Native Button element.
For some reason, the test fails unless I do BOTH of these things.
wrapper.find(Button).first().props().onPress();
wrapper.find(Button).first().simulate('press');
If I comment out either of the lines, the test fails indicating that expect(instance.toggleEmailPasswordModal).toHaveBeenCalled(); failed.
Here is my component:
import React, { Component } from 'react';
import { Button, SafeAreaView, Text } from 'react-native';
import EmailPasswordModal from './EmailPasswordModal/EmailPasswordModal';
class Login extends Component {
state = {
emailPasswordModalVisible: false,
};
toggleEmailPasswordModal = () => {
console.log('TOGGLED!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!');
const { emailPasswordModalVisible } = this.state;
this.setState({ emailPasswordModalVisible: !emailPasswordModalVisible });
};
render() {
const { emailPasswordModalVisible } = this.state;
return (
<SafeAreaView>
<EmailPasswordModal
visible={ emailPasswordModalVisible }
close={ this.toggleEmailPasswordModal }
/>
<Text>Login Screen!</Text>
<Button
onPress={ this.toggleEmailPasswordModal }
title="Login with Email and Password"
color="#841584"
accessibilityLabel="Login with Email and Password"
/>
</SafeAreaView>
);
}
}
export default Login;
Here is my test:
import React from 'react';
import ShallowRenderer from 'react-test-renderer/shallow';
import { shallow } from 'enzyme';
import { Button } from 'react-native';
import Login from './Login';
describe('Login Screen', () => {
describe('Snapshot Tests', () => {
it('renders the screen with default state', () => {
const renderer = new ShallowRenderer();
const props = {};
renderer.render(<Login { ...props } />);
expect(renderer.getRenderOutput()).toMatchSnapshot();
});
});
describe('Functional Tests', () => {
it('calls the toggleEmailPasswordModal method', () => {
const wrapper = shallow(<Login />);
const instance = wrapper.instance();
jest.spyOn(instance, 'toggleEmailPasswordModal');
wrapper.find(Button).first().props().onPress();
wrapper.find(Button).first().simulate('press');
expect(instance.toggleEmailPasswordModal).toHaveBeenCalled();
});
});
});
Oddly, when the test runs, the output shows "TOGGLED!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!" twice because of the logging in the component.
However, if I change the expect to :
expect(instance.toggleEmailPasswordModal).toHaveBeenCalledTimes(1);
the test passes.
If I change the expect to :
expect(instance.toggleEmailPasswordModal).toHaveBeenCalledTimes(2);
the test fails saying toggleEmailPasswordModal was only called 1 time.
Why do I need BOTH of those wrapper.find(Button)... lines? I've never seen any other tests requiring both of them.
Thanks,
Justin
UPDATE:
I updated my test as follows:
it('calls the toggleEmailPasswordModal method', () => {
const wrapper = shallow(<Login />);
const instance = wrapper.instance();
jest.spyOn(instance, 'toggleEmailPasswordModal');
wrapper.find(Button).first().props().onPress();
wrapper.find(Button).first().simulate('press');
expect(instance.toggleEmailPasswordModal).toHaveBeenCalled();
// I ADDED THIS SECTION HERE
expect(instance.state.emailPasswordModalVisible).toBe(true);
});
The test fails because instance.state.emailPasswordModalVisible = false. That's strange as the toggleEmailPasswordModal apparently is called. However, since I suspect it's actually being called twice, I update the test as follows:
it('calls the toggleEmailPasswordModal method', () => {
const wrapper = shallow(<Login />);
const instance = wrapper.instance();
jest.spyOn(instance, 'toggleEmailPasswordModal');
wrapper.find(Button).first().props().onPress();
// CHANGES START HERE
// wrapper.find(Button).first().simulate('press');
// expect(instance.toggleEmailPasswordModal).toHaveBeenCalled();
expect(instance.state.emailPasswordModalVisible).toBe(true);
});
Guess what? The test passes properly. So CLEARLY calling the wrapper.find... functions twice truly is calling the toggleEmailPasswordModal method twice. So, why does it fail to detect it if I don't call twice? Why does it improperly believe the method has only been called once?

I have an answer finally. According to Jest spyOn function called, I need to do instance.forceUpdate() to attach the spy to the component.
it('calls the toggleEmailPasswordModal method', () => {
const wrapper = shallow(<Login />);
const instance = wrapper.instance();
const spy = jest.spyOn(instance, 'toggleEmailPasswordModal');
// This is added per https://stackoverflow.com/questions/44769404/jest-spyon-function-called/44778519#44778519
instance.forceUpdate();
wrapper.find(Button).first().props().onPress();
expect(spy).toHaveBeenCalledTimes(1);
expect(instance.state.emailPasswordModalVisible).toBe(true);
});
Now, the test passes!

Related

How to write a jest test for opening of an URL in react native?

I'm trying to write a test case for testing URL in my react native app this is my mock
import { Linking } from "react-native";
jest.mock('react-native/Libraries/Linking/Linking', () => {
return {
openURL: jest.fn()
}
})
Linking.openURL.mockImplementation(() => true)
and this is my test
test('open google url',async ()=>{
expect(Linking.openURL()).toHaveBeenCalled('https://www.google.com/')
})
but I get this error what should I do?
Name the mock function in a constant and then test if that function has been called. Here's how you would set up:
import * as ReactNative from "react-native";
const mockOpenURL = jest.fn();
jest.spyOn(ReactNative, 'Linking').mockImplementation(() => {
return {
openURL: mockOpenURL,
}
});
and then you can test this way (my example uses react-testing-library, but you can use whatever). Note you should use toHaveBeenCalledWith(...) instead of toHaveBeenCalled(...)
test('open google url', async () => {
// I assume you're rendering the screen here and pressing the button in your test
// example code below
const { getByTestId } = render(<ScreenToTest />);
await act(async () => {
await fireEvent.press(getByTestId('TestButton'));
});
expect(mockOpenURL.toHaveBeenCalledWith('https://www.google.com/'));
});
If I understoof your question then you can use react-native-webview.
import WebView from 'react-native-webview';
export const WebView: React.FC<Props> = ({route}) => {
const {url} = route.params;
<WebView
source={{uri: url}}
/>
);
};
This is how I use my webview screen for any url I need to open (like terms and conditions, etc...)

test content of a Text element in a stateful component

I am using react-native-testing-library. My component is quite simple:
import React, {Component} from 'react';
import {Text, View} from 'react-native';
import {information} from './core/information';
export default class Logo extends Component {
constructor() {
super();
this.state = {
name: ''
};
information()
.then((details) => {
this.setState({
name: details['name']
});
})
.catch((e) => {
console.log(e);
});
}
render() {
return (
<>
<View>
<Text>{this.state.name}</Text>
</View>
</>
);
}
}
I want to make sure contains the right content. I tried the following but it is failing:
import * as info from "./lib/information";
it('displays correct text', () => {
const spy = jest.spyOn(info, 'information')
const data = {'name':'name'}
spy.mockResolvedValue(Promise.resolve(data));
const {queryByText, debug} = render(<Logo />);
expect(queryByText(data.name)).not.toBeNull();
expect(spy).toHaveBeenCalled();
});
I can confirm the function information() was spied on correctly but still debug(Logo) shows the Text element with empty string.
If it's correctly spying you can try this. I encourage you to use the testID props for the components
render() {
return (
<>
<View>
<Text testID="logo-text">{this.state.name}</Text>
</View>
</>
);
}
import * as info from "./lib/information";
import { waitForElement, render } from "react-native-testing-library";
it('displays correct text', () => {
const spy = jest.spyOn(info, 'information')
const data = {'name':'name'}
//this is already resolving the value, no need for the promise
spy.mockResolvedValue(data);
const {getByTestId, debug} = render(<Logo />);
//You better wait for the spy being called first and then checking
expect(spy).toHaveBeenCalled();
//Spy function involves a state update, wait for it to be updated
await waitForElement(() => getByTestId("logo-text"));
expect(getByTestId("logo-text").props.children).toEqual(data.name);
});
Also, you should move your information call inside a componentDidMount

How Redirect to Login if page is protected and the user is not signed in?

In my App I have some public screens that are accessible even if the user is not logged in, and some screens are protected (you must be logged in to access them).
My solution to the problem is to check the component willFocus Listener and if not logged in, the user should be redirected to the loginPage.
export async function ProtectRoute(navigation){
//if page will enter and the user is not authenticated return to login
navigation.addListener(
'willFocus',
async () => {
let token = await getTokenAsync();
if(!token){
navigation.navigate('Login');
}
})
}
In my screen I Call this function in ComponentWillMount lifecycle.
The issue is that it takes like a second to verify the token and the page is displayed briefly.
How can I make it so that he goes directly to the Login Page without that lag ?
I wrote a quick example below. You can examine and use it.
import React, { Component } from "react";
import { Text, View } from "react-native";
const withAuth = WrappedComponent => {
class AuthenticationScreen extends Component {
constructor(props) {
super(props);
this.state = {
isAuthenticated: false
};
props.navigation.addListener("willFocus", async () => {
await this.checkAuth();
});
}
remoteReuqest = async () => {
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve(true);
}, 2000);
});
};
checkAuth = async () => {
const result = await this.remoteReuqest();
if (result) {
this.setState({
isAuthenticated: true
});
} else {
this.props.navigation.navigate("Login");
}
};
render() {
if (!this.state.isAuthenticated) {
return <Text>Waiting...</Text>;
}
return <WrappedComponent {...this.props} />;
}
}
return AuthenticationScreen;
};
export default withAuth;
You can use it as follows.
import React, { Component } from "react";
import { Text, StyleSheet, View } from "react-native";
import withAuth from "./withAuth";
class ContactScreen extends Component {
render() {
return (
<View>
<Text> Contact Screen </Text>
</View>
);
}
}
const styles = StyleSheet.create({});
const extendedComponent = withAuth(ContactScreen);
extendedComponent.navigationOptions = {
title: "Contact"
};
export default extendedComponent;
The issue is that it takes like a second to verify the token and the page is displayed briefly.
The reason is because reading/writing from/to AsyncStorage is an asychronous operation.
In my screen I Call this function in ComponentWillMount lifecycle.
I suggest you to not use ComponentWillMount lifecycle because it's deprecated and it will be removed from React (https://reactjs.org/docs/react-component.html#unsafe_componentwillmount)
After this introduction, now i show you how I have achieved this in my app: CONTEXT API! (https://reactjs.org/docs/context.html)
Context provides a way to pass data through the component tree without having to pass props down manually at every level.
How to implement context api:
the context will be the 'state' of your App.js file. You root App.js will be the provider of the context, while other views which will need the context are called the consumers of the context.
First of all, you need to create a 'skeleton' of your context into a separate file, something like this:
// AuthContext.js
import React from 'react'
const AuthContext = React.createContext({
isLogged: false,
login: () => {},
logout: () => {}
})
export default AuthContext
Your App.js will import, contain and initialize the context:
// App.js
// all necessary imports
import AuthContext from '....'
export default class App extends React.Component {
constructor (props) {
super(props)
this.state = {
isAuth: false,
login: this.login,
logout: this.logout
}
login = async userData => {
// do your login stuff and then
this.setState({ isAuth: true })
}
logout = async () => {
// do your logout stuff and then
this.setState({ isAuth: false })
}
async ComponentDidMount () {
// check the asyncStorage here and then, if logged:
this.setState({ isAuth: true })
}
render () {
return (
<AuthContext.Provider value={this.state}>
<AppContainer />
</AuthContext.Provider>
)
}
Then, into the View contained into AppContainer, you could access context like this:
import AuthContext from '.....'
// all necessary imports
export default class YourView extends React.Component<Props, State> {
constructor (props) {
super(props)
this.props = props
this.state = { ... }
}
// THIS IS IMPORTANT
static contextType = AuthContext
// with this, you can access the context through 'this.context'
ComponentDidMount () {
if (!this.context.isAuth) this.props.navigation.navigate('login')
}
Advantages of this approach:
Checking a boolean is so fast that you will not notice a blank screen.
Sharing an Authentication Context everywhere in you app
Making the access to asyncstorage only the first time that app mounts and not everytime you need to check if the user is logged
Sharing methods to login/logout everywhere in your app

How to listen to route changes in react router v4?

I have a couple of buttons that acts as routes. Everytime the route is changed, I want to make sure the button that is active changes.
Is there a way to listen to route changes in react router v4?
I use withRouter to get the location prop. When the component is updated because of a new route, I check if the value changed:
#withRouter
class App extends React.Component {
static propTypes = {
location: React.PropTypes.object.isRequired
}
// ...
componentDidUpdate(prevProps) {
if (this.props.location !== prevProps.location) {
this.onRouteChanged();
}
}
onRouteChanged() {
console.log("ROUTE CHANGED");
}
// ...
render(){
return <Switch>
<Route path="/" exact component={HomePage} />
<Route path="/checkout" component={CheckoutPage} />
<Route path="/success" component={SuccessPage} />
// ...
<Route component={NotFound} />
</Switch>
}
}
To expand on the above, you will need to get at the history object. If you are using BrowserRouter, you can import withRouter and wrap your component with a higher-order component (HoC) in order to have access via props to the history object's properties and functions.
import { withRouter } from 'react-router-dom';
const myComponent = ({ history }) => {
history.listen((location, action) => {
// location is an object like window.location
console.log(action, location.pathname, location.state)
});
return <div>...</div>;
};
export default withRouter(myComponent);
The only thing to be aware of is that withRouter and most other ways to access the history seem to pollute the props as they de-structure the object into it.
As others have said, this has been superseded by the hooks exposed by react router and it has a memory leak. If you are registering listeners in a functional component you should be doing so via useEffect and unregistering them in the return of that function.
v5.1 introduces the useful hook useLocation
https://reacttraining.com/blog/react-router-v5-1/#uselocation
import { Switch, useLocation } from 'react-router-dom'
function usePageViews() {
let location = useLocation()
useEffect(
() => {
ga.send(['pageview', location.pathname])
},
[location]
)
}
function App() {
usePageViews()
return <Switch>{/* your routes here */}</Switch>
}
You should to use history v4 lib.
Example from there
history.listen((location, action) => {
console.log(`The current URL is ${location.pathname}${location.search}${location.hash}`)
console.log(`The last navigation action was ${action}`)
})
withRouter, history.listen, and useEffect (React Hooks) works quite nicely together:
import React, { useEffect } from 'react'
import { withRouter } from 'react-router-dom'
const Component = ({ history }) => {
useEffect(() => history.listen(() => {
// do something on route change
// for my example, close a drawer
}), [])
//...
}
export default withRouter(Component)
The listener callback will fire any time a route is changed, and the return for history.listen is a shutdown handler that plays nicely with useEffect.
import React, { useEffect } from 'react';
import { useLocation } from 'react-router';
function MyApp() {
const location = useLocation();
useEffect(() => {
console.log('route has been changed');
...your code
},[location.pathname]);
}
with hooks
With hooks:
import { useEffect } from 'react'
import { withRouter } from 'react-router-dom'
import { history as historyShape } from 'react-router-prop-types'
const DebugHistory = ({ history }) => {
useEffect(() => {
console.log('> Router', history.action, history.location)
}, [history.location.key])
return null
}
DebugHistory.propTypes = { history: historyShape }
export default withRouter(DebugHistory)
Import and render as <DebugHistory> component
import { useHistory } from 'react-router-dom';
const Scroll = () => {
const history = useHistory();
useEffect(() => {
window.scrollTo(0, 0);
}, [history.location.pathname]);
return null;
}
With react Hooks, I am using useEffect
import React from 'react'
const history = useHistory()
const queryString = require('query-string')
const parsed = queryString.parse(location.search)
const [search, setSearch] = useState(parsed.search ? parsed.search : '')
useEffect(() => {
const parsedSearch = parsed.search ? parsed.search : ''
if (parsedSearch !== search) {
// do some action! The route Changed!
}
}, [location.search])
in this example, Im scrolling up when the route change:
import React from 'react'
import { useLocation } from 'react-router-dom'
const ScrollToTop = () => {
const location = useLocation()
React.useEffect(() => {
window.scrollTo(0, 0)
}, [location.key])
return null
}
export default ScrollToTop
In some cases you might use render attribute instead of component, in this way:
class App extends React.Component {
constructor (props) {
super(props);
}
onRouteChange (pageId) {
console.log(pageId);
}
render () {
return <Switch>
<Route path="/" exact render={(props) => {
this.onRouteChange('home');
return <HomePage {...props} />;
}} />
<Route path="/checkout" exact render={(props) => {
this.onRouteChange('checkout');
return <CheckoutPage {...props} />;
}} />
</Switch>
}
}
Notice that if you change state in onRouteChange method, this could cause 'Maximum update depth exceeded' error.
For functional components try useEffect with props.location.
import React, {useEffect} from 'react';
const SampleComponent = (props) => {
useEffect(() => {
console.log(props.location);
}, [props.location]);
}
export default SampleComponent;
For React Router v6 & React Hooks,
You need to use useLocation instead of useHistory as it is deprecated
import { useLocation } from 'react-router-dom'
import { useEffect } from 'react'
export default function Component() {
const history = useLocation();
useEffect(() => {
console.log('> Router', history.pathname)
}, [history.pathname]);
}
With the useEffect hook it's possible to detect route changes without adding a listener.
import React, { useEffect } from 'react';
import { Switch, Route, withRouter } from 'react-router-dom';
import Main from './Main';
import Blog from './Blog';
const App = ({history}) => {
useEffect( () => {
// When route changes, history.location.pathname changes as well
// And the code will execute after this line
}, [history.location.pathname]);
return (<Switch>
<Route exact path = '/' component = {Main}/>
<Route exact path = '/blog' component = {Blog}/>
</Switch>);
}
export default withRouter(App);
I just dealt with this problem, so I'll add my solution as a supplement on other answers given.
The problem here is that useEffect doesn't really work as you would want it to, since the call only gets triggered after the first render so there is an unwanted delay.
If you use some state manager like redux, chances are that you will get a flicker on the screen because of lingering state in the store.
What you really want is to use useLayoutEffect since this gets triggered immediately.
So I wrote a small utility function that I put in the same directory as my router:
export const callApis = (fn, path) => {
useLayoutEffect(() => {
fn();
}, [path]);
};
Which I call from within the component HOC like this:
callApis(() => getTopicById({topicId}), path);
path is the prop that gets passed in the match object when using withRouter.
I'm not really in favour of listening / unlistening manually on history.
That's just imo.

How to test arrow function in React ES6 class

I used arrow function inside of my React component to avoid binding this context, for example my component look like this;
class Comp extends Component {
_fn1 = () => {}
_fn2 = () => {}
render() {
return (<div></div>);
}
}
How do I test _fn1 and _fn2 function in my test cases? Because these kind of function did not associated with React component itself, so when I do
fnStub = sandbox.stub(Comp.prototype, "_fn1");
it is not going work, since _fn did not bind with Comp.prototype. Thus, how can I test those functions in React if I want to create function with arrow syntax? Thanks!
ES6 functions or arrow functions are not added to the class prototype.
However, there are a couple of ways to test them:-
Test that the functions themselves are called when a suitable event occurs
ES5 functions exist on the class prototype and something like this is possible:
import Component from 'path/to/component';
import { shallow } from 'enzyme';
describe(<Component>, () => {
it('should call handleSubmit', () => {
const spy = jest.spyOn(Component.prototype, 'handleSubmit');
const wrapper = shallow(<Component />);
...
//Invoke handleSubmit
...
expect(spy).toBeCalled()
});
});
whereas ES6 functions exist on the instance of the mounted component(you can also use shallow)
import Component from 'path/to/component';
import { mount } from 'enzyme';
describe(<Component>, () => {
it('should call handleSubmit', () => {
const wrapper = mount(<Component />);
...
const spy = jest.spyOn(wrapper.instance(), 'handleSubmit');
//update the instance with the new spy
wrapper.instance().forceUpdate();
...
//invoke handleSubmit
expect(spy).toBeCalled()
});
});
Test their functionality by simulating actions that will invoke these functions and test for the expected behavior
Assuming component content such as:
state = {
title: 'Current Title'
};
updateTitle = (event) => {
title = event.target.value;
this.setState({ title });
}
render() {
return (
<div>
<input type="text" value={this.state.title} onChange={this.updateTitle} />
<div>
)
}
Test
...
wrapper.find('input').simulate('change', {target: {value: 'New title'}});
expect(wrapper.state().title).toBe('New Title');
...
I hope this helps.
In general I find it easier to test that these functions have resulted in a correct component state, rather than test the function itself. For example, here is a component that toggles a state variable when a button is clicked:
class MyComponent extends Component {
state = {
toggle: false
}
_fn1 = () => {
this.setState(previousState => ({
toggle: !previousState.toggle
});
}
render() {
const { toggle } = this.state;
return (
<button onClick={this.clickHandler}>
Turn me {toggle ? 'on' : 'off'}
</button>
);
}
}
My preferred approach here would be to test the component as a whole, i.e. the "unit" of the unit test is the component. The test would therefore find the button, simulate a click, and ensure that the correct text is shown. This may not be a textbook unit test, but it achieves the goal of testing the component.
Using sinon/chai/mocha/enzyme:
describe('My Component', () => {
it('alternates text display when the button is clicked', () => {
const wrapper = shallow(<MyComponent />);
expect(wrapper).to.have.text('Turn me off');
wrapper.find('button').simulate('click');
expect(wrapper).to.have.text('Turn me on');
});
});