From 151348835fd2bd41dcf9d185a130ccdb9d8135e9 Mon Sep 17 00:00:00 2001 From: Scotty Waggoner Date: Fri, 29 Mar 2019 22:16:50 -0700 Subject: [PATCH] MHP-2233: Bring in react-native-testing-library and add utility functions - Update tests for App.js and route helpers --- __mock__/react-native-firebase.js | 5 +- __mock__/react-native-omniture.js | 1 + package.json | 3 +- src/__tests__/App.js | 69 ++++++++----------- .../SwipeTabMenu/__tests__/SwipeTabMenu.js | 6 +- .../__tests__/__snapshots__/helpers.js.snap | 12 ++-- src/routes/__tests__/helpers.js | 24 ++++--- testUtils/index.js | 45 ++++++++++-- yarn.lock | 65 ++++++++++++++--- 9 files changed, 154 insertions(+), 76 deletions(-) diff --git a/__mock__/react-native-firebase.js b/__mock__/react-native-firebase.js index bc0800fd90..679a2d4d54 100644 --- a/__mock__/react-native-firebase.js +++ b/__mock__/react-native-firebase.js @@ -1,3 +1,6 @@ jest.mock('react-native-firebase', () => ({ - links: jest.fn(), + links: jest.fn(() => ({ + onLink: jest.fn(), + getInitialLink: jest.fn(() => Promise.resolve('firebaseDeepLinkUri')), + })), })); diff --git a/__mock__/react-native-omniture.js b/__mock__/react-native-omniture.js index 3f73d1e0c9..2045ea8b46 100644 --- a/__mock__/react-native-omniture.js +++ b/__mock__/react-native-omniture.js @@ -3,4 +3,5 @@ jest.mock('react-native-omniture', () => ({ trackState: jest.fn(), syncIdentifier: jest.fn(), loadMarketingCloudId: jest.fn(), + collectLifecycleData: jest.fn(), })); diff --git a/package.json b/package.json index a7a638f912..fa486bfd85 100644 --- a/package.json +++ b/package.json @@ -99,7 +99,8 @@ "prettier": "^1.14.3", "pretty-quick": "^1.8.0", "react-dom": "^16.3.2", - "react-test-renderer": "^16.3.2", + "react-native-testing-library": "^1.6.1", + "react-test-renderer": "^16.8.6", "redux-mock-store": "^1.5.1" }, "jest": { diff --git a/src/__tests__/App.js b/src/__tests__/App.js index 062801f2ee..2a3f509a67 100644 --- a/src/__tests__/App.js +++ b/src/__tests__/App.js @@ -1,8 +1,6 @@ import React from 'react'; -import ReactNative from 'react-native'; -import Adapter from 'enzyme-adapter-react-16/build/index'; -import { shallow } from 'enzyme/build/index'; -import Enzyme from 'enzyme/build/index'; +import { Alert } from 'react-native'; +import { render } from 'react-native-testing-library'; import App from '../App'; import { @@ -15,10 +13,6 @@ import * as auth from '../actions/auth/auth'; import locale from '../i18n/locales/en-US'; import { rollbar } from '../utils/rollbar.config'; -Enzyme.configure({ adapter: new Adapter() }); - -jest.mock('../AppNavigator', () => ({ AppNavigator: 'mockAppNavigator' })); - jest.mock('react-native-default-preference', () => ({ get: jest.fn().mockReturnValue(Promise.reject()), })); @@ -27,15 +21,6 @@ global.window = {}; const logoutResponse = { type: 'logged out' }; auth.logout = jest.fn().mockReturnValue(logoutResponse); -jest.mock('react-navigation-redux-helpers', () => ({ - createReactNavigationReduxMiddleware: jest.fn(), -})); - -jest.mock('../store', () => ({ - store: require('../../testUtils').createMockStore(), - persistor: {}, -})); - const { youreOffline, connectToInternet } = locale.offline; const lastTwoArgs = [ @@ -44,22 +29,20 @@ const lastTwoArgs = [ ]; beforeEach(() => - (ReactNative.Alert.alert = jest + (Alert.alert = jest .fn() .mockImplementation((_, __, buttons) => buttons[0].onPress()))); -const test = async response => { - const shallowScreen = shallow(); - - await shallowScreen.instance().handleError(response); +const testUnhandledRejection = response => { + render(); - return shallowScreen; + window.onunhandledrejection({ reason: response }); }; it('shows offline alert if network request failed', () => { - test({ apiError: { message: NETWORK_REQUEST_FAILED } }); + testUnhandledRejection({ apiError: { message: NETWORK_REQUEST_FAILED } }); - expect(ReactNative.Alert.alert).toHaveBeenCalledWith( + expect(Alert.alert).toHaveBeenCalledWith( youreOffline, connectToInternet, ...lastTwoArgs, @@ -67,37 +50,41 @@ it('shows offline alert if network request failed', () => { }); it('should not show alert for expired access token', () => { - test({ apiError: { errors: [{ detail: EXPIRED_ACCESS_TOKEN }] } }); + testUnhandledRejection({ + apiError: { errors: [{ detail: EXPIRED_ACCESS_TOKEN }] }, + }); - expect(ReactNative.Alert.alert).not.toHaveBeenCalled(); + expect(Alert.alert).not.toHaveBeenCalled(); }); it('should not show alert for invalid access token', () => { - test({ apiError: { errors: [{ detail: INVALID_ACCESS_TOKEN }] } }); + testUnhandledRejection({ + apiError: { errors: [{ detail: INVALID_ACCESS_TOKEN }] }, + }); - expect(ReactNative.Alert.alert).not.toHaveBeenCalled(); + expect(Alert.alert).not.toHaveBeenCalled(); }); it('should not show alert for invalid grant', () => { - test({ apiError: { error: INVALID_GRANT } }); + testUnhandledRejection({ apiError: { error: INVALID_GRANT } }); - expect(ReactNative.Alert.alert).not.toHaveBeenCalled(); + expect(Alert.alert).not.toHaveBeenCalled(); }); it('should not show alert if not ApiError', () => { const message = 'some message\nwith break'; - test({ key: 'test', method: '', message }); + testUnhandledRejection({ key: 'test', method: '', message }); - expect(ReactNative.Alert.alert).not.toHaveBeenCalled(); + expect(Alert.alert).not.toHaveBeenCalled(); }); it('should not show alert if no error message', () => { const unknownError = { key: 'test', method: '' }; - test(unknownError); + testUnhandledRejection(unknownError); - expect(ReactNative.Alert.alert).not.toHaveBeenCalled(); + expect(Alert.alert).not.toHaveBeenCalled(); }); describe('__DEV__ === false', () => { @@ -111,7 +98,7 @@ describe('__DEV__ === false', () => { __DEV__ = dev; }); - it('Sends Rollbar report for API error', async () => { + it('Sends Rollbar report for API error', () => { const apiError = { apiError: { message: 'Error Text' }, key: 'ADD_NEW_PERSON', @@ -120,7 +107,7 @@ describe('__DEV__ === false', () => { query: { filters: { organization_ids: '1' } }, }; - await test(apiError); + testUnhandledRejection(apiError); expect(rollbar.error).toHaveBeenCalledWith( Error( @@ -135,20 +122,20 @@ describe('__DEV__ === false', () => { ); }); - it('Sends Rollbar report for JS Error', async () => { + it('Sends Rollbar report for JS Error', () => { const errorName = 'Error Name'; const errorDetails = 'Error Details'; const error = Error(`${errorName}\n${errorDetails}`); - await test(error); + testUnhandledRejection(error); expect(rollbar.error).toHaveBeenCalledWith(error); }); - it('Sends Rollbar report for unknown error', async () => { + it('Sends Rollbar report for unknown error', () => { const unknownError = { key: 'test', method: '' }; - await test(unknownError); + testUnhandledRejection(unknownError); expect(rollbar.error).toHaveBeenCalledWith( Error(`Unknown Error:\n${JSON.stringify(unknownError, null, 2)}`), diff --git a/src/components/SwipeTabMenu/__tests__/SwipeTabMenu.js b/src/components/SwipeTabMenu/__tests__/SwipeTabMenu.js index e0822b0737..1269757a68 100644 --- a/src/components/SwipeTabMenu/__tests__/SwipeTabMenu.js +++ b/src/components/SwipeTabMenu/__tests__/SwipeTabMenu.js @@ -34,12 +34,13 @@ const tabs = [ ]; it('should render correctly', () => { - const component = testSnapshotShallow( + const component = renderShallow( , ); + expect(component).toMatchSnapshot(); // Render update from manual onLayout callback with new rendered element size component @@ -51,13 +52,14 @@ it('should render correctly', () => { }); it('should render light version correctly', () => { - const component = testSnapshotShallow( + const component = renderShallow( , ); + expect(component).toMatchSnapshot(); // Render update from manual onLayout callback with new rendered element size component diff --git a/src/routes/__tests__/__snapshots__/helpers.js.snap b/src/routes/__tests__/__snapshots__/helpers.js.snap index 6b9cc2d3fc..a216630da7 100644 --- a/src/routes/__tests__/__snapshots__/helpers.js.snap +++ b/src/routes/__tests__/__snapshots__/helpers.js.snap @@ -1,9 +1,11 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`wrapProps should add extra props to component 1`] = ` - + + {"extraProp1":true,"extraProp2":false} + `; diff --git a/src/routes/__tests__/helpers.js b/src/routes/__tests__/helpers.js index 0bf536417d..575c30d51a 100644 --- a/src/routes/__tests__/helpers.js +++ b/src/routes/__tests__/helpers.js @@ -1,4 +1,5 @@ import React from 'react'; +import { Text } from 'react-native'; import { connect } from 'react-redux'; import configureStore from 'redux-mock-store'; import thunk from 'redux-thunk'; @@ -11,16 +12,17 @@ import { wrapProps, buildTrackedScreen, } from '../helpers'; -import { renderShallow, testSnapshotShallow } from '../../../testUtils'; +import { renderWithRedux, snapshotWithRedux } from '../../../testUtils'; const store = configureStore([thunk])(); const nextScreenName = 'testNextScreenName'; const routeParams = { testKey: 'testValue' }; -const TestComponent = connect()(({ next, dispatch }) => - dispatch(next(routeParams)), -); +const TestComponent = connect()(({ next, dispatch }) => { + dispatch(next(routeParams)); + return null; +}); beforeEach(() => { store.clearActions(); @@ -30,7 +32,7 @@ describe('wrapNextScreen', () => { it('should pass the next prop and fire a navigatePush action with the provided screen name', () => { const WrappedTestComponent = wrapNextScreen(TestComponent, nextScreenName); - renderShallow(, store).dive(); + const { store } = renderWithRedux(); expect(store.getActions()).toEqual([ StackActions.push({ @@ -48,7 +50,7 @@ describe('wrapNextScreenFn', () => { () => nextScreenName, ); - renderShallow(, store).dive(); + const { store } = renderWithRedux(); expect(store.getActions()).toEqual([ StackActions.push({ @@ -66,7 +68,7 @@ describe('wrapNextAction', () => { props => dispatch => dispatch({ type: 'test', nextScreenName, props }), ); - renderShallow(, store).dive(); + const { store } = renderWithRedux(); expect(store.getActions()).toEqual([ { @@ -80,12 +82,16 @@ describe('wrapNextAction', () => { describe('wrapProps', () => { it('should add extra props to component', () => { - const WrappedTestComponent = wrapProps(TestComponent, { + const PropStringifyComponent = props => ( + {JSON.stringify(props)} + ); + + const WrappedTestComponent = wrapProps(PropStringifyComponent, { extraProp1: true, extraProp2: false, }); - testSnapshotShallow(, store); + snapshotWithRedux(); }); }); diff --git a/testUtils/index.js b/testUtils/index.js index c526aa41e0..6f10895833 100644 --- a/testUtils/index.js +++ b/testUtils/index.js @@ -1,9 +1,11 @@ +import React from 'react'; import 'react-native'; -import renderer from 'react-test-renderer'; -import Enzyme, { shallow } from 'enzyme'; +import { Provider } from 'react-redux'; +import thunk from 'redux-thunk'; +import { render, shallow } from 'react-native-testing-library'; +import Enzyme, { shallow as enzymeShallow } from 'enzyme'; import Adapter from 'enzyme-adapter-react-16'; import configureStore from 'redux-mock-store'; -import thunk from 'redux-thunk'; Enzyme.configure({ adapter: new Adapter() }); @@ -19,12 +21,41 @@ export const createMockNavState = (params = {}) => { return { state: { params } }; }; -export const testSnapshot = data => { - expect(renderer.create(data)).toMatchSnapshot(); +export const snapshot = component => { + const { toJSON } = render(component); + expect(toJSON()).toMatchSnapshot(); }; +export const testSnapshot = snapshot; // TODO: remove and rename existing usages +export const snapshotShallow = component => { + const { output } = shallow(component); + expect(output).toMatchSnapshot(); +}; + +// Stolen from https://github.com/kentcdodds/react-testing-library/blob/52575005579307bcfbe7fbe4ef4636147c03c6fb/examples/__tests__/react-redux.js#L69-L80 +export function renderWithRedux( + ui, + { initialState, store = configureStore([thunk])(initialState) } = {}, +) { + return { + ...render({ui}), + // adding `store` to the returned utilities to allow us + // to reference it in our tests (just try to avoid using + // this to test implementation details). + store, + }; +} + +export function snapshotWithRedux(ui, { initialState, store } = {}) { + const { toJSON } = renderWithRedux(ui, { initialState, store }); + expect(toJSON()).toMatchSnapshot(); +} + +// TODO: stop using and remove export const renderShallow = (component, store = configureStore([thunk])()) => { - let renderedComponent = shallow(component, { context: { store: store } }); + let renderedComponent = enzymeShallow(component, { + context: { store: store }, + }); // If component has translation wrappers, dive deeper while (renderedComponent.is('Translate') || renderedComponent.is('I18n')) { @@ -36,11 +67,11 @@ export const renderShallow = (component, store = configureStore([thunk])()) => { return renderedComponent; }; +// TODO: stop using and remove export const testSnapshotShallow = ( component, store = configureStore([thunk])(), ) => { const renderedComponent = renderShallow(component, store); expect(renderedComponent).toMatchSnapshot(); - return renderedComponent; }; diff --git a/yarn.lock b/yarn.lock index 9b2146857a..23b6ab4283 100644 --- a/yarn.lock +++ b/yarn.lock @@ -518,12 +518,25 @@ md5 "^2.0.0" request-promise "^0.4.2" +"@jest/types@^24.5.0": + version "24.5.0" + resolved "https://registry.yarnpkg.com/@jest/types/-/types-24.5.0.tgz#feee214a4d0167b0ca447284e95a57aa10b3ee95" + integrity sha512-kN7RFzNMf2R8UDadPOl6ReyI+MT8xfqRuAnuVL+i4gwjv/zubdDK+EDeLHYwq1j0CSSR2W/MmgaRlMZJzXdmVA== + dependencies: + "@types/istanbul-lib-coverage" "^1.1.0" + "@types/yargs" "^12.0.9" + "@ringierag/snowplow-reactjs-native-tracker@^0.0.3": version "0.0.3" resolved "https://registry.yarnpkg.com/@ringierag/snowplow-reactjs-native-tracker/-/snowplow-reactjs-native-tracker-0.0.3.tgz#cd77ee6635bc0b4a05919cfded4072b3c8033c0c" dependencies: snowplow-tracker-core "^0.6.1" +"@types/istanbul-lib-coverage@^1.1.0": + version "1.1.0" + resolved "https://registry.yarnpkg.com/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-1.1.0.tgz#2cc2ca41051498382b43157c8227fea60363f94a" + integrity sha512-ohkhb9LehJy+PA40rDtGAji61NCgdtKLAlFoYp4cnuuQEswwdK3vz9SOIkkyc3wrk8dzjphQApNs56yyXLStaQ== + "@types/markdown-it@^0.0.4": version "0.0.4" resolved "https://registry.yarnpkg.com/@types/markdown-it/-/markdown-it-0.0.4.tgz#c5f67365916044b342dae8d702724788ba0b5b74" @@ -554,6 +567,11 @@ "@types/prop-types" "*" csstype "^2.2.0" +"@types/yargs@^12.0.9": + version "12.0.10" + resolved "https://registry.yarnpkg.com/@types/yargs/-/yargs-12.0.10.tgz#17a8ec65cd8e88f51b418ceb271af18d3137df67" + integrity sha512-WsVzTPshvCSbHThUduGGxbmnwcpkgSctHGHTqzWyFg4lYAuV5qXlyFPOsP3OWqCINfmg/8VXP+zJaa4OxEsBQQ== + abab@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/abab/-/abab-2.0.0.tgz#aba0ab4c5eee2d4c79d3487d85450fb2376ebb0f" @@ -6183,6 +6201,16 @@ pretty-format@^23.6.0: ansi-regex "^3.0.0" ansi-styles "^3.2.0" +pretty-format@^24.0.0: + version "24.5.0" + resolved "https://registry.yarnpkg.com/pretty-format/-/pretty-format-24.5.0.tgz#cc69a0281a62cd7242633fc135d6930cd889822d" + integrity sha512-/3RuSghukCf8Riu5Ncve0iI+BzVkbRU5EeUoArKARZobREycuH5O4waxvaNIloEXdb0qwgmEAed5vTpX1HNROQ== + dependencies: + "@jest/types" "^24.5.0" + ansi-regex "^4.0.0" + ansi-styles "^3.2.0" + react-is "^16.8.4" + pretty-format@^4.2.1: version "4.3.1" resolved "https://registry.yarnpkg.com/pretty-format/-/pretty-format-4.3.1.tgz#530be5c42b3c05b36414a7a2a4337aa80acd0e8d" @@ -6430,10 +6458,6 @@ react-i18next@^7.6.0: html-parse-stringify2 "2.0.1" prop-types "^15.6.0" -react-is@^16.3.2: - version "16.3.2" - resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.3.2.tgz#f4d3d0e2f5fbb6ac46450641eb2e25bf05d36b22" - react-is@^16.5.2: version "16.8.3" resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.8.3.tgz#4ad8b029c2a718fc0cfc746c8d4e1b7221e5387d" @@ -6447,6 +6471,11 @@ react-is@^16.8.1: resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.8.4.tgz#90f336a68c3a29a096a3d648ab80e87ec61482a2" integrity sha512-PVadd+WaUDOAciICm/J1waJaSvgq+4rHE/K70j0PFqKhkTBsPv/82UGQJNXAngz1fOQLLxI6z1sEDmJDQhCTAA== +react-is@^16.8.4, react-is@^16.8.6: + version "16.8.6" + resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.8.6.tgz#5bbc1e2d29141c9fbdfed456343fe2bc430a6a16" + integrity sha512-aUk3bHfZ2bRSVFFbbeVS4i+lNPZr3/WM5jT2J5omUVV1zzcs1nAaf3l51ctA5FFvCRbhrH0bdAsRRQddFJZPtA== + react-lifecycles-compat@^3, react-lifecycles-compat@^3.0.4: version "3.0.4" resolved "https://registry.yarnpkg.com/react-lifecycles-compat/-/react-lifecycles-compat-3.0.4.tgz#4f1a273afdfc8f3488a8c516bfda78f872352362" @@ -6588,6 +6617,13 @@ react-native-tab-view@^1.0.0: dependencies: prop-types "^15.6.1" +react-native-testing-library@^1.6.1: + version "1.6.1" + resolved "https://registry.yarnpkg.com/react-native-testing-library/-/react-native-testing-library-1.6.1.tgz#655c6ddbca6447339a7cd8729fb574ba3cf00d69" + integrity sha512-plGZ3zCP5zT4nxL+4x7sdmv/OFOIGRyWceIAe6CxO2ZC9BXauhAUKP/AUNI/5fviW2EkVnNqni61RVNoqsSYEQ== + dependencies: + pretty-format "^24.0.0" + react-native-vector-icons@^6.1.0: version "6.2.0" resolved "https://registry.yarnpkg.com/react-native-vector-icons/-/react-native-vector-icons-6.2.0.tgz#2682b718099eb9470c0defd38da47deb844a0f9c" @@ -6740,14 +6776,15 @@ react-test-renderer@^16.0.0-0: react-is "^16.8.2" scheduler "^0.13.2" -react-test-renderer@^16.3.2: - version "16.3.2" - resolved "https://registry.yarnpkg.com/react-test-renderer/-/react-test-renderer-16.3.2.tgz#3d1ed74fda8db42521fdf03328e933312214749a" +react-test-renderer@^16.8.6: + version "16.8.6" + resolved "https://registry.yarnpkg.com/react-test-renderer/-/react-test-renderer-16.8.6.tgz#188d8029b8c39c786f998aa3efd3ffe7642d5ba1" + integrity sha512-H2srzU5IWYT6cZXof6AhUcx/wEyJddQ8l7cLM/F7gDXYyPr4oq+vCIxJYXVGhId1J706sqziAjuOEjyNkfgoEw== dependencies: - fbjs "^0.8.16" object-assign "^4.1.1" - prop-types "^15.6.0" - react-is "^16.3.2" + prop-types "^15.6.2" + react-is "^16.8.6" + scheduler "^0.13.6" react-timer-mixin@^0.13.2: version "0.13.4" @@ -7281,6 +7318,14 @@ scheduler@^0.13.2: loose-envify "^1.1.0" object-assign "^4.1.1" +scheduler@^0.13.6: + version "0.13.6" + resolved "https://registry.yarnpkg.com/scheduler/-/scheduler-0.13.6.tgz#466a4ec332467b31a91b9bf74e5347072e4cd889" + integrity sha512-IWnObHt413ucAYKsD9J1QShUKkbKLQQHdxRyw73sw4FN26iWr3DY/H34xGPe4nmL1DwXyWmSWmMrA9TfQbE/XQ== + dependencies: + loose-envify "^1.1.0" + object-assign "^4.1.1" + semver-compare@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/semver-compare/-/semver-compare-1.0.0.tgz#0dee216a1c941ab37e9efb1788f6afc5ff5537fc"