diff --git a/README.md b/README.md index 41fe6db28..f82129b2b 100644 --- a/README.md +++ b/README.md @@ -25,11 +25,11 @@ This library is a replacement for [Enzyme](http://airbnb.io/enzyme/). ## Example ```jsx -import { render } from 'react-native-testing-library'; +import { render, fireEvent } from 'react-native-testing-library'; import { QuestionsBoard } from '../QuestionsBoard'; function setAnswer(question, answer) { - question.props.onChangeText(answer); + fireEvent.changeText(question, answer); } test('should verify two questions', () => { @@ -39,7 +39,7 @@ test('should verify two questions', () => { setAnswer(allQuestions[0], 'a1'); setAnswer(allQuestions[1], 'a2'); - getByText('submit').props.onPress(); + fireEvent.press(getByText('submit')); expect(props.verifyQuestions).toBeCalledWith({ '1': { q: 'q1', a: 'a1' }, @@ -148,6 +148,111 @@ test('Component has a structure', () => { }); ``` +## `FireEvent API` + +### `fireEvent: (element: ReactTestInstance, eventName: string, data?: *) => void` + +Invokes named event handler on the element or parent element in the tree. + +```jsx +import { View } from 'react-native'; +import { render, fireEvent } from 'react-native-testing-library'; +import { MyComponent } from './MyComponent'; + +const onEventMock = jest.fn(); +const { getByTestId } = render( + +); + +fireEvent(getByTestId('custom'), 'myCustomEvent'); +``` + +### `fireEvent.press: (element: ReactTestInstance) => void` + +Invokes `press` event handler on the element or parent element in the tree. + +```jsx +import { View, Text, TouchableOpacity } from 'react-native'; +import { render, fireEvent } from 'react-native-testing-library'; + +const onPressMock = jest.fn(); + +const { getByTestId } = render( + + + Press me + + +); + +fireEvent.press(getByTestId('button')); +``` + +### `fireEvent.doublePress: (element: ReactTestInstance) => void` + +Invokes `doublePress` event handler on the element or parent element in the tree. + +```jsx +import { TouchableOpacity, Text } from 'react-native'; +import { render, fireEvent } from 'react-native-testing-library'; + +const onDoublePressMock = jest.fn(); + +const { getByTestId } = render( + + Click me + +); + +fireEvent.doublePress(getByTestId('button-text')); +``` + +### `fireEvent.changeText: (element: ReactTestInstance, data?: *) => void` + +Invokes `changeText` event handler on the element or parent element in the tree. + +```jsx +import { View, TextInput } from 'react-native'; +import { render, fireEvent } from 'react-native-testing-library'; + +const onChangeTextMock = jest.fn(); +const CHANGE_TEXT = 'content'; + +const { getByTestId } = render( + + + +); + +fireEvent.changeText(getByTestId('text-input'), CHANGE_TEXT); +``` + +### `fireEvent.scroll: (element: ReactTestInstance, data?: *) => void` + +Invokes `scroll` event handler on the element or parent element in the tree. + +```jsx +import { ScrollView, TextInput } from 'react-native'; +import { render, fireEvent } from 'react-native-testing-library'; + +const onScrollMock = jest.fn(); +const eventData = { + nativeEvent: { + contentOffset: { + y: 200, + }, + }, +}; + +const { getByTestId } = render( + + XD + +); + +fireEvent.scroll(getByTestId('scroll-view'), eventData); +``` + ## `debug` Log prettified shallowly rendered component or test instance (just like snapshot) to stdout. @@ -178,6 +283,7 @@ test('fetch data', async () => { ``` + [build-badge]: https://img.shields.io/circleci/project/github/callstack/react-native-testing-library/master.svg?style=flat-square [build]: https://circleci.com/gh/callstack/react-native-testing-library [version-badge]: https://img.shields.io/npm/v/react-native-testing-library.svg?style=flat-square diff --git a/src/__mocks__/reactNativeMock.js b/src/__mocks__/reactNativeMock.js new file mode 100644 index 000000000..7c38c54e5 --- /dev/null +++ b/src/__mocks__/reactNativeMock.js @@ -0,0 +1,6 @@ +// @flow +export const View = (props: *) => props.children; +export const ScrollView = (props: *) => props.children; +export const Text = (props: *) => props.children; +export const TextInput = () => null; +export const TouchableOpacity = (props: *) => props.children; diff --git a/src/__tests__/fireEvent.test.js b/src/__tests__/fireEvent.test.js new file mode 100644 index 000000000..028d18391 --- /dev/null +++ b/src/__tests__/fireEvent.test.js @@ -0,0 +1,132 @@ +import React from 'react'; +import { + View, + TouchableOpacity, + Text, + ScrollView, + TextInput, +} from '../__mocks__/reactNativeMock'; +import fireEvent from '../fireEvent'; +import { render } from '..'; + +const OnPressComponent = ({ onPress }) => ( + + + Press me + + +); + +const WithoutEventComponent = () => ( + + Content + +); + +const CustomEventComponent = ({ onCustomEvent }) => ( + + Click me + +); + +describe('fireEvent', () => { + test('should invoke specified event', () => { + const onPressMock = jest.fn(); + const { getByTestId } = render(); + + fireEvent(getByTestId('button'), 'press'); + + expect(onPressMock).toHaveBeenCalled(); + }); + + test('should invoke specified event on parent element', () => { + const onPressMock = jest.fn(); + const { getByTestId } = render(); + + fireEvent(getByTestId('text-button'), 'press'); + + expect(onPressMock).toHaveBeenCalled(); + }); + + test('should throw an Error when event handler was not found', () => { + const { getByTestId } = render(); + + expect(() => fireEvent(getByTestId('text'), 'press')).toThrowError( + 'No handler function found for event: press' + ); + }); + + test('should invoke event with custom name', () => { + const handlerMock = jest.fn(); + const EVENT_DATA = 'event data'; + + const { getByTestId } = render( + + + + ); + + fireEvent(getByTestId('custom'), 'customEvent', EVENT_DATA); + + expect(handlerMock).toHaveBeenCalledWith(EVENT_DATA); + }); +}); + +test('fireEvent.press', () => { + const onPressMock = jest.fn(); + const { getByTestId } = render(); + + fireEvent.press(getByTestId('text-button')); + + expect(onPressMock).toHaveBeenCalled(); +}); + +test('fireEvent.scroll', () => { + const onScrollMock = jest.fn(); + const eventData = { + nativeEvent: { + contentOffset: { + y: 200, + }, + }, + }; + + const { getByTestId } = render( + + XD + + ); + + fireEvent.scroll(getByTestId('scroll-view'), eventData); + + expect(onScrollMock).toHaveBeenCalledWith(eventData); +}); + +test('fireEvent.doublePress', () => { + const onDoublePressMock = jest.fn(); + + const { getByTestId } = render( + + Click me + + ); + + fireEvent.doublePress(getByTestId('button-text')); + + expect(onDoublePressMock).toHaveBeenCalled(); +}); + +test('fireEvent.changeText', () => { + const onChangeTextMock = jest.fn(); + const CHANGE_TEXT = 'content'; + + const { getByTestId } = render( + + + + ); + + fireEvent.changeText(getByTestId('text-input'), CHANGE_TEXT); + + expect(onChangeTextMock).toHaveBeenCalledWith(CHANGE_TEXT); +}); diff --git a/src/__tests__/render.test.js b/src/__tests__/render.test.js index 1c23015d2..a952ebd10 100644 --- a/src/__tests__/render.test.js +++ b/src/__tests__/render.test.js @@ -1,24 +1,8 @@ /* eslint-disable react/no-multi-comp */ import React from 'react'; -import { View, Text, TouchableOpacity } from 'react-native'; // eslint-disable-line import/no-unresolved +import { View, Text, TouchableOpacity } from '../__mocks__/reactNativeMock'; import { render } from '..'; -jest.mock( - 'react-native', - () => ({ - View(props) { - return props.children; - }, - Text(props) { - return props.children; - }, - TouchableOpacity(props) { - return props.children; - }, - }), - { virtual: true } -); - class Button extends React.Component { render() { return ( diff --git a/src/fireEvent.js b/src/fireEvent.js new file mode 100644 index 000000000..1cb51d1c1 --- /dev/null +++ b/src/fireEvent.js @@ -0,0 +1,68 @@ +// @flow +import ErrorWithStack from './helpers/errorWithStack'; + +const findEventHandler = (element: ReactTestInstance, eventName: string) => { + const eventHandler = toEventHandlerName(eventName); + + if (typeof element.props[eventHandler] === 'function') { + return element.props[eventHandler]; + } + + if (element.parent === null) { + throw new ErrorWithStack( + `No handler function found for event: ${eventName}`, + invokeEvent + ); + } + + return findEventHandler(element.parent, eventName); +}; + +const invokeEvent = ( + element: ReactTestInstance, + eventName: string, + data?: * +) => { + const handler = findEventHandler(element, eventName); + + return handler(data); +}; + +const toEventHandlerName = (eventName: string) => + `on${eventName.charAt(0).toUpperCase()}${eventName.slice(1)}`; + +const pressHandler = (element: ReactTestInstance) => + invokeEvent(element, 'press'); +const doublePressHandler = (element: ReactTestInstance) => + invokeEvent(element, 'doublePress'); +const changeTextHandler = (element: ReactTestInstance, data?: *) => + invokeEvent(element, 'changeText', data); +const scrollHandler = (element: ReactTestInstance, data?: *) => + invokeEvent(element, 'scroll', data); + +const EVENTS = [ + { + name: 'press', + handler: pressHandler, + }, + { + name: 'doublePress', + handler: doublePressHandler, + }, + { + name: 'changeText', + handler: changeTextHandler, + }, + { + name: 'scroll', + handler: scrollHandler, + }, +]; + +const fireEvent = invokeEvent; + +EVENTS.forEach(event => { + fireEvent[event.name] = event.handler; +}); + +export default fireEvent; diff --git a/src/index.js b/src/index.js index c9014bd68..a2181079a 100644 --- a/src/index.js +++ b/src/index.js @@ -3,8 +3,10 @@ import render from './render'; import shallow from './shallow'; import flushMicrotasksQueue from './flushMicrotasksQueue'; import debug from './debug'; +import fireEvent from './fireEvent'; export { render }; export { shallow }; export { flushMicrotasksQueue }; export { debug }; +export { fireEvent };