From ebab5c242cfeaa21227a238b40bab3bba931e2ee Mon Sep 17 00:00:00 2001 From: Kacper Wiszczuk Date: Sat, 6 Oct 2018 14:05:03 +0200 Subject: [PATCH 1/7] feat: Initial implementation of fireEvent API --- src/fireEvent.js | 43 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 43 insertions(+) create mode 100644 src/fireEvent.js diff --git a/src/fireEvent.js b/src/fireEvent.js new file mode 100644 index 000000000..34d672941 --- /dev/null +++ b/src/fireEvent.js @@ -0,0 +1,43 @@ +// @flow +import * as React from 'react'; + +const findEventHandler = (element: ReactTestInstance, eventName: string) => { + if (typeof element.props[`on${eventName}`] === 'function') { + return element.props[`on${eventName}`]; + } + + if (element.parent === null) { + throw new Error(`No handler function found for event: ${eventName}`); + } + + return findEventHandler(element.parent, eventName); +}; + +const invokeEvent = ( + element: ReactTestInstance, + eventName: string, + data: any +) => { + const handler = findEventHandler(element, capitalize(eventName)); + + return handler(data); +}; + +const capitalize = (name: string) => + name.charAt(0).toUpperCase() + name.slice(1); + +const press = (element: ReactTestInstance) => invokeEvent(element, 'press'); +const doublePress = (element: ReactTestInstance) => + invokeEvent(element, 'doublePress'); +const changeText = (element: ReactTestInstance, data: any) => + invokeEvent(element, 'changeText', data); +const scroll = (element: ReactTestInstance, data: any) => + invokeEvent(element, 'scroll', data); + +export default { + press, + doublePress, + changeText, + scroll, + invokeEvent, +}; From df677f48dfc51612a3b7bf80b24a249214f113d9 Mon Sep 17 00:00:00 2001 From: Kacper Wiszczuk Date: Sat, 6 Oct 2018 21:46:47 +0200 Subject: [PATCH 2/7] feat: Add tests to fireEvent API --- src/__tests__/fireEvent.test.js | 154 ++++++++++++++++++++++++++++++++ src/fireEvent.js | 14 +-- src/index.js | 2 + 3 files changed, 163 insertions(+), 7 deletions(-) create mode 100644 src/__tests__/fireEvent.test.js diff --git a/src/__tests__/fireEvent.test.js b/src/__tests__/fireEvent.test.js new file mode 100644 index 000000000..208291c4c --- /dev/null +++ b/src/__tests__/fireEvent.test.js @@ -0,0 +1,154 @@ +import React from 'react'; +import { + View, + TouchableOpacity, + Text, + ScrollView, + TextInput, +} from 'react-native'; // eslint-disable-line import/no-unresolved +import fireEvent from '../fireEvent'; +import { render } from '..'; + +const OnPressComponent = ({ onPress }) => ( + + + Press me + + +); + +const WithoutEventComponent = () => ( + + Content + +); + +const CustomEventComponent = ({ onCustomEvent }) => ( + + Click me + +); + +jest.mock( + 'react-native', + () => ({ + View(props) { + return props.children; + }, + ScrollView(props) { + return props.children; + }, + Text(props) { + return props.children; + }, + TextInput() { + return null; + }, + TouchableOpacity(props) { + return props.children; + }, + }), + { virtual: true } +); + +describe('fireEvent.invokeEvent', () => { + test('should invoke specified event', () => { + const onPressMock = jest.fn(); + const { getByTestId } = render(); + + fireEvent.invokeEvent(getByTestId('button'), 'press'); + + expect(onPressMock).toHaveBeenCalled(); + }); + + test('should invoke specified event on parent element', () => { + const onPressMock = jest.fn(); + const { getByTestId } = render(); + + fireEvent.invokeEvent(getByTestId('text-button'), 'press'); + + expect(onPressMock).toHaveBeenCalled(); + }); + + test('should throw an Error when event handler was not found', () => { + const { getByTestId } = render(); + + expect(() => + fireEvent.invokeEvent(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.invokeEvent(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/fireEvent.js b/src/fireEvent.js index 34d672941..6718a551f 100644 --- a/src/fireEvent.js +++ b/src/fireEvent.js @@ -1,9 +1,9 @@ // @flow -import * as React from 'react'; - const findEventHandler = (element: ReactTestInstance, eventName: string) => { - if (typeof element.props[`on${eventName}`] === 'function') { - return element.props[`on${eventName}`]; + const eventHandler = toEventHandlerName(eventName); + + if (typeof element.props[eventHandler] === 'function') { + return element.props[eventHandler]; } if (element.parent === null) { @@ -18,13 +18,13 @@ const invokeEvent = ( eventName: string, data: any ) => { - const handler = findEventHandler(element, capitalize(eventName)); + const handler = findEventHandler(element, eventName); return handler(data); }; -const capitalize = (name: string) => - name.charAt(0).toUpperCase() + name.slice(1); +const toEventHandlerName = (eventName: string) => + `on${eventName.charAt(0).toUpperCase()}${eventName.slice(1)}`; const press = (element: ReactTestInstance) => invokeEvent(element, 'press'); const doublePress = (element: ReactTestInstance) => diff --git a/src/index.js b/src/index.js index 1feb8a405..28318c9db 100644 --- a/src/index.js +++ b/src/index.js @@ -4,6 +4,7 @@ import { isValidElementType } from 'react-is'; import TestRenderer from 'react-test-renderer'; // eslint-disable-line import/no-extraneous-dependencies import ShallowRenderer from 'react-test-renderer/shallow'; // eslint-disable-line import/no-extraneous-dependencies import prettyFormat, { plugins } from 'pretty-format'; // eslint-disable-line import/no-extraneous-dependencies +import fireEvent from './fireEvent'; const getNodeByName = (node, name) => node.type.name === name || @@ -58,6 +59,7 @@ export const render = ( }; return { + fireEvent, getByTestId: (testID: string) => instance.findByProps({ testID }), getByName, getAllByName: (name: string | React.Element<*>) => From f4b999a867c23aac4802f299b01dff1ab37461d6 Mon Sep 17 00:00:00 2001 From: Kacper Wiszczuk Date: Sun, 7 Oct 2018 00:55:53 +0200 Subject: [PATCH 3/7] Feat Review fixes --- README.md | 6 +++++ src/__mocks__/reactNativeMock.js | 6 +++++ src/__tests__/fireEvent.test.js | 38 ++++++----------------------- src/__tests__/render.test.js | 18 +------------- src/fireEvent.js | 42 +++++++++++++++++++++++--------- src/index.js | 2 ++ 6 files changed, 54 insertions(+), 58 deletions(-) create mode 100644 src/__mocks__/reactNativeMock.js diff --git a/README.md b/README.md index 41fe6db28..5470eeb76 100644 --- a/README.md +++ b/README.md @@ -148,6 +148,12 @@ test('Component has a structure', () => { }); ``` +## `fireEvent` + +### press + +Invokes `press` event on the element or parent element + ## `debug` Log prettified shallowly rendered component or test instance (just like snapshot) to stdout. 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 index 208291c4c..028d18391 100644 --- a/src/__tests__/fireEvent.test.js +++ b/src/__tests__/fireEvent.test.js @@ -5,7 +5,7 @@ import { Text, ScrollView, TextInput, -} from 'react-native'; // eslint-disable-line import/no-unresolved +} from '../__mocks__/reactNativeMock'; import fireEvent from '../fireEvent'; import { render } from '..'; @@ -29,34 +29,12 @@ const CustomEventComponent = ({ onCustomEvent }) => ( ); -jest.mock( - 'react-native', - () => ({ - View(props) { - return props.children; - }, - ScrollView(props) { - return props.children; - }, - Text(props) { - return props.children; - }, - TextInput() { - return null; - }, - TouchableOpacity(props) { - return props.children; - }, - }), - { virtual: true } -); - -describe('fireEvent.invokeEvent', () => { +describe('fireEvent', () => { test('should invoke specified event', () => { const onPressMock = jest.fn(); const { getByTestId } = render(); - fireEvent.invokeEvent(getByTestId('button'), 'press'); + fireEvent(getByTestId('button'), 'press'); expect(onPressMock).toHaveBeenCalled(); }); @@ -65,7 +43,7 @@ describe('fireEvent.invokeEvent', () => { const onPressMock = jest.fn(); const { getByTestId } = render(); - fireEvent.invokeEvent(getByTestId('text-button'), 'press'); + fireEvent(getByTestId('text-button'), 'press'); expect(onPressMock).toHaveBeenCalled(); }); @@ -73,9 +51,9 @@ describe('fireEvent.invokeEvent', () => { test('should throw an Error when event handler was not found', () => { const { getByTestId } = render(); - expect(() => - fireEvent.invokeEvent(getByTestId('text'), 'press') - ).toThrowError('No handler function found for event: press'); + expect(() => fireEvent(getByTestId('text'), 'press')).toThrowError( + 'No handler function found for event: press' + ); }); test('should invoke event with custom name', () => { @@ -88,7 +66,7 @@ describe('fireEvent.invokeEvent', () => { ); - fireEvent.invokeEvent(getByTestId('custom'), 'customEvent', EVENT_DATA); + fireEvent(getByTestId('custom'), 'customEvent', EVENT_DATA); expect(handlerMock).toHaveBeenCalledWith(EVENT_DATA); }); 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 index 6718a551f..487db9295 100644 --- a/src/fireEvent.js +++ b/src/fireEvent.js @@ -26,18 +26,38 @@ const invokeEvent = ( const toEventHandlerName = (eventName: string) => `on${eventName.charAt(0).toUpperCase()}${eventName.slice(1)}`; -const press = (element: ReactTestInstance) => invokeEvent(element, 'press'); -const doublePress = (element: ReactTestInstance) => +const pressHandler = (element: ReactTestInstance) => + invokeEvent(element, 'press'); +const doublePressHandler = (element: ReactTestInstance) => invokeEvent(element, 'doublePress'); -const changeText = (element: ReactTestInstance, data: any) => +const changeTextHandler = (element: ReactTestInstance, data: any) => invokeEvent(element, 'changeText', data); -const scroll = (element: ReactTestInstance, data: any) => +const scrollHandler = (element: ReactTestInstance, data: any) => invokeEvent(element, 'scroll', data); -export default { - press, - doublePress, - changeText, - scroll, - invokeEvent, -}; +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 }; From 44f2d2eeee570e1facb9ca29e372c2b2cb56bae5 Mon Sep 17 00:00:00 2001 From: Kacper Wiszczuk Date: Sun, 7 Oct 2018 01:29:03 +0200 Subject: [PATCH 4/7] feat fireEvent doc update --- README.md | 103 +++++++++++++++++++++++++++++++++++++++++++++-- src/fireEvent.js | 6 +-- 2 files changed, 103 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index 5470eeb76..cbfebd51e 100644 --- a/README.md +++ b/README.md @@ -148,11 +148,108 @@ test('Component has a structure', () => { }); ``` -## `fireEvent` +## `fireEvent: (element: ReactTestInstance, eventName: string, data?: *) => void` -### press +Invokes named event handler on the element or parent element in the tree. -Invokes `press` event on the element or parent element +```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'); +``` + +### `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')); +``` + +### `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')); +``` + +### `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); +``` + +### `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` diff --git a/src/fireEvent.js b/src/fireEvent.js index 487db9295..dbed3fd0b 100644 --- a/src/fireEvent.js +++ b/src/fireEvent.js @@ -16,7 +16,7 @@ const findEventHandler = (element: ReactTestInstance, eventName: string) => { const invokeEvent = ( element: ReactTestInstance, eventName: string, - data: any + data?: * ) => { const handler = findEventHandler(element, eventName); @@ -30,9 +30,9 @@ const pressHandler = (element: ReactTestInstance) => invokeEvent(element, 'press'); const doublePressHandler = (element: ReactTestInstance) => invokeEvent(element, 'doublePress'); -const changeTextHandler = (element: ReactTestInstance, data: any) => +const changeTextHandler = (element: ReactTestInstance, data?: *) => invokeEvent(element, 'changeText', data); -const scrollHandler = (element: ReactTestInstance, data: any) => +const scrollHandler = (element: ReactTestInstance, data?: *) => invokeEvent(element, 'scroll', data); const EVENTS = [ From a4bf24faf263b246605f7902257e8497e2c82d80 Mon Sep 17 00:00:00 2001 From: Kacper Wiszczuk Date: Sun, 7 Oct 2018 01:45:28 +0200 Subject: [PATCH 5/7] feat Documentation tweaks --- README.md | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index cbfebd51e..cb4750ba7 100644 --- a/README.md +++ b/README.md @@ -147,8 +147,8 @@ test('Component has a structure', () => { expect(output).toMatchSnapshot(); }); ``` - -## `fireEvent: (element: ReactTestInstance, eventName: string, data?: *) => void` +## `FireEvent API` +### `fireEvent: (element: ReactTestInstance, eventName: string, data?: *) => void` Invokes named event handler on the element or parent element in the tree. @@ -165,7 +165,7 @@ const { getByTestId } = render( fireEvent(getByTestId('custom'), 'myCustomEvent'); ``` -### `press: (element: ReactTestInstance) => void` +### `fireEvent.press: (element: ReactTestInstance) => void` Invokes `press` event handler on the element or parent element in the tree. @@ -186,7 +186,7 @@ const { getByTestId } = render( fireEvent.press(getByTestId('button')); ``` -### `doublePress: (element: ReactTestInstance) => void` +### `fireEvent.doublePress: (element: ReactTestInstance) => void` Invokes `doublePress` event handler on the element or parent element in the tree. @@ -205,7 +205,7 @@ const { getByTestId } = render( fireEvent.doublePress(getByTestId('button-text')); ``` -### `changeText: (element: ReactTestInstance, data?: *) => void` +### `fireEvent.changeText: (element: ReactTestInstance, data?: *) => void` Invokes `changeText` event handler on the element or parent element in the tree. @@ -225,7 +225,7 @@ const { getByTestId } = render( fireEvent.changeText(getByTestId('text-input'), CHANGE_TEXT); ``` -### `scroll: (element: ReactTestInstance, data?: *) => void` +### `fireEvent.scroll: (element: ReactTestInstance, data?: *) => void` Invokes `scroll` event handler on the element or parent element in the tree. From fc071deb7a7883487ade5e3baa7ee142b63ef04c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Pierzcha=C5=82a?= Date: Sun, 7 Oct 2018 07:54:07 +0200 Subject: [PATCH 6/7] update example --- README.md | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index cb4750ba7..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' }, @@ -147,7 +147,9 @@ test('Component has a structure', () => { expect(output).toMatchSnapshot(); }); ``` + ## `FireEvent API` + ### `fireEvent: (element: ReactTestInstance, eventName: string, data?: *) => void` Invokes named event handler on the element or parent element in the tree. @@ -281,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 From 0098fef1ad3526b7badee32b820a0fdb38aacace Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Pierzcha=C5=82a?= Date: Sun, 7 Oct 2018 08:21:59 +0200 Subject: [PATCH 7/7] use ErrorWithStack --- src/fireEvent.js | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/fireEvent.js b/src/fireEvent.js index dbed3fd0b..1cb51d1c1 100644 --- a/src/fireEvent.js +++ b/src/fireEvent.js @@ -1,4 +1,6 @@ // @flow +import ErrorWithStack from './helpers/errorWithStack'; + const findEventHandler = (element: ReactTestInstance, eventName: string) => { const eventHandler = toEventHandlerName(eventName); @@ -7,7 +9,10 @@ const findEventHandler = (element: ReactTestInstance, eventName: string) => { } if (element.parent === null) { - throw new Error(`No handler function found for event: ${eventName}`); + throw new ErrorWithStack( + `No handler function found for event: ${eventName}`, + invokeEvent + ); } return findEventHandler(element.parent, eventName);