From b97799eb4672a20db862d191eee9a496fe73de35 Mon Sep 17 00:00:00 2001 From: Emiliano Sanchez Date: Mon, 5 Jul 2021 13:36:36 -0300 Subject: [PATCH 1/5] RNSignalListener implementation --- src/platform/RNSignalListener.ts | 105 +++++++++++++++++++++++++++++++ src/platform/getModules.ts | 6 +- 2 files changed, 109 insertions(+), 2 deletions(-) create mode 100644 src/platform/RNSignalListener.ts diff --git a/src/platform/RNSignalListener.ts b/src/platform/RNSignalListener.ts new file mode 100644 index 0000000..947908a --- /dev/null +++ b/src/platform/RNSignalListener.ts @@ -0,0 +1,105 @@ +import { ISignalListener } from '@splitsoftware/splitio-commons/src/listeners/types'; +import { ISplitApi } from '@splitsoftware/splitio-commons/src/services/types'; +import { IStorageSync } from '@splitsoftware/splitio-commons/src/storages/types'; +import { ISyncManager } from '@splitsoftware/splitio-commons/src/sync/types'; +import { ISettings } from '@splitsoftware/splitio-commons/src/types'; +import { AppState, AppStateStatus } from 'react-native'; + +type Transition = undefined | 'TO_FOREGROUND' | 'TO_BACKGROUND'; +const TO_FOREGROUND = 'TO_FOREGROUND'; +const TO_BACKGROUND = 'TO_BACKGROUND'; + +const BACKGROUND_STOP_DELAY_MILLIS = 60000; // 1 minute +const FOREGROUND_MATCHER = /active/; + +export interface IBackgroundTimer { + setTimeout(callback: (...args: any[]) => void, timeout: number): number; + clearTimeout(timeoutId: number): void; +} + +/** + * In mobile, unlike browser, we cannot listen + */ +export class RNSignalListener implements ISignalListener { + private _timeoutId: number | undefined; + private _lastTransition: Transition | undefined; + + constructor( + private syncManager: ISyncManager, + private settings: ISettings & { flushDataOnBackground?: boolean }, + storage: IStorageSync, + serviceApi: ISplitApi, + private platform: { + backgroundTimer?: IBackgroundTimer; + } = {} + ) {} + + private _getTransition(nextAppState: AppStateStatus): Transition { + // 'inactive' iOS state and 'active' state are considered foreground states. + // In both states, the features used by the SDK (fetch, timers, EventSource, NetInfo) work fine. + // https://developer.apple.com/documentation/uikit/app_and_environment/managing_your_app_s_life_cycle + + let transition: Transition = + FOREGROUND_MATCHER.test(nextAppState) && this._lastTransition !== TO_FOREGROUND + ? TO_FOREGROUND + : nextAppState === 'background' && this._lastTransition !== TO_FOREGROUND + ? TO_BACKGROUND + : undefined; + + if (transition) this._lastTransition = transition; + return transition; + } + + private _handleAppStateChange(nextAppState: AppStateStatus) { + const action = this._getTransition(nextAppState); + + switch (action) { + case TO_FOREGROUND: + // This branch is called when app transition to foreground. + // Here, synchronization is started or resumed. + + if (this.platform.backgroundTimer && this._timeoutId) { + this.platform.backgroundTimer.clearTimeout(this._timeoutId); + } + this.syncManager.start(); + break; + case TO_BACKGROUND: + // This branch is called when the app transition to background. + // Here, synchronization is stopped to reduce battery consumption, particularly because of streaming connections + // in Android: we need to close it. In iOS, connections are automatically closed/resumed by the OS. + // JS timers callbacks are not executed in the background and thus polling mode doesn't work. + // Other features like Fetch, AsyncStorage, AppState and NetInfo listeners, can be used. + + // If background timer is available, the syncManager is stopped with a delay. + if (this.platform.backgroundTimer) { + this._timeoutId = this.platform.backgroundTimer.setTimeout(this.syncManager.stop, BACKGROUND_STOP_DELAY_MILLIS); + } else { + this.syncManager.stop(); + } + + // We cannot listen when the app is evicted by the OS or the user, but it always transitions to background before that. + // So we should not flush events and impressions in the background, except that they cannot be stored in a persistent storage. + if (this.settings.flushDataOnBackground) this.syncManager.flush(); + break; + } + } + + /** + * start method. + * Called when SplitFactory is initialized. + */ + start() { + // Cannot start the syncManager without checking the app state, because the SDK + // might be initiated in the background and should not connect to streaming. + this._handleAppStateChange(AppState.currentState); + AppState.addEventListener('change', this._handleAppStateChange); + } + + /** + * stop method. + * Called when client is destroyed. + */ + stop() { + AppState.removeEventListener('change', this._handleAppStateChange); + } +} diff --git a/src/platform/getModules.ts b/src/platform/getModules.ts index 6c2ed5b..ead6728 100644 --- a/src/platform/getModules.ts +++ b/src/platform/getModules.ts @@ -12,6 +12,8 @@ import { shouldAddPt } from '@splitsoftware/splitio-commons/src/trackers/impress import { ISettingsInternal } from '@splitsoftware/splitio-commons/src/utils/settingsValidation/types'; import { ISdkFactoryParams } from '@splitsoftware/splitio-commons/src/sdkFactory/types'; +import { RNSignalListener } from './RNSignalListener'; + const rnPlatform = { // Return global fetch which is always available in RN runtime getFetch() { @@ -41,8 +43,8 @@ export function getModules(settings: ISettingsInternal): ISdkFactoryParams { sdkManagerFactory, sdkClientMethodFactory: sdkClientMethodCSFactory, - // @TODO provide RN signal listener - SignalListener: undefined, + + SignalListener: settings.mode === 'localhost' ? undefined : (RNSignalListener as ISdkFactoryParams['SignalListener']), // @ts-ignore impressionListener: settings.impressionListener, From f21ac5b706f9fa22c9fb9564d2b398fb50afc604 Mon Sep 17 00:00:00 2001 From: Emiliano Sanchez Date: Wed, 7 Jul 2021 15:47:45 -0300 Subject: [PATCH 2/5] removed background timers --- src/platform/RNSignalListener.ts | 50 +++++++++----------------------- src/settings/index.ts | 5 +++- 2 files changed, 18 insertions(+), 37 deletions(-) diff --git a/src/platform/RNSignalListener.ts b/src/platform/RNSignalListener.ts index 947908a..66b8c79 100644 --- a/src/platform/RNSignalListener.ts +++ b/src/platform/RNSignalListener.ts @@ -1,6 +1,4 @@ import { ISignalListener } from '@splitsoftware/splitio-commons/src/listeners/types'; -import { ISplitApi } from '@splitsoftware/splitio-commons/src/services/types'; -import { IStorageSync } from '@splitsoftware/splitio-commons/src/storages/types'; import { ISyncManager } from '@splitsoftware/splitio-commons/src/sync/types'; import { ISettings } from '@splitsoftware/splitio-commons/src/types'; import { AppState, AppStateStatus } from 'react-native'; @@ -9,30 +7,16 @@ type Transition = undefined | 'TO_FOREGROUND' | 'TO_BACKGROUND'; const TO_FOREGROUND = 'TO_FOREGROUND'; const TO_BACKGROUND = 'TO_BACKGROUND'; -const BACKGROUND_STOP_DELAY_MILLIS = 60000; // 1 minute +// const BACKGROUND_STOP_DELAY_MILLIS = 60000; // 1 minute const FOREGROUND_MATCHER = /active/; -export interface IBackgroundTimer { - setTimeout(callback: (...args: any[]) => void, timeout: number): number; - clearTimeout(timeoutId: number): void; -} - /** - * In mobile, unlike browser, we cannot listen + * In ReactNative, we listen app state (foreground/background) to pause/resume synchronization. */ export class RNSignalListener implements ISignalListener { - private _timeoutId: number | undefined; private _lastTransition: Transition | undefined; - constructor( - private syncManager: ISyncManager, - private settings: ISettings & { flushDataOnBackground?: boolean }, - storage: IStorageSync, - serviceApi: ISplitApi, - private platform: { - backgroundTimer?: IBackgroundTimer; - } = {} - ) {} + constructor(private syncManager: ISyncManager, private settings: ISettings & { flushDataOnBackground?: boolean }) {} private _getTransition(nextAppState: AppStateStatus): Transition { // 'inactive' iOS state and 'active' state are considered foreground states. @@ -55,27 +39,21 @@ export class RNSignalListener implements ISignalListener { switch (action) { case TO_FOREGROUND: - // This branch is called when app transition to foreground. - // Here, synchronization is started or resumed. + // This branch is called when app transition to foreground or it is launched, + // in which case calling pushManager.start has no effect (it has been already started). + // Here, synchronization is resumed. - if (this.platform.backgroundTimer && this._timeoutId) { - this.platform.backgroundTimer.clearTimeout(this._timeoutId); - } - this.syncManager.start(); + if (this.syncManager.pushManager) this.syncManager.pushManager.start(); break; case TO_BACKGROUND: // This branch is called when the app transition to background. - // Here, synchronization is stopped to reduce battery consumption, particularly because of streaming connections - // in Android: we need to close it. In iOS, connections are automatically closed/resumed by the OS. - // JS timers callbacks are not executed in the background and thus polling mode doesn't work. + // Here, pushManager is stopped to close streaming connections for Android. + // In iOS, connections are automatically closed/resumed by the OS. + // Polling mode is paused during background, since JS timers callbacks are executed only + // when the app is in the foreground. Therefore, stopping syncManager is not necessary. // Other features like Fetch, AsyncStorage, AppState and NetInfo listeners, can be used. - // If background timer is available, the syncManager is stopped with a delay. - if (this.platform.backgroundTimer) { - this._timeoutId = this.platform.backgroundTimer.setTimeout(this.syncManager.stop, BACKGROUND_STOP_DELAY_MILLIS); - } else { - this.syncManager.stop(); - } + if (this.syncManager.pushManager) this.syncManager.pushManager.stop(); // We cannot listen when the app is evicted by the OS or the user, but it always transitions to background before that. // So we should not flush events and impressions in the background, except that they cannot be stored in a persistent storage. @@ -89,8 +67,8 @@ export class RNSignalListener implements ISignalListener { * Called when SplitFactory is initialized. */ start() { - // Cannot start the syncManager without checking the app state, because the SDK - // might be initiated in the background and should not connect to streaming. + // Checking currentState in the rare case that the SDK is instantiated in the background, where streaming should not connect. + // The SDK should be instantiated when React Native JS context is initiated or the root componentDidMount method is called, where the app is in the foreground. this._handleAppStateChange(AppState.currentState); AppState.addEventListener('change', this._handleAppStateChange); } diff --git a/src/settings/index.ts b/src/settings/index.ts index bd53491..35a593d 100644 --- a/src/settings/index.ts +++ b/src/settings/index.ts @@ -12,5 +12,8 @@ const params = { }; export function settingsValidator(config: any) { - return settingsValidation(config, params); + const settings = settingsValidation(config, params); + // @ts-ignore. For internal use, flush data on background until a persistent storage is provided. + settings.flushDataOnBackground = true; + return settings; } From f825a14a7c4c3ec295eb1e399e2cdfd4ef93bc07 Mon Sep 17 00:00:00 2001 From: Emiliano Sanchez Date: Thu, 8 Jul 2021 14:47:33 -0300 Subject: [PATCH 3/5] added logs --- src/platform/RNSignalListener.ts | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/src/platform/RNSignalListener.ts b/src/platform/RNSignalListener.ts index 66b8c79..0883edd 100644 --- a/src/platform/RNSignalListener.ts +++ b/src/platform/RNSignalListener.ts @@ -1,4 +1,5 @@ import { ISignalListener } from '@splitsoftware/splitio-commons/src/listeners/types'; +import { CLEANUP_REGISTERING, CLEANUP_DEREGISTERING } from '@splitsoftware/splitio-commons/src/logger/constants'; import { ISyncManager } from '@splitsoftware/splitio-commons/src/sync/types'; import { ISettings } from '@splitsoftware/splitio-commons/src/types'; import { AppState, AppStateStatus } from 'react-native'; @@ -10,6 +11,8 @@ const TO_BACKGROUND = 'TO_BACKGROUND'; // const BACKGROUND_STOP_DELAY_MILLIS = 60000; // 1 minute const FOREGROUND_MATCHER = /active/; +const EVENT_NAME = 'for AppState change events.'; + /** * In ReactNative, we listen app state (foreground/background) to pause/resume synchronization. */ @@ -42,8 +45,9 @@ export class RNSignalListener implements ISignalListener { // This branch is called when app transition to foreground or it is launched, // in which case calling pushManager.start has no effect (it has been already started). // Here, synchronization is resumed. - if (this.syncManager.pushManager) this.syncManager.pushManager.start(); + + this.settings.log.debug(`App transition to foreground ${this.syncManager.pushManager ? ': attempting to resume streaming' : ''}`); break; case TO_BACKGROUND: // This branch is called when the app transition to background. @@ -52,12 +56,17 @@ export class RNSignalListener implements ISignalListener { // Polling mode is paused during background, since JS timers callbacks are executed only // when the app is in the foreground. Therefore, stopping syncManager is not necessary. // Other features like Fetch, AsyncStorage, AppState and NetInfo listeners, can be used. - if (this.syncManager.pushManager) this.syncManager.pushManager.stop(); // We cannot listen when the app is evicted by the OS or the user, but it always transitions to background before that. // So we should not flush events and impressions in the background, except that they cannot be stored in a persistent storage. if (this.settings.flushDataOnBackground) this.syncManager.flush(); + + this.settings.log.debug( + `App transition to background ${this.syncManager.pushManager ? ': pausing streaming' : ''}${ + this.settings.flushDataOnBackground ? ', flushing events and impressions' : '' + }` + ); break; } } @@ -67,6 +76,7 @@ export class RNSignalListener implements ISignalListener { * Called when SplitFactory is initialized. */ start() { + this.settings.log.debug(CLEANUP_REGISTERING, [EVENT_NAME]); // Checking currentState in the rare case that the SDK is instantiated in the background, where streaming should not connect. // The SDK should be instantiated when React Native JS context is initiated or the root componentDidMount method is called, where the app is in the foreground. this._handleAppStateChange(AppState.currentState); @@ -78,6 +88,7 @@ export class RNSignalListener implements ISignalListener { * Called when client is destroyed. */ stop() { + this.settings.log.debug(CLEANUP_DEREGISTERING, [EVENT_NAME]); AppState.removeEventListener('change', this._handleAppStateChange); } } From 40becff26aa16f016216746a463f84d4328df8ae Mon Sep 17 00:00:00 2001 From: Emiliano Sanchez Date: Mon, 19 Jul 2021 18:26:44 -0300 Subject: [PATCH 4/5] added UTs and fixed some issues --- package-lock.json | 8 +- package.json | 42 ++++--- src/platform/RNSignalListener.ts | 20 ++-- .../__tests__/RNSignalListener.spec.ts | 106 ++++++++++++++++++ src/settings/defaults.ts | 2 +- 5 files changed, 151 insertions(+), 27 deletions(-) create mode 100644 src/platform/__tests__/RNSignalListener.spec.ts diff --git a/package-lock.json b/package-lock.json index f27d14d..d68eb42 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "splitio-react-native-private", - "version": "0.0.1-beta.4", + "version": "0.0.1-beta.5", "lockfileVersion": 1, "requires": true, "dependencies": { @@ -2453,9 +2453,9 @@ } }, "@splitsoftware/splitio-commons": { - "version": "0.1.1-canary.10", - "resolved": "https://registry.npmjs.org/@splitsoftware/splitio-commons/-/splitio-commons-0.1.1-canary.10.tgz", - "integrity": "sha512-jwZ594xucI/dTyXaRgzBXp9VpRVKF4BWzkIjMKhZ0ChFxTGfCjrslKffyOdvtoZPlyzEVFaJoBWF47Hh2Hw2vw==", + "version": "0.1.1-canary.11", + "resolved": "https://registry.npmjs.org/@splitsoftware/splitio-commons/-/splitio-commons-0.1.1-canary.11.tgz", + "integrity": "sha512-zbTmbEfpMiG8FynCLg8nVoDToXTDSVNGIpE7SIyw3gsKg2gJlzGAHkr419pV4c0uXTq+N6BvfwrdKxlmsSUgFg==", "requires": { "object-assign": "^4.1.1", "tslib": "^2.1.0" diff --git a/package.json b/package.json index d3205ae..32ae154 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "splitio-react-native-private", - "version": "0.0.1-beta.4", + "version": "0.0.1-beta.5", "description": "Split SDK for React Native", "main": "lib/commonjs/index", "module": "lib/module/index", @@ -21,7 +21,7 @@ "!**/__mocks__" ], "scripts": { - "test": "jest", + "test": "jest src", "check": "npm run check:lint && npm run check:types && npm run check:version", "check:lint": "eslint \"**/*.{js,ts,tsx}\"", "check:types": "tsc --noEmit", @@ -52,7 +52,7 @@ "registry": "https://splitio.jfrog.io/artifactory/api/npm/npm-split/" }, "dependencies": { - "@splitsoftware/splitio-commons": "0.1.1-canary.10" + "@splitsoftware/splitio-commons": "0.1.1-canary.11" }, "devDependencies": { "@react-native-community/eslint-config": "^2.0.0", @@ -81,6 +81,9 @@ "preset": "react-native", "modulePathIgnorePatterns": [ "/lib/" + ], + "transformIgnorePatterns": [ + "/node_modules/(?!@splitsoftware)/" ] }, "husky": { @@ -98,14 +101,6 @@ "import" ], "rules": { - "no-restricted-syntax": [ - "error", - "ForOfStatement", - "ForInStatement" - ], - "no-throw-literal": "error", - "import/no-default-export": "error", - "import/no-self-import": "error", "prettier/prettier": [ "error", { @@ -117,11 +112,32 @@ "useTabs": false } ] - } + }, + "overrides": [ + { + "files": [ + "src/**/*.ts" + ], + "excludedFiles": [ + "src/**/__tests__/**" + ], + "rules": { + "no-restricted-syntax": [ + "error", + "ForOfStatement", + "ForInStatement" + ], + "no-throw-literal": "error", + "import/no-default-export": "error", + "import/no-self-import": "error" + } + } + ] }, "eslintIgnore": [ "node_modules/", - "lib/" + "lib/", + "example/" ], "prettier": { "quoteProps": "consistent", diff --git a/src/platform/RNSignalListener.ts b/src/platform/RNSignalListener.ts index 0883edd..9eadf05 100644 --- a/src/platform/RNSignalListener.ts +++ b/src/platform/RNSignalListener.ts @@ -29,7 +29,7 @@ export class RNSignalListener implements ISignalListener { let transition: Transition = FOREGROUND_MATCHER.test(nextAppState) && this._lastTransition !== TO_FOREGROUND ? TO_FOREGROUND - : nextAppState === 'background' && this._lastTransition !== TO_FOREGROUND + : nextAppState === 'background' && this._lastTransition !== TO_BACKGROUND ? TO_BACKGROUND : undefined; @@ -37,19 +37,26 @@ export class RNSignalListener implements ISignalListener { return transition; } - private _handleAppStateChange(nextAppState: AppStateStatus) { + private _handleAppStateChange = (nextAppState: AppStateStatus) => { const action = this._getTransition(nextAppState); switch (action) { case TO_FOREGROUND: + this.settings.log.debug(`App transition to foreground${this.syncManager.pushManager ? ': attempting to resume streaming' : ''}`); + // This branch is called when app transition to foreground or it is launched, // in which case calling pushManager.start has no effect (it has been already started). // Here, synchronization is resumed. if (this.syncManager.pushManager) this.syncManager.pushManager.start(); - this.settings.log.debug(`App transition to foreground ${this.syncManager.pushManager ? ': attempting to resume streaming' : ''}`); break; case TO_BACKGROUND: + this.settings.log.debug( + `App transition to background${this.syncManager.pushManager ? ': pausing streaming' : ''}${ + this.settings.flushDataOnBackground ? ': flushing events and impressions' : '' + }` + ); + // This branch is called when the app transition to background. // Here, pushManager is stopped to close streaming connections for Android. // In iOS, connections are automatically closed/resumed by the OS. @@ -62,14 +69,9 @@ export class RNSignalListener implements ISignalListener { // So we should not flush events and impressions in the background, except that they cannot be stored in a persistent storage. if (this.settings.flushDataOnBackground) this.syncManager.flush(); - this.settings.log.debug( - `App transition to background ${this.syncManager.pushManager ? ': pausing streaming' : ''}${ - this.settings.flushDataOnBackground ? ', flushing events and impressions' : '' - }` - ); break; } - } + }; /** * start method. diff --git a/src/platform/__tests__/RNSignalListener.spec.ts b/src/platform/__tests__/RNSignalListener.spec.ts new file mode 100644 index 0000000..3430dcf --- /dev/null +++ b/src/platform/__tests__/RNSignalListener.spec.ts @@ -0,0 +1,106 @@ +import { AppStateStatus } from 'react-native'; +import { RNSignalListener } from '../RNSignalListener'; + +let changeListeners = new Set<(state: AppStateStatus) => void>(); + +const AppStateMock = { + currentState: 'active', + addEventListener: jest.fn((type, listener) => { + if (type === 'change') { + changeListeners.add(listener); + } + }), + removeEventListener: jest.fn((type, listener) => { + if (type === 'change') { + changeListeners.delete(listener); + } + }), + _emitChangeEvent(event: AppStateStatus) { + changeListeners.forEach((listener) => { + listener(event); + }); + }, +}; + +jest.doMock('react-native/Libraries/AppState/AppState', () => AppStateMock); + +const syncManagerMockWithPushManager = { + flush: jest.fn(), + pushManager: { start: jest.fn(), stop: jest.fn() }, +}; +const settingsMock = { + log: { debug: jest.fn() }, + flushDataOnBackground: true, +}; + +describe('RNSignalListener', () => { + beforeEach(() => { + changeListeners.clear(); + AppStateMock.addEventListener.mockClear(); + AppStateMock.removeEventListener.mockClear(); + syncManagerMockWithPushManager.flush.mockClear(); + syncManagerMockWithPushManager.pushManager.stop.mockClear(); + syncManagerMockWithPushManager.pushManager.start.mockClear(); + }); + + test('starting in foreground', () => { + // @ts-expect-error. SyncManager mock partially implemented + const signalListener = new RNSignalListener(syncManagerMockWithPushManager, settingsMock); + + // Starting with app in foreground + AppStateMock.currentState = 'active'; + signalListener.start(); + expect(AppStateMock.addEventListener).toBeCalledTimes(1); + expect(syncManagerMockWithPushManager.pushManager.start).toBeCalledTimes(1); + + // Going to background should be handled + AppStateMock._emitChangeEvent('background'); + expect(syncManagerMockWithPushManager.flush).toBeCalledTimes(1); + expect(syncManagerMockWithPushManager.pushManager.stop).toBeCalledTimes(1); + + // Handling another background event, which shouldn't happen, have no effect + AppStateMock._emitChangeEvent('background'); + expect(syncManagerMockWithPushManager.flush).toBeCalledTimes(1); + expect(syncManagerMockWithPushManager.pushManager.stop).toBeCalledTimes(1); + expect(syncManagerMockWithPushManager.pushManager.stop).toBeCalledTimes(1); + + // Going to foreground should be handled + AppStateMock._emitChangeEvent('inactive'); + expect(syncManagerMockWithPushManager.pushManager.start).toBeCalledTimes(2); + + // Handling another foreground event, have no effect + AppStateMock._emitChangeEvent('active'); + AppStateMock._emitChangeEvent('inactive'); + AppStateMock._emitChangeEvent('inactive'); + expect(syncManagerMockWithPushManager.pushManager.start).toBeCalledTimes(2); + + // Going to background should be handled again + AppStateMock._emitChangeEvent('background'); + expect(syncManagerMockWithPushManager.flush).toBeCalledTimes(2); + expect(syncManagerMockWithPushManager.pushManager.stop).toBeCalledTimes(2); + + // Going to foreground should be handled again + AppStateMock._emitChangeEvent('active'); + expect(syncManagerMockWithPushManager.pushManager.start).toBeCalledTimes(3); + + // Stopping RNSignalListener + signalListener.stop(); + expect(AppStateMock.removeEventListener).toBeCalledTimes(1); + expect(syncManagerMockWithPushManager.flush).toBeCalledTimes(2); + expect(syncManagerMockWithPushManager.pushManager.stop).toBeCalledTimes(2); + expect(syncManagerMockWithPushManager.pushManager.start).toBeCalledTimes(3); + }); + + test('starting in background & without flushDataOnBackground', () => { + // @ts-expect-error. SyncManager mock partially implemented + const signalListener = new RNSignalListener(syncManagerMockWithPushManager, { ...settingsMock, flushDataOnBackground: undefined }); + + // Starting with app in background + AppStateMock.currentState = 'background'; + signalListener.start(); + expect(AppStateMock.addEventListener).toBeCalledTimes(1); + expect(syncManagerMockWithPushManager.flush).toBeCalledTimes(0); + expect(syncManagerMockWithPushManager.pushManager.stop).toBeCalledTimes(1); + expect(syncManagerMockWithPushManager.pushManager.start).toBeCalledTimes(0); + }); +}); diff --git a/src/settings/defaults.ts b/src/settings/defaults.ts index 7086314..ad309be 100644 --- a/src/settings/defaults.ts +++ b/src/settings/defaults.ts @@ -1,4 +1,4 @@ -const packageVersion = '0.0.1-beta.4'; +const packageVersion = '0.0.1-beta.5'; export const defaults = { startup: { From 656789786430c2fe80c5c3a766c04d41948aa76f Mon Sep 17 00:00:00 2001 From: Emiliano Sanchez Date: Tue, 20 Jul 2021 18:26:32 -0300 Subject: [PATCH 5/5] moved AppState mock to reuse --- package.json | 3 +- src/platform/EventSource/EventSource.ts | 1 + src/platform/__tests__/AppState.mock.ts | 28 +++++++++++++++++++ .../__tests__/RNSignalListener.spec.ts | 27 ++---------------- src/platform/getEventSource.ts | 8 ++++-- 5 files changed, 39 insertions(+), 28 deletions(-) create mode 100644 src/platform/__tests__/AppState.mock.ts diff --git a/package.json b/package.json index 32ae154..3f7a41e 100644 --- a/package.json +++ b/package.json @@ -21,7 +21,7 @@ "!**/__mocks__" ], "scripts": { - "test": "jest src", + "test": "jest", "check": "npm run check:lint && npm run check:types && npm run check:version", "check:lint": "eslint \"**/*.{js,ts,tsx}\"", "check:types": "tsc --noEmit", @@ -79,6 +79,7 @@ }, "jest": { "preset": "react-native", + "testMatch": ["/src/**/__tests__/**/?(*.)+(spec|test).[jt]s"], "modulePathIgnorePatterns": [ "/lib/" ], diff --git a/src/platform/EventSource/EventSource.ts b/src/platform/EventSource/EventSource.ts index 930d7fe..0ba13c5 100644 --- a/src/platform/EventSource/EventSource.ts +++ b/src/platform/EventSource/EventSource.ts @@ -54,6 +54,7 @@ class EventSource extends EventSourceBase { } _closeEventSource(id) { + this.readyState = this.CLOSED; RNEventSource.close(id); } diff --git a/src/platform/__tests__/AppState.mock.ts b/src/platform/__tests__/AppState.mock.ts new file mode 100644 index 0000000..22fb472 --- /dev/null +++ b/src/platform/__tests__/AppState.mock.ts @@ -0,0 +1,28 @@ +import { AppStateStatus } from 'react-native'; + +export const AppStateMock = { + _changeListeners: new Set<(state: AppStateStatus) => void>(), + _emitChangeEvent(event: AppStateStatus) { + AppStateMock._changeListeners.forEach((listener) => { + listener(event); + }); + }, + mockClear() { + AppStateMock._changeListeners.clear(); + AppStateMock.addEventListener.mockClear(); + AppStateMock.removeEventListener.mockClear(); + }, + + // AppState API: + currentState: 'active', + addEventListener: jest.fn((type, listener) => { + if (type === 'change') { + AppStateMock._changeListeners.add(listener); + } + }), + removeEventListener: jest.fn((type, listener) => { + if (type === 'change') { + AppStateMock._changeListeners.delete(listener); + } + }), +}; diff --git a/src/platform/__tests__/RNSignalListener.spec.ts b/src/platform/__tests__/RNSignalListener.spec.ts index 3430dcf..0cb7da0 100644 --- a/src/platform/__tests__/RNSignalListener.spec.ts +++ b/src/platform/__tests__/RNSignalListener.spec.ts @@ -1,26 +1,5 @@ -import { AppStateStatus } from 'react-native'; import { RNSignalListener } from '../RNSignalListener'; - -let changeListeners = new Set<(state: AppStateStatus) => void>(); - -const AppStateMock = { - currentState: 'active', - addEventListener: jest.fn((type, listener) => { - if (type === 'change') { - changeListeners.add(listener); - } - }), - removeEventListener: jest.fn((type, listener) => { - if (type === 'change') { - changeListeners.delete(listener); - } - }), - _emitChangeEvent(event: AppStateStatus) { - changeListeners.forEach((listener) => { - listener(event); - }); - }, -}; +import { AppStateMock } from './AppState.mock'; jest.doMock('react-native/Libraries/AppState/AppState', () => AppStateMock); @@ -35,9 +14,7 @@ const settingsMock = { describe('RNSignalListener', () => { beforeEach(() => { - changeListeners.clear(); - AppStateMock.addEventListener.mockClear(); - AppStateMock.removeEventListener.mockClear(); + AppStateMock.mockClear(); syncManagerMockWithPushManager.flush.mockClear(); syncManagerMockWithPushManager.pushManager.stop.mockClear(); syncManagerMockWithPushManager.pushManager.start.mockClear(); diff --git a/src/platform/getEventSource.ts b/src/platform/getEventSource.ts index 3a2503f..1c4b6ed 100644 --- a/src/platform/getEventSource.ts +++ b/src/platform/getEventSource.ts @@ -1,8 +1,12 @@ /* eslint-disable no-undef */ -import { NativeModules } from 'react-native'; +import reactNative from 'react-native'; let _RNEventSource: typeof EventSource | undefined; -if (NativeModules.RNEventSource) _RNEventSource = require('./EventSource/EventSource'); + +// Try-catch to avoid Jest error `Invariant Violation: __fbBatchedBridgeConfig is not set, cannot invoke native modules` +try { + if (reactNative.NativeModules && reactNative.NativeModules.RNEventSource) _RNEventSource = require('./EventSource/EventSource'); +} catch (e) {} /** * Returns native implementation of EventSource. If not available (e.g., Expo or other runtime than Android and iOS),