diff --git a/.circleci/config.yml b/.circleci/config.yml index 8915fbae..0ce035d8 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -73,9 +73,9 @@ aliases: name: Flow Checks command: yarn test:flow -- &javascript - name: Javascript Tests - command: yarn test:js +- &jest + name: Jest Unit Tests + command: yarn test:jest # ------------------------- # JOBS @@ -110,12 +110,12 @@ jobs: at: ~/react-native-netinfo - run: *flow - javascript: + jest: <<: *linux_defaults steps: - attach_workspace: at: ~/react-native-netinfo - - run: *javascript + - run: *jest android-compile: <<: *android_defaults @@ -172,7 +172,7 @@ workflows: - flow: requires: - linux-checkout - - javascript: + - jest: requires: - linux-checkout - android-compile: diff --git a/.gitignore b/.gitignore index 3547cb26..949247d8 100644 --- a/.gitignore +++ b/.gitignore @@ -46,3 +46,6 @@ buck-out/ # Editor config .vscode + +# Outputs +coverage diff --git a/jest.setup.js b/jest.setup.js new file mode 100644 index 00000000..e39dfeca --- /dev/null +++ b/jest.setup.js @@ -0,0 +1,25 @@ +/** + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @format + * @flow + */ +/* eslint-env jest */ + +import {NativeModules} from 'react-native'; + +// Mock the RNCNetInfo native module to allow us to unit test the JavaScript code +NativeModules.RNCNetInfo = { + getCurrentConnectivity: jest.fn(), + isConnectionMetered: jest.fn(), + addListener: jest.fn(), + removeListeners: jest.fn(), +}; + +// Reset the mocks before each test +global.beforeEach(() => { + jest.resetAllMocks(); +}); diff --git a/js/__tests__/eventListenerCallbacks.js b/js/__tests__/eventListenerCallbacks.js new file mode 100644 index 00000000..07e1de66 --- /dev/null +++ b/js/__tests__/eventListenerCallbacks.js @@ -0,0 +1,111 @@ +/** + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @format + * @flow + */ +/* eslint-env jest */ + +import {NativeModules} from 'react-native'; +import NetInfo from '../index'; +import {NetInfoEventEmitter} from '../nativeInterface'; + +describe('react-native-netinfo', () => { + describe('Event listener callbacks', () => { + it('should call the listener when the native event is emmitted', () => { + const listener = jest.fn(); + NetInfo.addEventListener('connectionChange', listener); + + const expectedConnectionType = 'cellular'; + const expectedEffectiveConnectionType = '3g'; + + NetInfoEventEmitter.emit(NetInfo.Events.NetworkStatusDidChange, { + connectionType: expectedConnectionType, + effectiveConnectionType: expectedEffectiveConnectionType, + }); + + expect(listener).toBeCalledWith({ + type: expectedConnectionType, + effectiveType: expectedEffectiveConnectionType, + }); + }); + + it('should call the listener multiple times when multiple native events are emmitted', () => { + const listener = jest.fn(); + NetInfo.addEventListener('connectionChange', listener); + + NetInfoEventEmitter.emit(NetInfo.Events.NetworkStatusDidChange, { + connectionType: 'cellular', + effectiveConnectionType: '3g', + }); + NetInfoEventEmitter.emit(NetInfo.Events.NetworkStatusDidChange, { + connectionType: 'wifi', + effectiveConnectionType: 'unknown', + }); + + expect(listener).toBeCalledTimes(2); + }); + + it('should call all listeners when the native event is emmitted', () => { + const listener1 = jest.fn(); + const listener2 = jest.fn(); + NetInfo.addEventListener('connectionChange', listener1); + NetInfo.addEventListener('connectionChange', listener2); + + const expectedConnectionType = 'cellular'; + const expectedEffectiveConnectionType = '3g'; + + NetInfoEventEmitter.emit(NetInfo.Events.NetworkStatusDidChange, { + connectionType: expectedConnectionType, + effectiveConnectionType: expectedEffectiveConnectionType, + }); + + const expectedResults = { + type: expectedConnectionType, + effectiveType: expectedEffectiveConnectionType, + }; + + expect(listener1).toBeCalledWith(expectedResults); + expect(listener2).toBeCalledWith(expectedResults); + }); + + it('should not call the listener after being removed', () => { + const listener = jest.fn(); + NetInfo.addEventListener('connectionChange', listener); + NetInfo.removeEventListener('connectionChange', listener); + + NetInfoEventEmitter.emit(NetInfo.Events.NetworkStatusDidChange, { + connectionType: 'cellular', + effectiveConnectionType: '3g', + }); + + expect(listener).not.toBeCalled(); + }); + + it('should call the remaining listeners when one has been removed', () => { + const listener1 = jest.fn(); + const listener2 = jest.fn(); + NetInfo.addEventListener('connectionChange', listener1); + NetInfo.addEventListener('connectionChange', listener2); + + NetInfo.removeEventListener('connectionChange', listener1); + + const expectedConnectionType = 'cellular'; + const expectedEffectiveConnectionType = '3g'; + + NetInfoEventEmitter.emit(NetInfo.Events.NetworkStatusDidChange, { + connectionType: expectedConnectionType, + effectiveConnectionType: expectedEffectiveConnectionType, + }); + + expect(listener1).not.toBeCalled(); + expect(listener2).toBeCalledWith({ + type: expectedConnectionType, + effectiveType: expectedEffectiveConnectionType, + }); + }); + }); +}); diff --git a/js/__tests__/eventListenerManagement.js b/js/__tests__/eventListenerManagement.js new file mode 100644 index 00000000..2922ec8f --- /dev/null +++ b/js/__tests__/eventListenerManagement.js @@ -0,0 +1,60 @@ +/** + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @format + * @flow + */ +/* eslint-env jest */ + +import {NativeModules} from 'react-native'; +import NetInfo from '../index'; +import {NetInfoEventEmitter} from '../nativeInterface'; + +describe('react-native-netinfo', () => { + describe('Event listener management', () => { + it('should add the listener to the native module when passing the correct event name', () => { + NetInfo.addEventListener('connectionChange', jest.fn()); + expect(NativeModules.RNCNetInfo.addListener).toBeCalledWith( + NetInfo.Events.NetworkStatusDidChange, + ); + }); + + it('should do nothing when passing the wrong event name', () => { + // $FlowExpectedError We are testing passing in the wrong name + NetInfo.addEventListener('WRONGNAME', jest.fn()); + expect(NativeModules.RNCNetInfo.addListener).not.toBeCalled(); + }); + + it('should not error when calling remove on an invalid subscription', () => { + // $FlowExpectedError We are testing passing in the wrong name + const subscription = NetInfo.addEventListener('WRONGNAME', jest.fn()); + subscription.remove(); + expect(NativeModules.RNCNetInfo.addListener).not.toBeCalled(); + }); + + it('should remove the listener from the native module when calling removeEventListener', () => { + const listener = jest.fn(); + NetInfo.addEventListener('connectionChange', listener); + NetInfo.removeEventListener('connectionChange', listener); + expect(NativeModules.RNCNetInfo.removeListeners).toBeCalled(); + }); + + it('should not call the native module if asked to remove a listener which was never added', () => { + NetInfo.removeEventListener('connectionChange', jest.fn()); + expect(NativeModules.RNCNetInfo.removeListeners).not.toBeCalled(); + }); + + it('should remove the listener from the native module when calling remove on the returned subscription', () => { + const listener = jest.fn(); + const subscription = NetInfo.addEventListener( + 'connectionChange', + listener, + ); + subscription.remove(); + expect(NativeModules.RNCNetInfo.removeListeners).toBeCalled(); + }); + }); +}); diff --git a/js/__tests__/getConnectionInfo.js b/js/__tests__/getConnectionInfo.js new file mode 100644 index 00000000..75d59608 --- /dev/null +++ b/js/__tests__/getConnectionInfo.js @@ -0,0 +1,40 @@ +/** + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @format + * @flow + */ +/* eslint-env jest */ + +import NetInfo from '../index'; +import {RNCNetInfo} from '../nativeInterface'; + +describe('react-native-netinfo', () => { + describe('getConnectionInfo', () => { + it('should pass on the responses when the library promise returns', () => { + const expectedConnectionType = 'cellular'; + const expectedEffectiveConnectionType = '3g'; + + RNCNetInfo.getCurrentConnectivity.mockResolvedValue({ + connectionType: expectedConnectionType, + effectiveConnectionType: expectedEffectiveConnectionType, + }); + + return expect(NetInfo.getConnectionInfo()).resolves.toEqual({ + type: expectedConnectionType, + effectiveType: expectedEffectiveConnectionType, + }); + }); + + it('should pass on errors through the promise chain', () => { + const expectedError = new Error('A test error'); + + RNCNetInfo.getCurrentConnectivity.mockRejectedValue(expectedError); + + return expect(NetInfo.getConnectionInfo()).rejects.toBe(expectedError); + }); + }); +}); diff --git a/js/__tests__/isConnected.js b/js/__tests__/isConnected.js new file mode 100644 index 00000000..ac2b3f76 --- /dev/null +++ b/js/__tests__/isConnected.js @@ -0,0 +1,158 @@ +/** + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @format + * @flow + */ +/* eslint-env jest */ + +import {NativeModules} from 'react-native'; +import NetInfo from '../index'; +import {NetInfoEventEmitter} from '../nativeInterface'; + +const CONNECTED_STATES = [ + {type: 'cellular', connected: true}, + {type: 'wifi', connected: true}, + {type: 'bluetooth', connected: true}, + {type: 'ethernet', connected: true}, + {type: 'wimax', connected: true}, + {type: 'none', connected: false}, + {type: 'unknown', connected: false}, +]; + +describe('react-native-netinfo', () => { + describe('isConnected', () => { + describe('fetch', () => { + CONNECTED_STATES.map(({type, connected}) => { + it(`should resolve to ${connected.toString()} when the native module returns a ${type} state`, () => { + NativeModules.RNCNetInfo.getCurrentConnectivity.mockResolvedValue({ + connectionType: type, + effectiveConnectionType: 'unknown', + }); + + return expect(NetInfo.isConnected.fetch()).resolves.toBe(connected); + }); + }); + + it('should pass on errors through the promise chain', () => { + const expectedError = new Error('A test error'); + + NativeModules.RNCNetInfo.getCurrentConnectivity.mockRejectedValue( + expectedError, + ); + + return expect(NetInfo.getConnectionInfo()).rejects.toBe(expectedError); + }); + }); + + describe('Event listener management', () => { + it('should add the listener to the native module when passing the correct event name', () => { + NetInfo.isConnected.addEventListener('connectionChange', jest.fn()); + expect(NativeModules.RNCNetInfo.addListener).toBeCalledWith( + NetInfo.Events.NetworkStatusDidChange, + ); + }); + + it('should do nothing when passing the wrong event name', () => { + // $FlowExpectedError We are testing passing in the wrong name + NetInfo.isConnected.addEventListener('WRONGNAME', jest.fn()); + expect(NativeModules.RNCNetInfo.addListener).not.toBeCalled(); + }); + + it('should remove the listener from the native module when calling removeEventListener', () => { + const listener = jest.fn(); + NetInfo.isConnected.addEventListener('connectionChange', listener); + NetInfo.isConnected.removeEventListener('connectionChange', listener); + expect(NativeModules.RNCNetInfo.removeListeners).toBeCalled(); + }); + + it('should remove the listener from the native module when calling remove on the returned subscription', () => { + const listener = jest.fn(); + const subscription = NetInfo.isConnected.addEventListener( + 'connectionChange', + listener, + ); + subscription.remove(); + expect(NativeModules.RNCNetInfo.removeListeners).toBeCalled(); + }); + }); + + describe('Event listener callbacks', () => { + it('should call the listener when the native event is emmitted', () => { + const listener = jest.fn(); + NetInfo.isConnected.addEventListener('connectionChange', listener); + + NetInfoEventEmitter.emit(NetInfo.Events.NetworkStatusDidChange, { + connectionType: 'cellular', + effectiveConnectionType: '4g', + }); + + expect(listener).toBeCalledWith(true); + }); + + it('should call the listener multiple times when multiple native events are emmitted', () => { + const listener = jest.fn(); + NetInfo.isConnected.addEventListener('connectionChange', listener); + + NetInfoEventEmitter.emit(NetInfo.Events.NetworkStatusDidChange, { + connectionType: 'cellular', + effectiveConnectionType: '3g', + }); + NetInfoEventEmitter.emit(NetInfo.Events.NetworkStatusDidChange, { + connectionType: 'wifi', + effectiveConnectionType: 'unknown', + }); + + expect(listener).toBeCalledTimes(2); + }); + + it('should call all listeners when the native event is emmitted', () => { + const listener1 = jest.fn(); + const listener2 = jest.fn(); + NetInfo.isConnected.addEventListener('connectionChange', listener1); + NetInfo.isConnected.addEventListener('connectionChange', listener2); + + NetInfoEventEmitter.emit(NetInfo.Events.NetworkStatusDidChange, { + connectionType: 'cellular', + effectiveConnectionType: '2g', + }); + + expect(listener1).toBeCalledWith(true); + expect(listener2).toBeCalledWith(true); + }); + + it('should not call the listener after being removed', () => { + const listener = jest.fn(); + NetInfo.isConnected.addEventListener('connectionChange', listener); + NetInfo.isConnected.removeEventListener('connectionChange', listener); + + NetInfoEventEmitter.emit(NetInfo.Events.NetworkStatusDidChange, { + connectionType: 'cellular', + effectiveConnectionType: '3g', + }); + + expect(listener).not.toBeCalled(); + }); + + it('should call the remaining listeners when one has been removed', () => { + const listener1 = jest.fn(); + const listener2 = jest.fn(); + NetInfo.isConnected.addEventListener('connectionChange', listener1); + NetInfo.isConnected.addEventListener('connectionChange', listener2); + + NetInfo.isConnected.removeEventListener('connectionChange', listener1); + + NetInfoEventEmitter.emit(NetInfo.Events.NetworkStatusDidChange, { + connectionType: 'unknown', + effectiveConnectionType: 'unknown', + }); + + expect(listener1).not.toBeCalled(); + expect(listener2).toBeCalledWith(false); + }); + }); + }); +}); diff --git a/js/__tests__/isConnectionExpensiveAndroid.js b/js/__tests__/isConnectionExpensiveAndroid.js new file mode 100644 index 00000000..ebe7d5e2 --- /dev/null +++ b/js/__tests__/isConnectionExpensiveAndroid.js @@ -0,0 +1,33 @@ +/** + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @format + * @flow + */ +/* eslint-env jest */ + +jest.mock('Platform', () => { + const Platform = jest.requireActual('Platform'); + Platform.OS = 'android'; + return Platform; +}); + +import NetInfo from '../index'; +import {RNCNetInfo} from '../nativeInterface'; + +describe('react-native-netinfo', () => { + describe('isConnectionExpensive', () => { + describe('Android', () => { + it('should pass on errors through the promise chain', () => { + const expectedError = new Error('A test error'); + RNCNetInfo.isConnectionMetered.mockRejectedValue(expectedError); + return expect(NetInfo.isConnectionExpensive()).rejects.toBe( + expectedError, + ); + }); + }); + }); +}); diff --git a/js/__tests__/isConnectionExpensiveIOS.js b/js/__tests__/isConnectionExpensiveIOS.js new file mode 100644 index 00000000..2f5739fb --- /dev/null +++ b/js/__tests__/isConnectionExpensiveIOS.js @@ -0,0 +1,29 @@ +/** + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @format + * @flow + */ +/* eslint-env jest */ + +jest.mock('Platform', () => { + const Platform = jest.requireActual('Platform'); + Platform.OS = 'ios'; + return Platform; +}); + +import NetInfo from '../index'; +import {RNCNetInfo} from '../nativeInterface'; + +describe('react-native-netinfo', () => { + describe('isConnectionExpensive', () => { + describe('iOS', () => { + it('should reject with an error when called', () => { + return expect(NetInfo.isConnectionExpensive()).rejects.toThrowError(); + }); + }); + }); +}); diff --git a/js/index.js b/js/index.js index a66a41de..3a1e1335 100644 --- a/js/index.js +++ b/js/index.js @@ -10,10 +10,8 @@ 'use strict'; -import {NativeEventEmitter, NativeModules, Platform} from 'react-native'; - -const {RNCNetInfo} = NativeModules; -const NetInfoEventEmitter = new NativeEventEmitter(RNCNetInfo); +import {Platform} from 'react-native'; +import {RNCNetInfo, NetInfoEventEmitter} from './nativeInterface'; const DEVICE_CONNECTIVITY_EVENT = 'networkStatusDidChange'; @@ -53,6 +51,10 @@ const _isConnectedSubscriptions = new Map(); * See https://facebook.github.io/react-native/docs/netinfo.html */ const NetInfo = { + Events: { + NetworkStatusDidChange: DEVICE_CONNECTIVITY_EVENT, + }, + /** * Adds an event handler. * diff --git a/js/nativeInterface.js b/js/nativeInterface.js new file mode 100644 index 00000000..3559c297 --- /dev/null +++ b/js/nativeInterface.js @@ -0,0 +1,28 @@ +/** + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @format + * @flow + */ + +import {NativeEventEmitter, NativeModules} from 'react-native'; + +const {RNCNetInfo} = NativeModules; + +/** + * We export the native interface in this way to give easy shared access to it between the + * JavaScript code and the tests + */ +let nativeEventEmitter = null; +module.exports = { + RNCNetInfo, + get NetInfoEventEmitter() { + if (!nativeEventEmitter) { + nativeEventEmitter = new NativeEventEmitter(RNCNetInfo); + } + return nativeEventEmitter; + }, +}; diff --git a/package.json b/package.json index ce6c2d0e..c1821400 100644 --- a/package.json +++ b/package.json @@ -10,10 +10,10 @@ "license": "MIT", "scripts": { "start": "node node_modules/react-native/local-cli/cli.js start", - "test": "yarn test:eslint && yarn test:flow && yarn test:js", + "test": "yarn test:eslint && yarn test:flow && yarn test:jest", "test:eslint": "eslint 'js/**/*.js' 'example/**/*.js'", "test:flow": "flow check", - "test:js": "echo 0" + "test:jest": "jest" }, "keywords": [ "react-native", @@ -50,6 +50,10 @@ "react-native": "0.58.4", "react-test-renderer": "16.6.3" }, + "jest": { + "preset": "react-native", + "setupFilesAfterEnv": ["/jest.setup.js"] + }, "repository": { "type": "git", "url": "https://github.com/react-native-community/react-native-netinfo.git"