Skip to content

Commit d66255d

Browse files
Esemesekthymikee
authored andcommitted
feat: fireEvent API (#11)
Implementation of fireEvent API. Fixes #7
1 parent 9d14406 commit d66255d

File tree

6 files changed

+318
-20
lines changed

6 files changed

+318
-20
lines changed

README.md

Lines changed: 109 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -25,11 +25,11 @@ This library is a replacement for [Enzyme](http://airbnb.io/enzyme/).
2525
## Example
2626

2727
```jsx
28-
import { render } from 'react-native-testing-library';
28+
import { render, fireEvent } from 'react-native-testing-library';
2929
import { QuestionsBoard } from '../QuestionsBoard';
3030

3131
function setAnswer(question, answer) {
32-
question.props.onChangeText(answer);
32+
fireEvent.changeText(question, answer);
3333
}
3434

3535
test('should verify two questions', () => {
@@ -39,7 +39,7 @@ test('should verify two questions', () => {
3939
setAnswer(allQuestions[0], 'a1');
4040
setAnswer(allQuestions[1], 'a2');
4141

42-
getByText('submit').props.onPress();
42+
fireEvent.press(getByText('submit'));
4343

4444
expect(props.verifyQuestions).toBeCalledWith({
4545
'1': { q: 'q1', a: 'a1' },
@@ -148,6 +148,111 @@ test('Component has a structure', () => {
148148
});
149149
```
150150

151+
## `FireEvent API`
152+
153+
### `fireEvent: (element: ReactTestInstance, eventName: string, data?: *) => void`
154+
155+
Invokes named event handler on the element or parent element in the tree.
156+
157+
```jsx
158+
import { View } from 'react-native';
159+
import { render, fireEvent } from 'react-native-testing-library';
160+
import { MyComponent } from './MyComponent';
161+
162+
const onEventMock = jest.fn();
163+
const { getByTestId } = render(
164+
<MyComponent testID="custom" onMyCustomEvent={onEventMock} />
165+
);
166+
167+
fireEvent(getByTestId('custom'), 'myCustomEvent');
168+
```
169+
170+
### `fireEvent.press: (element: ReactTestInstance) => void`
171+
172+
Invokes `press` event handler on the element or parent element in the tree.
173+
174+
```jsx
175+
import { View, Text, TouchableOpacity } from 'react-native';
176+
import { render, fireEvent } from 'react-native-testing-library';
177+
178+
const onPressMock = jest.fn();
179+
180+
const { getByTestId } = render(
181+
<View>
182+
<TouchableOpacity onPress={onPressMock} testID="button">
183+
<Text>Press me</Text>
184+
</TouchableOpacity>
185+
</View>
186+
);
187+
188+
fireEvent.press(getByTestId('button'));
189+
```
190+
191+
### `fireEvent.doublePress: (element: ReactTestInstance) => void`
192+
193+
Invokes `doublePress` event handler on the element or parent element in the tree.
194+
195+
```jsx
196+
import { TouchableOpacity, Text } from 'react-native';
197+
import { render, fireEvent } from 'react-native-testing-library';
198+
199+
const onDoublePressMock = jest.fn();
200+
201+
const { getByTestId } = render(
202+
<TouchableOpacity onDoublePress={onDoublePressMock}>
203+
<Text testID="button-text">Click me</Text>
204+
</TouchableOpacity>
205+
);
206+
207+
fireEvent.doublePress(getByTestId('button-text'));
208+
```
209+
210+
### `fireEvent.changeText: (element: ReactTestInstance, data?: *) => void`
211+
212+
Invokes `changeText` event handler on the element or parent element in the tree.
213+
214+
```jsx
215+
import { View, TextInput } from 'react-native';
216+
import { render, fireEvent } from 'react-native-testing-library';
217+
218+
const onChangeTextMock = jest.fn();
219+
const CHANGE_TEXT = 'content';
220+
221+
const { getByTestId } = render(
222+
<View>
223+
<TextInput testID="text-input" onChangeText={onChangeTextMock} />
224+
</View>
225+
);
226+
227+
fireEvent.changeText(getByTestId('text-input'), CHANGE_TEXT);
228+
```
229+
230+
### `fireEvent.scroll: (element: ReactTestInstance, data?: *) => void`
231+
232+
Invokes `scroll` event handler on the element or parent element in the tree.
233+
234+
```jsx
235+
import { ScrollView, TextInput } from 'react-native';
236+
import { render, fireEvent } from 'react-native-testing-library';
237+
238+
const onScrollMock = jest.fn();
239+
const eventData = {
240+
nativeEvent: {
241+
contentOffset: {
242+
y: 200,
243+
},
244+
},
245+
};
246+
247+
const { getByTestId } = render(
248+
<ScrollView testID="scroll-view" onScroll={onScrollMock}>
249+
<Text>XD</Text>
250+
</ScrollView>
251+
);
252+
253+
fireEvent.scroll(getByTestId('scroll-view'), eventData);
254+
```
255+
151256
## `debug`
152257

153258
Log prettified shallowly rendered component or test instance (just like snapshot) to stdout.
@@ -178,6 +283,7 @@ test('fetch data', async () => {
178283
```
179284

180285
<!-- badges -->
286+
181287
[build-badge]: https://img.shields.io/circleci/project/github/callstack/react-native-testing-library/master.svg?style=flat-square
182288
[build]: https://circleci.com/gh/callstack/react-native-testing-library
183289
[version-badge]: https://img.shields.io/npm/v/react-native-testing-library.svg?style=flat-square

src/__mocks__/reactNativeMock.js

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
// @flow
2+
export const View = (props: *) => props.children;
3+
export const ScrollView = (props: *) => props.children;
4+
export const Text = (props: *) => props.children;
5+
export const TextInput = () => null;
6+
export const TouchableOpacity = (props: *) => props.children;

src/__tests__/fireEvent.test.js

Lines changed: 132 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,132 @@
1+
import React from 'react';
2+
import {
3+
View,
4+
TouchableOpacity,
5+
Text,
6+
ScrollView,
7+
TextInput,
8+
} from '../__mocks__/reactNativeMock';
9+
import fireEvent from '../fireEvent';
10+
import { render } from '..';
11+
12+
const OnPressComponent = ({ onPress }) => (
13+
<View>
14+
<TouchableOpacity onPress={onPress} testID="button">
15+
<Text testID="text-button">Press me</Text>
16+
</TouchableOpacity>
17+
</View>
18+
);
19+
20+
const WithoutEventComponent = () => (
21+
<View>
22+
<Text testID="text">Content</Text>
23+
</View>
24+
);
25+
26+
const CustomEventComponent = ({ onCustomEvent }) => (
27+
<TouchableOpacity onPress={onCustomEvent}>
28+
<Text>Click me</Text>
29+
</TouchableOpacity>
30+
);
31+
32+
describe('fireEvent', () => {
33+
test('should invoke specified event', () => {
34+
const onPressMock = jest.fn();
35+
const { getByTestId } = render(<OnPressComponent onPress={onPressMock} />);
36+
37+
fireEvent(getByTestId('button'), 'press');
38+
39+
expect(onPressMock).toHaveBeenCalled();
40+
});
41+
42+
test('should invoke specified event on parent element', () => {
43+
const onPressMock = jest.fn();
44+
const { getByTestId } = render(<OnPressComponent onPress={onPressMock} />);
45+
46+
fireEvent(getByTestId('text-button'), 'press');
47+
48+
expect(onPressMock).toHaveBeenCalled();
49+
});
50+
51+
test('should throw an Error when event handler was not found', () => {
52+
const { getByTestId } = render(<WithoutEventComponent />);
53+
54+
expect(() => fireEvent(getByTestId('text'), 'press')).toThrowError(
55+
'No handler function found for event: press'
56+
);
57+
});
58+
59+
test('should invoke event with custom name', () => {
60+
const handlerMock = jest.fn();
61+
const EVENT_DATA = 'event data';
62+
63+
const { getByTestId } = render(
64+
<View>
65+
<CustomEventComponent testID="custom" onCustomEvent={handlerMock} />
66+
</View>
67+
);
68+
69+
fireEvent(getByTestId('custom'), 'customEvent', EVENT_DATA);
70+
71+
expect(handlerMock).toHaveBeenCalledWith(EVENT_DATA);
72+
});
73+
});
74+
75+
test('fireEvent.press', () => {
76+
const onPressMock = jest.fn();
77+
const { getByTestId } = render(<OnPressComponent onPress={onPressMock} />);
78+
79+
fireEvent.press(getByTestId('text-button'));
80+
81+
expect(onPressMock).toHaveBeenCalled();
82+
});
83+
84+
test('fireEvent.scroll', () => {
85+
const onScrollMock = jest.fn();
86+
const eventData = {
87+
nativeEvent: {
88+
contentOffset: {
89+
y: 200,
90+
},
91+
},
92+
};
93+
94+
const { getByTestId } = render(
95+
<ScrollView testID="scroll-view" onScroll={onScrollMock}>
96+
<Text>XD</Text>
97+
</ScrollView>
98+
);
99+
100+
fireEvent.scroll(getByTestId('scroll-view'), eventData);
101+
102+
expect(onScrollMock).toHaveBeenCalledWith(eventData);
103+
});
104+
105+
test('fireEvent.doublePress', () => {
106+
const onDoublePressMock = jest.fn();
107+
108+
const { getByTestId } = render(
109+
<TouchableOpacity onDoublePress={onDoublePressMock}>
110+
<Text testID="button-text">Click me</Text>
111+
</TouchableOpacity>
112+
);
113+
114+
fireEvent.doublePress(getByTestId('button-text'));
115+
116+
expect(onDoublePressMock).toHaveBeenCalled();
117+
});
118+
119+
test('fireEvent.changeText', () => {
120+
const onChangeTextMock = jest.fn();
121+
const CHANGE_TEXT = 'content';
122+
123+
const { getByTestId } = render(
124+
<View>
125+
<TextInput testID="text-input" onChangeText={onChangeTextMock} />
126+
</View>
127+
);
128+
129+
fireEvent.changeText(getByTestId('text-input'), CHANGE_TEXT);
130+
131+
expect(onChangeTextMock).toHaveBeenCalledWith(CHANGE_TEXT);
132+
});

src/__tests__/render.test.js

Lines changed: 1 addition & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,8 @@
11
/* eslint-disable react/no-multi-comp */
22
import React from 'react';
3-
import { View, Text, TouchableOpacity } from 'react-native'; // eslint-disable-line import/no-unresolved
3+
import { View, Text, TouchableOpacity } from '../__mocks__/reactNativeMock';
44
import { render } from '..';
55

6-
jest.mock(
7-
'react-native',
8-
() => ({
9-
View(props) {
10-
return props.children;
11-
},
12-
Text(props) {
13-
return props.children;
14-
},
15-
TouchableOpacity(props) {
16-
return props.children;
17-
},
18-
}),
19-
{ virtual: true }
20-
);
21-
226
class Button extends React.Component {
237
render() {
248
return (

src/fireEvent.js

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
// @flow
2+
import ErrorWithStack from './helpers/errorWithStack';
3+
4+
const findEventHandler = (element: ReactTestInstance, eventName: string) => {
5+
const eventHandler = toEventHandlerName(eventName);
6+
7+
if (typeof element.props[eventHandler] === 'function') {
8+
return element.props[eventHandler];
9+
}
10+
11+
if (element.parent === null) {
12+
throw new ErrorWithStack(
13+
`No handler function found for event: ${eventName}`,
14+
invokeEvent
15+
);
16+
}
17+
18+
return findEventHandler(element.parent, eventName);
19+
};
20+
21+
const invokeEvent = (
22+
element: ReactTestInstance,
23+
eventName: string,
24+
data?: *
25+
) => {
26+
const handler = findEventHandler(element, eventName);
27+
28+
return handler(data);
29+
};
30+
31+
const toEventHandlerName = (eventName: string) =>
32+
`on${eventName.charAt(0).toUpperCase()}${eventName.slice(1)}`;
33+
34+
const pressHandler = (element: ReactTestInstance) =>
35+
invokeEvent(element, 'press');
36+
const doublePressHandler = (element: ReactTestInstance) =>
37+
invokeEvent(element, 'doublePress');
38+
const changeTextHandler = (element: ReactTestInstance, data?: *) =>
39+
invokeEvent(element, 'changeText', data);
40+
const scrollHandler = (element: ReactTestInstance, data?: *) =>
41+
invokeEvent(element, 'scroll', data);
42+
43+
const EVENTS = [
44+
{
45+
name: 'press',
46+
handler: pressHandler,
47+
},
48+
{
49+
name: 'doublePress',
50+
handler: doublePressHandler,
51+
},
52+
{
53+
name: 'changeText',
54+
handler: changeTextHandler,
55+
},
56+
{
57+
name: 'scroll',
58+
handler: scrollHandler,
59+
},
60+
];
61+
62+
const fireEvent = invokeEvent;
63+
64+
EVENTS.forEach(event => {
65+
fireEvent[event.name] = event.handler;
66+
});
67+
68+
export default fireEvent;

src/index.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,10 @@ import render from './render';
33
import shallow from './shallow';
44
import flushMicrotasksQueue from './flushMicrotasksQueue';
55
import debug from './debug';
6+
import fireEvent from './fireEvent';
67

78
export { render };
89
export { shallow };
910
export { flushMicrotasksQueue };
1011
export { debug };
12+
export { fireEvent };

0 commit comments

Comments
 (0)