Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

MHP-2233: Bring in react-native-testing-library and add utility funct… #891

Closed
wants to merge 2 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
5 changes: 4 additions & 1 deletion __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')),
})),
}));
1 change: 1 addition & 0 deletions __mock__/react-native-omniture.js
Expand Up @@ -3,4 +3,5 @@ jest.mock('react-native-omniture', () => ({
trackState: jest.fn(),
syncIdentifier: jest.fn(),
loadMarketingCloudId: jest.fn(),
collectLifecycleData: jest.fn(),
}));
3 changes: 2 additions & 1 deletion package.json
Expand Up @@ -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": {
Expand Down
69 changes: 28 additions & 41 deletions 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 {
Expand All @@ -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()),
}));
Expand All @@ -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 = [
Expand All @@ -44,60 +29,62 @@ const lastTwoArgs = [
];

beforeEach(() =>
(ReactNative.Alert.alert = jest
(Alert.alert = jest
.fn()
.mockImplementation((_, __, buttons) => buttons[0].onPress())));

const test = async response => {
const shallowScreen = shallow(<App />);

await shallowScreen.instance().handleError(response);
const testUnhandledRejection = response => {
render(<App />);

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,
);
});

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', () => {
Expand All @@ -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',
Expand All @@ -120,7 +107,7 @@ describe('__DEV__ === false', () => {
query: { filters: { organization_ids: '1' } },
};

await test(apiError);
testUnhandledRejection(apiError);

expect(rollbar.error).toHaveBeenCalledWith(
Error(
Expand All @@ -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)}`),
Expand Down
6 changes: 4 additions & 2 deletions src/components/SwipeTabMenu/__tests__/SwipeTabMenu.js
Expand Up @@ -34,12 +34,13 @@ const tabs = [
];

it('should render correctly', () => {
const component = testSnapshotShallow(
const component = renderShallow(
<SwipeTabMenu
tabs={tabs}
navigation={{ state: { index: 0, params: {} } }}
/>,
);
expect(component).toMatchSnapshot();

// Render update from manual onLayout callback with new rendered element size
component
Expand All @@ -51,13 +52,14 @@ it('should render correctly', () => {
});

it('should render light version correctly', () => {
const component = testSnapshotShallow(
const component = renderShallow(
<SwipeTabMenu
tabs={tabs}
navigation={{ state: { index: 0, params: {} } }}
isLight={true}
/>,
);
expect(component).toMatchSnapshot();

// Render update from manual onLayout callback with new rendered element size
component
Expand Down
12 changes: 7 additions & 5 deletions 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`] = `
<Component
dispatch={[Function]}
extraProp1={true}
extraProp2={false}
/>
<Text
accessible={true}
allowFontScaling={true}
ellipsizeMode="tail"
>
{"extraProp1":true,"extraProp2":false}
</Text>
`;
24 changes: 15 additions & 9 deletions 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';
Expand All @@ -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();
Expand All @@ -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(<WrappedTestComponent />, store).dive();
const { store } = renderWithRedux(<WrappedTestComponent />);

expect(store.getActions()).toEqual([
StackActions.push({
Expand All @@ -48,7 +50,7 @@ describe('wrapNextScreenFn', () => {
() => nextScreenName,
);

renderShallow(<WrappedTestComponent />, store).dive();
const { store } = renderWithRedux(<WrappedTestComponent />);

expect(store.getActions()).toEqual([
StackActions.push({
Expand All @@ -66,7 +68,7 @@ describe('wrapNextAction', () => {
props => dispatch => dispatch({ type: 'test', nextScreenName, props }),
);

renderShallow(<WrappedTestComponent />, store).dive();
const { store } = renderWithRedux(<WrappedTestComponent />);

expect(store.getActions()).toEqual([
{
Expand All @@ -80,12 +82,16 @@ describe('wrapNextAction', () => {

describe('wrapProps', () => {
it('should add extra props to component', () => {
const WrappedTestComponent = wrapProps(TestComponent, {
const PropStringifyComponent = props => (
<Text>{JSON.stringify(props)}</Text>
);

const WrappedTestComponent = wrapProps(PropStringifyComponent, {
extraProp1: true,
extraProp2: false,
});

testSnapshotShallow(<WrappedTestComponent />, store);
snapshotWithRedux(<WrappedTestComponent />);
});
});

Expand Down
45 changes: 38 additions & 7 deletions 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() });

Expand All @@ -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(<Provider store={store}>{ui}</Provider>),
// 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')) {
Expand All @@ -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;
};