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');
});
});
Related
I'm implementing a solution in a React Native to avoid double taps on React Native Router Flux and I came up with this custom hook. What I really don't know is if this is a valid use case for this. Basically I created this as a hook because I'm using the useRef hook inside it. But maybe just a helper function would've been enough.
I also have a wrapper component for Pressable and a HOC for other components that may require onPress, this is for the current case where my press function call is neither of both.
Hook:
import { useRef } from 'react';
import { Platform } from 'react-native';
const timeout = Platform.OS === 'android' ? 2000 : 1000;
const useSafePress = () => {
const disabled = useRef(false);
const onSafePress = fn => {
if (!disabled.current) {
fn();
disabled.current = true;
setTimeout(() => {
disabled.current = false;
}, timeout);
}
};
return onSafePress;
};
export default useSafePress;
Usage:
const MyComponent = () => {
const onSafePress = useSafePress();
[...]
return (
<SomeOtherComponent
open={isOpen}
icon={isOpen ? 'window-close' : 'plus'}
actions={[
{
icon: 'share',
label: 'Action 1',
onPress: () => onSafePress(onPressShare),
},
{
icon: 'calendar-month',
label: 'Action 2',
onPress: () => onSafePress(onPressCalendar),
},
]}
color={white}
onStateChange={onStateChange}
/>
);
};
This varies on opinion for sure. In my opinion, you are just wrapping a singular function and maybe it doesn't have enough unique logic to warrant it being a hook.
Either way, this works, and it's really just a stylistic choice. In my projects, I try to only extract hooks when I find myself using a collection of hooks/logic over and over again in multiple places. Or there's lots of common logic that utilize a specific hook.
I have functional component where I am passing params at onPress. Below is the code:
PAGE1
const onPress = (data) => {
navigation.goBack();
route.params.onPress(data);
};
I was able to pass this to a functional component, where I was getting the data, and was able to update my state in the functional component. Below is the code snippet from functional component:
PAGE2
function gotoSearch() {
navigation.navigate('SearchScreen',{
onPress:(data)=>{
console.log("Location Selected",data);
updateStateVar({
...stateVar,
address_line_1: data.address_line1,
address_line_2: data.address_line2,
area: data.area,
city: data.city,
country: data.country,
pincode: data.postCode,
lat:data.lat,
lng:data.lng,
});
}
});
}
I have another component which is a class component, I tried to do the same thing, but is showed me the error. Below is the code snippet from class component:
PAGE3
gotoSearch = () => {
this.props.navigation.navigate('SearchScreen'), {
onPress: (data) => {
console.log("Location Selected",data);
}
}
}
error:
cannot read property of 'onPress' of undefined
const onPress = (data) => {
navigation.goBack();
route.params.onPress(data);
^
};
you will need to export onPress function
export const onPress = (data) => {
navigation.goBack();
route.params.onPress(data);
};
then use it inside your class component
gotoSearch = () => {
this.props.navigation.navigate('SearchScreen'), {
onPress: (data) => {
console.log("Location Selected",data);
}
}
if your function exists in a different file and then first import it and then use
import {onPress} from './file.js'
I would like to write tests for my React-native app. My parent component will execute the methods within the child component.
My child component is using the Hooks forwardRef, useImperativeHandle, Ref as seen below
childs.tsx
export interface RefChild {
toggle: () => void,
close: () => void
}
const Child = forwardRef((props: ChildProps, ref: Ref<RefChild>) => {
const [isVisible, setIsVisible] = useState(false);
useImperativeHandle(ref, () => ({ toggle, close }));
const toggle = () => {
setIsVisible(!isVisible);
}
const close = () => {
setIsVisible(false)
}
return (...mycomponent)
}
My Parent component is catching the 'ref' call with
ref={(el: RefChild) => childRef.current = el}
Which allows me to call the 'toggle' and 'close' methods from within the Parent.
Now, I fail to understand how to do the same thing within my test
my parent-test.tsx:
describe('Parent', () => {
let wrapper: ShallowWrapper;
let props: any;
beforeEach(() => {
props = createTestProps({});
wrapper = shallow(<Parent {...props} />);
});
//this is what I am currently trying to do, but not working
//test 1 (not working)
it("useRef child", () => {
const useRefSpy = jest.spyOn(React, 'useRef').mockReturnValueOnce({ current: <Child/> });
expect(useRefSpy).toBeCalled();
useRefSpy.current.toggle();
})
//test 2 (not working)
it("useRef child2", () => {
const ref = {
current: {
toggle: jest.fn(),
close: jest.fn()
}
}
ref.current.toggle();
})
//test 3 (not working)
it("useRef child3", () => {
wrapper.instance().childref.current.toggle(); //failing as functional components don't have instance
})
})
My versions of React and RN are:
"react": "16.13.1",
"react-native": "0.63.3"
Could anyone explain me how should I achieve this?
As you mentioned in your question there is no instance in functional component, I think there is a better way to handle toggle and close functions from parent component using a boolean prop for each of them and the listen to changes in this value like this:
you have a state in parent component called isClose set to false and then in child component you use something like this:
useEffect(() => {
if(isClose){
//call close function
close()
}
}, [isClose])
But by the way in your current setup I think you need to mock the useRef hook something like this:
const useRefSpy = jest
.spyOn(React, "useRef")
.mockReturnValueOnce(() => ({ current: <Child /> }));
How can I assert that a button is disabled in React Native Testing Library? I would imagine something like:
expect(getByRole('button')).toBeDisabled()
but RNTL doesn't provide toBeDisabled assertion.
this is a common issue due to RN nature. I have managed to reach my goal by just testing the actual effect of callback function, not just comparing the number of calls or something like that...
describe('<Button /> - ', () => {
let state = false
const onPressMock = jest.fn(() => {
state = !state
})
const props: ButtonProps = {
text: 'Submit',
onPress: onPressMock
}
it('should become disabled', () => {
// act: render container
const { container } = render(<Button {...props} isDisabled />)
// assert 1: check if button receives {isDisabled}
expect(container.props.isDisabled).toEqual(true)
// act2: fire callback
fireEvent(container, 'onPress')
// assert 2: "state" should remain as false.
expect(state).toEqual(false)
})
})
make sure that your button looks like:
const isBlockedInteraction: boolean = isLoading || isDisabled;
return (
<TouchableOpacity
onPress={!isBlockedInteraction && onPress}
disabled={isBlockedInteraction}
{...props}
/>
)
Quite a simple try toHaveProperty method, I hope that helped.
example:
import React from 'react'
import {fireEvent, render} from '#testing-library/react-native';
import {SignInScreen} from './SignInScreen';
it('disabled button if email and password are empty', () => {
const screen = render(<SignInScreen />);
const button = screen.getByText('Login');
// screen.debug();
// console.log(button.props);
expect(button.props).toHaveProperty('disabled', true);
});
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!