diff --git a/.codeclimate.yml b/.codeclimate.yml index 963da3bb6..cbbbbab9b 100644 --- a/.codeclimate.yml +++ b/.codeclimate.yml @@ -20,3 +20,20 @@ exclude_patterns: - "**/*.yml" - "**/build.gradle" - "**/AndroidManifest.xml" +plugins: + eslint: + enabled: true + editorconfig: + enabled: true + fixme: + enabled: true + git-legal: + enabled: true + shellcheck: + enabled: true + swiftlint: + enabled: true + tailor: + enabled: true + tslint: + enabled: true \ No newline at end of file diff --git a/.github/workflows/test-and-publish.yml b/.github/workflows/test-and-publish.yml deleted file mode 100644 index 1b7d0d102..000000000 --- a/.github/workflows/test-and-publish.yml +++ /dev/null @@ -1,27 +0,0 @@ -name: Test & Publish code coverage - -on: - push: - branches: - - "master" - pull_request: - branches: - - "**" - -jobs: - build: - runs-on: ubuntu-latest - - steps: - - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 - - - name: Install dependencies - run: yarn install - - - name: Test & publish code coverage - uses: paambaati/codeclimate-action@60d1b18d039c7b06c721984a5c3d98b724baf991 # v2.6.0 - env: - CC_TEST_REPORTER_ID: ${{ secrets.CC_TEST_REPORTER_ID }} - with: - coverageCommand: yarn coverage - debug: true diff --git a/.github/workflows/unit-test.yml b/.github/workflows/unit-test.yml new file mode 100644 index 000000000..0eb7af4c4 --- /dev/null +++ b/.github/workflows/unit-test.yml @@ -0,0 +1,53 @@ +name: Unit Tests + +on: + push: + branches: + - "master" + pull_request: + branches: + - "**" + +jobs: + test-comment: + name: Unit tests + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v3 + + - name: Setup + uses: ./.github/actions/setup + + - name: Run tests + run: | + yarn test --coverage --coverageReporters json-summary + + - name: Test coverage comment + id: coverageComment + uses: MishaKav/jest-coverage-comment@main + with: + hide-comment: false + coverage-summary-path: ./coverage/coverage-summary.json + test-code-climate: + name: Upload unit test results to Code Climate + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v3 + + - name: Setup + uses: ./.github/actions/setup + + - name: Run tests + run: | + yarn test --coverage --coverageReporters lcov + + - name: Upload coverage to Code Climate + uses: paambaati/codeclimate-action@v9.0.0 + env: + CC_TEST_REPORTER_ID: ${{ secrets.CC_TEST_REPORTER_ID }} + with: + coverageCommand: yarn test --coverage --coverageReporters lcov + coverageLocations: | + ${{github.workspace}}/coverage/lcov.info:lcov \ No newline at end of file diff --git a/.gitignore b/.gitignore index c6ed36a35..efd5c4e59 100644 --- a/.gitignore +++ b/.gitignore @@ -83,4 +83,5 @@ android/generated # Iterable .env -.xcode.env.local \ No newline at end of file +.xcode.env.local +coverage/ \ No newline at end of file diff --git a/jest.config.js b/jest.config.js index 092ec62b5..7fae0017b 100644 --- a/jest.config.js +++ b/jest.config.js @@ -1,6 +1,28 @@ module.exports = { preset: 'react-native', - setupFiles: ['/ts/__mocks__/jest.setup.ts'], - moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx', 'json', 'node'], - testMatch: ['/ts/__tests__/**/*.(test|spec).[jt]s?(x)'], + setupFiles: ['/src/__mocks__/jest.setup.ts'], + setupFilesAfterEnv: [ + '/node_modules/@testing-library/jest-native/extend-expect', + ], + testMatch: ['/src/__tests__/**/*.(test|spec).[jt]s?(x)'], + transformIgnorePatterns: [ + 'node_modules/(?!((jest-)?react-native(-.*)?|@react-native(-community)?)/)', + ], + collectCoverageFrom: [ + 'src/**/*.{cjs,js,jsx,mjs,ts,tsx}', + '!src/**/*.test.{cjs,js,jsx,mjs,mdx,ts,tsx}', + '!src/(__tests__|__mocks__)/*', + ], + modulePathIgnorePatterns: [ + '/example/node_modules', + '/lib/', + ], + coverageThreshold: { + global: { + branches: 10, + functions: 30, + lines: 30, + statements: 30, + }, + }, }; diff --git a/package.json b/package.json index 793d43f41..9646f269b 100644 --- a/package.json +++ b/package.json @@ -38,6 +38,7 @@ "scripts": { "example": "yarn workspace @iterable/react-native-sdk-example", "test": "jest", + "test:coverage": "jest --coverage", "typecheck": "tsc", "lint": "eslint \"**/*.{js,ts,tsx}\"", "clean": "del-cli android/build example/android/build example/android/app/build example/ios/build lib", @@ -69,6 +70,8 @@ "@react-native/eslint-config": "^0.73.1", "@react-navigation/native": "^6.1.18", "@release-it/conventional-changelog": "^5.0.0", + "@testing-library/jest-native": "^5.4.3", + "@testing-library/react-native": "^12.7.2", "@types/jest": "^29.5.5", "@types/react": "^18.2.44", "@types/react-native-vector-icons": "^6.4.18", @@ -104,13 +107,6 @@ "example" ], "packageManager": "yarn@3.6.1", - "jest": { - "preset": "react-native", - "modulePathIgnorePatterns": [ - "/example/node_modules", - "/lib/" - ] - }, "commitlint": { "extends": [ "@commitlint/config-conventional" diff --git a/src/__mocks__/jest.setup.ts b/src/__mocks__/jest.setup.ts index 5be4d351c..a4e6c8264 100644 --- a/src/__mocks__/jest.setup.ts +++ b/src/__mocks__/jest.setup.ts @@ -1,9 +1,17 @@ +import * as ReactNative from 'react-native'; + import { MockRNIterableAPI } from './MockRNIterableAPI'; import { MockLinking } from './MockLinking'; -import * as ReactNative from 'react-native'; jest.mock('react-native/Libraries/EventEmitter/NativeEventEmitter.js'); +jest.mock('react-native-webview', () => { + const { View } = require('react-native'); + return { + WebView: View, + }; +}); + jest.doMock('react-native', () => { // Extend ReactNative return Object.setPrototypeOf( diff --git a/src/__tests__/Iterable.spec.ts b/src/__tests__/Iterable.spec.ts deleted file mode 100644 index 3eafe2d07..000000000 --- a/src/__tests__/Iterable.spec.ts +++ /dev/null @@ -1,529 +0,0 @@ -import { NativeEventEmitter } from 'react-native'; - -import { MockRNIterableAPI } from '../__mocks__/MockRNIterableAPI'; -import { MockLinking } from '../__mocks__/MockLinking'; -import { TestHelper } from './TestHelper'; - -// import from the same location that consumers import from -import { Iterable, IterableConfig, IterableLogLevel } from '../index'; -import { - IterableAttributionInfo, - IterableCommerceItem, - IterableActionContext, - EventName, - IterableAction, - IterableActionSource, -} from '../Iterable'; -import { IterableLogger } from '../IterableLogger'; -import { IterableDataRegion } from '../IterableDataRegion'; - -beforeEach(() => { - jest.clearAllMocks(); - Iterable.logger = new IterableLogger(new IterableConfig()); -}); - -it('setEmail_getEmail_email_returnsEmail', async () => { - Iterable.logger.log('setEmail_getEmail_email_returnsEmail'); - const result = 'user@example.com'; - - // GIVEN an email - const email = 'user@example.com'; - - // WHEN Iterable.setEmail is called with the given email - Iterable.setEmail(email); - - // THEN Iterable.getEmail returns the given email - return await Iterable.getEmail().then((mail) => { - expect(mail).toBe(result); - }); -}); - -test('setUserId_getUserId_userId_returnsUserId', async () => { - Iterable.logger.log('setUserId_getUserId_userId_returnsUserId'); - const result = 'user1'; - - // GIVEN an userId - const userId = 'user1'; - - // WHEN Iterable.setUserId is called with the given userId - Iterable.setUserId(userId); - - // THEN Iterable.getUserId returns the given userId - return await Iterable.getUserId().then((id) => { - expect(id).toBe(result); - }); -}); - -test('disableDeviceForCurrentUser_noParams_methodCalled', () => { - Iterable.logger.log('disableDeviceForCurrentUser_noParams_methodCalled'); - - // GIVEN no parameters - - // WHEN Iterable.disableDeviceForCurrentUser is called - Iterable.disableDeviceForCurrentUser(); - - // THEN corresponding method is called on RNITerableAPI - expect(MockRNIterableAPI.disableDeviceForCurrentUser).toBeCalled(); -}); - -test('getLastPushPayload_noParams_returnLastPushPayload', async () => { - Iterable.logger.log('getLastPushPayload_noParams_returnLastPushPayload'); - const result = { var1: 'val1', var2: true }; - - // GIVEN no parameters - - // WHEN the lastPushPayload is set - MockRNIterableAPI.lastPushPayload = { var1: 'val1', var2: true }; - - // THEN the lastPushPayload is returned when getLastPushPayload is called - return await Iterable.getLastPushPayload().then((payload) => { - expect(payload).toEqual(result); - }); -}); - -test('trackPushOpenWithCampaignId_pushParams_methodCalled', () => { - Iterable.logger.log('getLastPushPayload_noParams_returnLastPushPayload'); - - // GIVEN the following parameters - const campaignId = 123; - const templateId = 234; - const messageId = 'someMessageId'; - const appAlreadyRunning = false; - const dataFields = { dataFieldKey: 'dataFieldValue' }; - - // WHEN Iterable.trackPushOpenWithCampaignId is called - Iterable.trackPushOpenWithCampaignId( - campaignId, - templateId, - messageId, - appAlreadyRunning, - dataFields - ); - - // THEN corresponding function is called on RNIterableAPI - expect(MockRNIterableAPI.trackPushOpenWithCampaignId).toBeCalledWith( - campaignId, - templateId, - messageId, - appAlreadyRunning, - dataFields - ); -}); - -test('updateCart_items_methodCalled', () => { - Iterable.logger.log('updateCart_items_methodCalled'); - - // GIVEN list of items - const items = [new IterableCommerceItem('id1', 'Boba Tea', 18, 26)]; - - // WHEN Iterable.updateCart is called - Iterable.updateCart(items); - - // THEN corresponding function is called on RNIterableAPI - expect(MockRNIterableAPI.updateCart).toBeCalledWith(items); -}); - -test('trackPurchase_params_methodCalled', () => { - Iterable.logger.log('trackPurchase_params_methodCalled'); - - // GIVEN the following parameters - const total = 10; - const items = [new IterableCommerceItem('id1', 'Boba Tea', 18, 26)]; - const dataFields = { dataFieldKey: 'dataFieldValue' }; - - // WHEN Iterable.trackPurchase is called - Iterable.trackPurchase(total, items, dataFields); - - // THEN corresponding function is called on RNIterableAPI - expect(MockRNIterableAPI.trackPurchase).toBeCalledWith( - total, - items, - dataFields - ); -}); - -test('trackPurchase_paramsWithOptionalFields_methodCalled', () => { - Iterable.logger.log('trackPurchase_paramsWithOptionalFields_methodCalled'); - - // GIVEN the following parameters - const total = 5; - const items = [ - new IterableCommerceItem( - 'id', - 'swordfish', - 64, - 1, - 'SKU', - 'description', - 'url', - 'imageUrl', - ['sword', 'shield'] - ), - ]; - const dataFields = { key: 'value' }; - - // WHEN Iterable.trackPurchase is called - Iterable.trackPurchase(total, items, dataFields); - - // THEN corresponding function is called on RNIterableAPI - expect(MockRNIterableAPI.trackPurchase).toBeCalledWith( - total, - items, - dataFields - ); -}); - -test('trackEvent_params_methodCalled', () => { - Iterable.logger.log('trackPurchase_paramsWithOptionalFields_methodCalled'); - - // GIVEN the following parameters - const name = 'EventName'; - const dataFields = { DatafieldKey: 'DatafieldValue' }; - - // WHEN Iterable.trackEvent is called - Iterable.trackEvent(name, dataFields); - - // THEN corresponding function is called on RNIterableAPI - expect(MockRNIterableAPI.trackEvent).toBeCalledWith(name, dataFields); -}); - -test('setAttributionInfo_getAttributionInfo_attributionInfo_returnsAttributionInfo', async () => { - Iterable.logger.log( - 'setAttributionInfo_getAttributionInfo_attributionInfo_returnsAttributionInfo' - ); - - // GIVEN attribution info - const campaignId = 1234; - const templateId = 5678; - const messageId = 'qwer'; - - // WHEN Iterable.setAttributionInfo is called with the given attribution info - Iterable.setAttributionInfo( - new IterableAttributionInfo(campaignId, templateId, messageId) - ); - - // THEN Iterable.getAttrbutionInfo returns the given attribution info - return await Iterable.getAttributionInfo().then((attributionInfo) => { - expect(attributionInfo?.campaignId).toBe(campaignId); - expect(attributionInfo?.templateId).toBe(templateId); - expect(attributionInfo?.messageId).toBe(messageId); - }); -}); - -test('updateUser_params_methodCalled', () => { - Iterable.logger.log('updateUser_params_methodCalled'); - - // GIVEN the following parameters - const dataFields = { field: 'value1' }; - - // WHEN Iterable.updateUser is called - Iterable.updateUser(dataFields, false); - - // THEN corresponding function is called on RNIterableAPI - expect(MockRNIterableAPI.updateUser).toBeCalledWith(dataFields, false); -}); - -test('updateEmail_email_methodCalled', () => { - Iterable.logger.log('updateEmail_email_methodCalled'); - - // GIVEN the new email - const newEmail = 'woo@newemail.com'; - - // WHEN Iterable.updateEmail is called - Iterable.updateEmail(newEmail); - - // THEN corresponding function is called on RNIterableAPI - expect(MockRNIterableAPI.updateEmail).toBeCalledWith(newEmail, undefined); -}); - -test('updateEmail_emailAndToken_methodCalled', () => { - Iterable.logger.log('updateEmail_emailAndToken_methodCalled'); - - // GIVEN the new email and a token - const newEmail = 'woo@newemail.com'; - const newToken = 'token2'; - - // WHEN Iterable.updateEmail is called - Iterable.updateEmail(newEmail, newToken); - - // THEN corresponding function is called on RNITerableAPI - expect(MockRNIterableAPI.updateEmail).toBeCalledWith(newEmail, newToken); -}); - -test('iterableConfig_noParams_defaultValues', () => { - Iterable.logger.log('iterableConfig_noParams_defaultValues'); - - // GIVEN no parameters - // WHEN config is initialized - const config = new IterableConfig(); - - // THEN config has default values - expect(config.pushIntegrationName).toBe(undefined); - expect(config.autoPushRegistration).toBe(true); - expect(config.checkForDeferredDeeplink).toBe(false); - expect(config.inAppDisplayInterval).toBe(30.0); - expect(config.urlHandler).toBe(undefined); - expect(config.customActionHandler).toBe(undefined); - expect(config.inAppHandler).toBe(undefined); - expect(config.authHandler).toBe(undefined); - expect(config.logLevel).toBe(IterableLogLevel.info); - expect(config.logReactNativeSdkCalls).toBe(true); - expect(config.expiringAuthTokenRefreshPeriod).toBe(60.0); - expect(config.allowedProtocols).toEqual([]); - expect(config.androidSdkUseInMemoryStorageForInApps).toBe(false); - expect(config.useInMemoryStorageForInApps).toBe(false); - expect(config.dataRegion).toBe(IterableDataRegion.US); - expect(config.encryptionEnforced).toBe(false); -}); - -test('iterableConfig_noParams_defaultDictValues', () => { - Iterable.logger.log('iterableConfig_noParams_defaultDictValues'); - - // GIVEN no parameters - // WHEN config is initialized and converted to a dictionary - const configDict = new IterableConfig().toDict(); - - // THEN config has default dictionary values - expect(configDict.pushIntegrationName).toBe(undefined); - expect(configDict.autoPushRegistration).toBe(true); - expect(configDict.inAppDisplayInterval).toBe(30.0); - expect(configDict.urlHandlerPresent).toBe(false); - expect(configDict.customActionHandlerPresent).toBe(false); - expect(configDict.inAppHandlerPresent).toBe(false); - expect(configDict.authHandlerPresent).toBe(false); - expect(configDict.logLevel).toBe(IterableLogLevel.info); - expect(configDict.expiringAuthTokenRefreshPeriod).toBe(60.0); - expect(configDict.allowedProtocols).toEqual([]); - expect(configDict.androidSdkUseInMemoryStorageForInApps).toBe(false); - expect(configDict.useInMemoryStorageForInApps).toBe(false); - expect(configDict.dataRegion).toBe(IterableDataRegion.US); - expect(configDict.encryptionEnforced).toBe(false); -}); - -test('urlHandler_canOpenUrlSetToTrueAndUrlHandlerReturnsFalse_openUrlCalled', async () => { - Iterable.logger.log( - 'urlHandler_canOpenUrlSetToTrueAndUrlHandlerReturnsFalse_openUrlCalled' - ); - - // sets up event emitter - const nativeEmitter = new NativeEventEmitter(); - nativeEmitter.removeAllListeners(EventName.handleUrlCalled); - - // sets up config file and urlHandler function - // urlHandler set to return false - const config = new IterableConfig(); - config.urlHandler = jest.fn((_url: string, _: IterableActionContext) => { - return false; - }); - - // initialize Iterable object - - Iterable.initialize('apiKey', config); - - // GIVEN canOpenUrl set to return a promise that resolves to true - MockLinking.canOpenURL = jest.fn(async () => { - return await new Promise((resolve) => { - resolve(true); - }); - }); - MockLinking.openURL.mockReset(); - const expectedUrl = 'https://somewhere.com'; - - const actionDict = { type: 'openUrl' }; - const dict = { - url: expectedUrl, - context: { action: actionDict, source: 'inApp' }, - }; - - // WHEN handleUrlCalled event is emitted - nativeEmitter.emit(EventName.handleUrlCalled, dict); - - // THEN urlHandler and MockLinking is called with expected url - return await TestHelper.delayed(0, () => { - expect(config.urlHandler).toBeCalledWith(expectedUrl, dict.context); - expect(MockLinking.openURL).toBeCalledWith(expectedUrl); - }); -}); - -test('urlHandler_canOpenUrlSetToFalseAndUrlHandlerReturnsFalse_openUrlNotCalled', async () => { - Iterable.logger.log( - 'urlHandler_canOpenUrlSetToFalseAndUrlHandlerReturnsFalse_openUrlNotCalled' - ); - - // sets up event emitter - const nativeEmitter = new NativeEventEmitter(); - nativeEmitter.removeAllListeners(EventName.handleUrlCalled); - - // sets up config file and urlHandler function - // urlHandler set to return false - const config = new IterableConfig(); - config.urlHandler = jest.fn((_url: string, _: IterableActionContext) => { - return false; - }); - - // initialize Iterable object - - Iterable.initialize('apiKey', config); - - // GIVEN canOpenUrl set to return a promise that resolves to false - MockLinking.canOpenURL = jest.fn(async () => { - return await new Promise((resolve) => { - resolve(false); - }); - }); - MockLinking.openURL.mockReset(); - const expectedUrl = 'https://somewhere.com'; - - const actionDict = { type: 'openUrl' }; - const dict = { - url: expectedUrl, - context: { action: actionDict, source: 'inApp' }, - }; - - // WHEN handleUrlCalled event is emitted - nativeEmitter.emit(EventName.handleUrlCalled, dict); - - // THEN urlHandler is called and MockLinking.openURL is not called - return await TestHelper.delayed(0, () => { - expect(config.urlHandler).toBeCalledWith(expectedUrl, dict.context); - expect(MockLinking.openURL).not.toBeCalled(); - }); -}); - -test('urlHandler_canOpenUrlSetToTrueAndUrlHandlerReturnsTrue_openUrlNotCalled', async () => { - Iterable.logger.log( - 'urlHandler_canOpenUrlSetToTrueAndUrlHandlerReturnsTrue_openUrlNotCalled' - ); - - // sets up event emitter - const nativeEmitter = new NativeEventEmitter(); - nativeEmitter.removeAllListeners(EventName.handleUrlCalled); - - // sets up config file and urlHandler function - // urlHandler set to return true - const config = new IterableConfig(); - config.urlHandler = jest.fn((_url: string, _: IterableActionContext) => { - return true; - }); - - // initialize Iterable object - - Iterable.initialize('apiKey', config); - - // GIVEN canOpenUrl set to return a promise that resolves to true - MockLinking.canOpenURL = jest.fn(async () => { - return await new Promise((resolve) => { - resolve(true); - }); - }); - MockLinking.openURL.mockReset(); - const expectedUrl = 'https://somewhere.com'; - - const actionDict = { type: 'openUrl' }; - const dict = { - url: expectedUrl, - context: { action: actionDict, source: 'inApp' }, - }; - - // WHEN handleUrlCalled event is emitted - nativeEmitter.emit(EventName.handleUrlCalled, dict); - - // THEN urlHandler is called and MockLinking.openURL is not called - return await TestHelper.delayed(0, () => { - expect(config.urlHandler).toBeCalledWith(expectedUrl, dict.context); - expect(MockLinking.openURL).not.toBeCalled(); - }); -}); - -test('customActionHandler_actionNameAndActionData_customActionHandlerCalled', () => { - Iterable.logger.log( - 'customActionHandler_actionNameAndActionData_customActionHandlerCalled' - ); - - // sets up event emitter - const nativeEmitter = new NativeEventEmitter(); - nativeEmitter.removeAllListeners(EventName.handleCustomActionCalled); - - // sets up config file and customActionHandler function - // customActionHandler set to return true - const config = new IterableConfig(); - config.customActionHandler = jest.fn( - (_action: IterableAction, _context: IterableActionContext) => { - return true; - } - ); - - // initialize Iterable object - - Iterable.initialize('apiKey', config); - - // GIVEN custom action name and custom action data - const actionName = 'zeeActionName'; - const actionData = 'zeeActionData'; - const actionDict = { type: actionName, data: actionData }; - const actionSource = IterableActionSource.inApp; - const dict = { - action: actionDict, - context: { action: actionDict, source: IterableActionSource.inApp }, - }; - - // WHEN handleCustomActionCalled event is emitted - nativeEmitter.emit(EventName.handleCustomActionCalled, dict); - - // THEN customActionHandler is called with expected action and expected context - const expectedAction = new IterableAction(actionName, actionData); - const expectedContext = new IterableActionContext( - expectedAction, - actionSource - ); - expect(config.customActionHandler).toBeCalledWith( - expectedAction, - expectedContext - ); -}); - -test('handleAppLink_link_methodCalled', () => { - Iterable.logger.log('handleAppLink_link_methodCalled'); - - // GIVEN a link - const link = 'https://somewhere.com/link/something'; - - // WHEN Iterable.handleAppLink is called - - Iterable.handleAppLink(link); - - // THEN corresponding function is called on RNITerableAPI - expect(MockRNIterableAPI.handleAppLink).toBeCalledWith(link); -}); - -test('updateSubscriptions_params_methodCalled', () => { - Iterable.logger.log('update subscriptions is called'); - - // GIVEN the following parameters - const emailListIds = [1, 2, 3]; - const unsubscribedChannelIds = [4, 5, 6]; - const unsubscribedMessageTypeIds = [7, 8]; - const subscribedMessageTypeIds = [9]; - const campaignId = 10; - const templateId = 11; - - // WHEN Iterable.updateSubscriptions is called - Iterable.updateSubscriptions( - emailListIds, - unsubscribedChannelIds, - unsubscribedMessageTypeIds, - subscribedMessageTypeIds, - campaignId, - templateId - ); - - // THEN corresponding function is called on RNIterableAPI - expect(MockRNIterableAPI.updateSubscriptions).toBeCalledWith( - emailListIds, - unsubscribedChannelIds, - unsubscribedMessageTypeIds, - subscribedMessageTypeIds, - campaignId, - templateId - ); -}); diff --git a/src/__tests__/Iterable.test.ts b/src/__tests__/Iterable.test.ts new file mode 100644 index 000000000..bfc0c52f8 --- /dev/null +++ b/src/__tests__/Iterable.test.ts @@ -0,0 +1,430 @@ +import { NativeEventEmitter } from 'react-native'; + +import { MockRNIterableAPI } from '../__mocks__/MockRNIterableAPI'; +import { MockLinking } from '../__mocks__/MockLinking'; +import { TestHelper } from './TestHelper'; + +// import from the same location that consumers import from +import { Iterable, IterableConfig, IterableLogLevel } from '../index'; +import { + IterableAttributionInfo, + IterableCommerceItem, + IterableActionContext, + EventName, + IterableAction, + IterableActionSource, +} from '../Iterable'; +import { IterableLogger } from '../IterableLogger'; +import { IterableDataRegion } from '../IterableDataRegion'; + +describe('Iterable', () => { + beforeEach(() => { + jest.clearAllMocks(); + Iterable.logger = new IterableLogger(new IterableConfig()); + }); + + it('setEmail_getEmail_email_returnsEmail', async () => { + Iterable.logger.log('setEmail_getEmail_email_returnsEmail'); + const result = 'user@example.com'; + // GIVEN an email + const email = 'user@example.com'; + // WHEN Iterable.setEmail is called with the given email + Iterable.setEmail(email); + // THEN Iterable.getEmail returns the given email + return await Iterable.getEmail().then((mail) => { + expect(mail).toBe(result); + }); + }); + test('setUserId_getUserId_userId_returnsUserId', async () => { + Iterable.logger.log('setUserId_getUserId_userId_returnsUserId'); + const result = 'user1'; + // GIVEN an userId + const userId = 'user1'; + // WHEN Iterable.setUserId is called with the given userId + Iterable.setUserId(userId); + // THEN Iterable.getUserId returns the given userId + return await Iterable.getUserId().then((id) => { + expect(id).toBe(result); + }); + }); + test('disableDeviceForCurrentUser_noParams_methodCalled', () => { + Iterable.logger.log('disableDeviceForCurrentUser_noParams_methodCalled'); + // GIVEN no parameters + // WHEN Iterable.disableDeviceForCurrentUser is called + Iterable.disableDeviceForCurrentUser(); + // THEN corresponding method is called on RNITerableAPI + expect(MockRNIterableAPI.disableDeviceForCurrentUser).toBeCalled(); + }); + test('getLastPushPayload_noParams_returnLastPushPayload', async () => { + Iterable.logger.log('getLastPushPayload_noParams_returnLastPushPayload'); + const result = { var1: 'val1', var2: true }; + // GIVEN no parameters + // WHEN the lastPushPayload is set + MockRNIterableAPI.lastPushPayload = { var1: 'val1', var2: true }; + // THEN the lastPushPayload is returned when getLastPushPayload is called + return await Iterable.getLastPushPayload().then((payload) => { + expect(payload).toEqual(result); + }); + }); + test('trackPushOpenWithCampaignId_pushParams_methodCalled', () => { + Iterable.logger.log('getLastPushPayload_noParams_returnLastPushPayload'); + // GIVEN the following parameters + const campaignId = 123; + const templateId = 234; + const messageId = 'someMessageId'; + const appAlreadyRunning = false; + const dataFields = { dataFieldKey: 'dataFieldValue' }; + // WHEN Iterable.trackPushOpenWithCampaignId is called + Iterable.trackPushOpenWithCampaignId( + campaignId, + templateId, + messageId, + appAlreadyRunning, + dataFields + ); + // THEN corresponding function is called on RNIterableAPI + expect(MockRNIterableAPI.trackPushOpenWithCampaignId).toBeCalledWith( + campaignId, + templateId, + messageId, + appAlreadyRunning, + dataFields + ); + }); + test('updateCart_items_methodCalled', () => { + Iterable.logger.log('updateCart_items_methodCalled'); + // GIVEN list of items + const items = [new IterableCommerceItem('id1', 'Boba Tea', 18, 26)]; + // WHEN Iterable.updateCart is called + Iterable.updateCart(items); + // THEN corresponding function is called on RNIterableAPI + expect(MockRNIterableAPI.updateCart).toBeCalledWith(items); + }); + test('trackPurchase_params_methodCalled', () => { + Iterable.logger.log('trackPurchase_params_methodCalled'); + // GIVEN the following parameters + const total = 10; + const items = [new IterableCommerceItem('id1', 'Boba Tea', 18, 26)]; + const dataFields = { dataFieldKey: 'dataFieldValue' }; + // WHEN Iterable.trackPurchase is called + Iterable.trackPurchase(total, items, dataFields); + // THEN corresponding function is called on RNIterableAPI + expect(MockRNIterableAPI.trackPurchase).toBeCalledWith( + total, + items, + dataFields + ); + }); + test('trackPurchase_paramsWithOptionalFields_methodCalled', () => { + Iterable.logger.log('trackPurchase_paramsWithOptionalFields_methodCalled'); + // GIVEN the following parameters + const total = 5; + const items = [ + new IterableCommerceItem( + 'id', + 'swordfish', + 64, + 1, + 'SKU', + 'description', + 'url', + 'imageUrl', + ['sword', 'shield'] + ), + ]; + const dataFields = { key: 'value' }; + // WHEN Iterable.trackPurchase is called + Iterable.trackPurchase(total, items, dataFields); + // THEN corresponding function is called on RNIterableAPI + expect(MockRNIterableAPI.trackPurchase).toBeCalledWith( + total, + items, + dataFields + ); + }); + test('trackEvent_params_methodCalled', () => { + Iterable.logger.log('trackPurchase_paramsWithOptionalFields_methodCalled'); + // GIVEN the following parameters + const name = 'EventName'; + const dataFields = { DatafieldKey: 'DatafieldValue' }; + // WHEN Iterable.trackEvent is called + Iterable.trackEvent(name, dataFields); + // THEN corresponding function is called on RNIterableAPI + expect(MockRNIterableAPI.trackEvent).toBeCalledWith(name, dataFields); + }); + test('setAttributionInfo_getAttributionInfo_attributionInfo_returnsAttributionInfo', async () => { + Iterable.logger.log( + 'setAttributionInfo_getAttributionInfo_attributionInfo_returnsAttributionInfo' + ); + // GIVEN attribution info + const campaignId = 1234; + const templateId = 5678; + const messageId = 'qwer'; + // WHEN Iterable.setAttributionInfo is called with the given attribution info + Iterable.setAttributionInfo( + new IterableAttributionInfo(campaignId, templateId, messageId) + ); + // THEN Iterable.getAttrbutionInfo returns the given attribution info + return await Iterable.getAttributionInfo().then((attributionInfo) => { + expect(attributionInfo?.campaignId).toBe(campaignId); + expect(attributionInfo?.templateId).toBe(templateId); + expect(attributionInfo?.messageId).toBe(messageId); + }); + }); + test('updateUser_params_methodCalled', () => { + Iterable.logger.log('updateUser_params_methodCalled'); + // GIVEN the following parameters + const dataFields = { field: 'value1' }; + // WHEN Iterable.updateUser is called + Iterable.updateUser(dataFields, false); + // THEN corresponding function is called on RNIterableAPI + expect(MockRNIterableAPI.updateUser).toBeCalledWith(dataFields, false); + }); + test('updateEmail_email_methodCalled', () => { + Iterable.logger.log('updateEmail_email_methodCalled'); + // GIVEN the new email + const newEmail = 'woo@newemail.com'; + // WHEN Iterable.updateEmail is called + Iterable.updateEmail(newEmail); + // THEN corresponding function is called on RNIterableAPI + expect(MockRNIterableAPI.updateEmail).toBeCalledWith(newEmail, undefined); + }); + test('updateEmail_emailAndToken_methodCalled', () => { + Iterable.logger.log('updateEmail_emailAndToken_methodCalled'); + // GIVEN the new email and a token + const newEmail = 'woo@newemail.com'; + const newToken = 'token2'; + // WHEN Iterable.updateEmail is called + Iterable.updateEmail(newEmail, newToken); + // THEN corresponding function is called on RNITerableAPI + expect(MockRNIterableAPI.updateEmail).toBeCalledWith(newEmail, newToken); + }); + test('iterableConfig_noParams_defaultValues', () => { + Iterable.logger.log('iterableConfig_noParams_defaultValues'); + // GIVEN no parameters + // WHEN config is initialized + const config = new IterableConfig(); + // THEN config has default values + expect(config.pushIntegrationName).toBe(undefined); + expect(config.autoPushRegistration).toBe(true); + expect(config.checkForDeferredDeeplink).toBe(false); + expect(config.inAppDisplayInterval).toBe(30.0); + expect(config.urlHandler).toBe(undefined); + expect(config.customActionHandler).toBe(undefined); + expect(config.inAppHandler).toBe(undefined); + expect(config.authHandler).toBe(undefined); + expect(config.logLevel).toBe(IterableLogLevel.info); + expect(config.logReactNativeSdkCalls).toBe(true); + expect(config.expiringAuthTokenRefreshPeriod).toBe(60.0); + expect(config.allowedProtocols).toEqual([]); + expect(config.androidSdkUseInMemoryStorageForInApps).toBe(false); + expect(config.useInMemoryStorageForInApps).toBe(false); + expect(config.dataRegion).toBe(IterableDataRegion.US); + expect(config.encryptionEnforced).toBe(false); + }); + test('iterableConfig_noParams_defaultDictValues', () => { + Iterable.logger.log('iterableConfig_noParams_defaultDictValues'); + // GIVEN no parameters + // WHEN config is initialized and converted to a dictionary + const configDict = new IterableConfig().toDict(); + // THEN config has default dictionary values + expect(configDict.pushIntegrationName).toBe(undefined); + expect(configDict.autoPushRegistration).toBe(true); + expect(configDict.inAppDisplayInterval).toBe(30.0); + expect(configDict.urlHandlerPresent).toBe(false); + expect(configDict.customActionHandlerPresent).toBe(false); + expect(configDict.inAppHandlerPresent).toBe(false); + expect(configDict.authHandlerPresent).toBe(false); + expect(configDict.logLevel).toBe(IterableLogLevel.info); + expect(configDict.expiringAuthTokenRefreshPeriod).toBe(60.0); + expect(configDict.allowedProtocols).toEqual([]); + expect(configDict.androidSdkUseInMemoryStorageForInApps).toBe(false); + expect(configDict.useInMemoryStorageForInApps).toBe(false); + expect(configDict.dataRegion).toBe(IterableDataRegion.US); + expect(configDict.encryptionEnforced).toBe(false); + }); + test('urlHandler_canOpenUrlSetToTrueAndUrlHandlerReturnsFalse_openUrlCalled', async () => { + Iterable.logger.log( + 'urlHandler_canOpenUrlSetToTrueAndUrlHandlerReturnsFalse_openUrlCalled' + ); + // sets up event emitter + const nativeEmitter = new NativeEventEmitter(); + nativeEmitter.removeAllListeners(EventName.handleUrlCalled); + // sets up config file and urlHandler function + // urlHandler set to return false + const config = new IterableConfig(); + config.urlHandler = jest.fn((_url: string, _: IterableActionContext) => { + return false; + }); + // initialize Iterable object + Iterable.initialize('apiKey', config); + // GIVEN canOpenUrl set to return a promise that resolves to true + MockLinking.canOpenURL = jest.fn(async () => { + return await new Promise((resolve) => { + resolve(true); + }); + }); + MockLinking.openURL.mockReset(); + const expectedUrl = 'https://somewhere.com'; + const actionDict = { type: 'openUrl' }; + const dict = { + url: expectedUrl, + context: { action: actionDict, source: 'inApp' }, + }; + // WHEN handleUrlCalled event is emitted + nativeEmitter.emit(EventName.handleUrlCalled, dict); + // THEN urlHandler and MockLinking is called with expected url + return await TestHelper.delayed(0, () => { + expect(config.urlHandler).toBeCalledWith(expectedUrl, dict.context); + expect(MockLinking.openURL).toBeCalledWith(expectedUrl); + }); + }); + test('urlHandler_canOpenUrlSetToFalseAndUrlHandlerReturnsFalse_openUrlNotCalled', async () => { + Iterable.logger.log( + 'urlHandler_canOpenUrlSetToFalseAndUrlHandlerReturnsFalse_openUrlNotCalled' + ); + // sets up event emitter + const nativeEmitter = new NativeEventEmitter(); + nativeEmitter.removeAllListeners(EventName.handleUrlCalled); + // sets up config file and urlHandler function + // urlHandler set to return false + const config = new IterableConfig(); + config.urlHandler = jest.fn((_url: string, _: IterableActionContext) => { + return false; + }); + // initialize Iterable object + Iterable.initialize('apiKey', config); + // GIVEN canOpenUrl set to return a promise that resolves to false + MockLinking.canOpenURL = jest.fn(async () => { + return await new Promise((resolve) => { + resolve(false); + }); + }); + MockLinking.openURL.mockReset(); + const expectedUrl = 'https://somewhere.com'; + const actionDict = { type: 'openUrl' }; + const dict = { + url: expectedUrl, + context: { action: actionDict, source: 'inApp' }, + }; + // WHEN handleUrlCalled event is emitted + nativeEmitter.emit(EventName.handleUrlCalled, dict); + // THEN urlHandler is called and MockLinking.openURL is not called + return await TestHelper.delayed(0, () => { + expect(config.urlHandler).toBeCalledWith(expectedUrl, dict.context); + expect(MockLinking.openURL).not.toBeCalled(); + }); + }); + test('urlHandler_canOpenUrlSetToTrueAndUrlHandlerReturnsTrue_openUrlNotCalled', async () => { + Iterable.logger.log( + 'urlHandler_canOpenUrlSetToTrueAndUrlHandlerReturnsTrue_openUrlNotCalled' + ); + // sets up event emitter + const nativeEmitter = new NativeEventEmitter(); + nativeEmitter.removeAllListeners(EventName.handleUrlCalled); + // sets up config file and urlHandler function + // urlHandler set to return true + const config = new IterableConfig(); + config.urlHandler = jest.fn((_url: string, _: IterableActionContext) => { + return true; + }); + // initialize Iterable object + Iterable.initialize('apiKey', config); + // GIVEN canOpenUrl set to return a promise that resolves to true + MockLinking.canOpenURL = jest.fn(async () => { + return await new Promise((resolve) => { + resolve(true); + }); + }); + MockLinking.openURL.mockReset(); + const expectedUrl = 'https://somewhere.com'; + const actionDict = { type: 'openUrl' }; + const dict = { + url: expectedUrl, + context: { action: actionDict, source: 'inApp' }, + }; + // WHEN handleUrlCalled event is emitted + nativeEmitter.emit(EventName.handleUrlCalled, dict); + // THEN urlHandler is called and MockLinking.openURL is not called + return await TestHelper.delayed(0, () => { + expect(config.urlHandler).toBeCalledWith(expectedUrl, dict.context); + expect(MockLinking.openURL).not.toBeCalled(); + }); + }); + test('customActionHandler_actionNameAndActionData_customActionHandlerCalled', () => { + Iterable.logger.log( + 'customActionHandler_actionNameAndActionData_customActionHandlerCalled' + ); + // sets up event emitter + const nativeEmitter = new NativeEventEmitter(); + nativeEmitter.removeAllListeners(EventName.handleCustomActionCalled); + // sets up config file and customActionHandler function + // customActionHandler set to return true + const config = new IterableConfig(); + config.customActionHandler = jest.fn( + (_action: IterableAction, _context: IterableActionContext) => { + return true; + } + ); + // initialize Iterable object + Iterable.initialize('apiKey', config); + // GIVEN custom action name and custom action data + const actionName = 'zeeActionName'; + const actionData = 'zeeActionData'; + const actionDict = { type: actionName, data: actionData }; + const actionSource = IterableActionSource.inApp; + const dict = { + action: actionDict, + context: { action: actionDict, source: IterableActionSource.inApp }, + }; + // WHEN handleCustomActionCalled event is emitted + nativeEmitter.emit(EventName.handleCustomActionCalled, dict); + // THEN customActionHandler is called with expected action and expected context + const expectedAction = new IterableAction(actionName, actionData); + const expectedContext = new IterableActionContext( + expectedAction, + actionSource + ); + expect(config.customActionHandler).toBeCalledWith( + expectedAction, + expectedContext + ); + }); + test('handleAppLink_link_methodCalled', () => { + Iterable.logger.log('handleAppLink_link_methodCalled'); + // GIVEN a link + const link = 'https://somewhere.com/link/something'; + // WHEN Iterable.handleAppLink is called + Iterable.handleAppLink(link); + // THEN corresponding function is called on RNITerableAPI + expect(MockRNIterableAPI.handleAppLink).toBeCalledWith(link); + }); + test('updateSubscriptions_params_methodCalled', () => { + Iterable.logger.log('update subscriptions is called'); + // GIVEN the following parameters + const emailListIds = [1, 2, 3]; + const unsubscribedChannelIds = [4, 5, 6]; + const unsubscribedMessageTypeIds = [7, 8]; + const subscribedMessageTypeIds = [9]; + const campaignId = 10; + const templateId = 11; + // WHEN Iterable.updateSubscriptions is called + Iterable.updateSubscriptions( + emailListIds, + unsubscribedChannelIds, + unsubscribedMessageTypeIds, + subscribedMessageTypeIds, + campaignId, + templateId + ); + // THEN corresponding function is called on RNIterableAPI + expect(MockRNIterableAPI.updateSubscriptions).toBeCalledWith( + emailListIds, + unsubscribedChannelIds, + unsubscribedMessageTypeIds, + subscribedMessageTypeIds, + campaignId, + templateId + ); + }); +}); diff --git a/src/__tests__/IterableInApp.spec.ts b/src/__tests__/IterableInApp.spec.ts deleted file mode 100644 index 894e0a8c2..000000000 --- a/src/__tests__/IterableInApp.spec.ts +++ /dev/null @@ -1,283 +0,0 @@ -import { NativeEventEmitter } from 'react-native'; - -import { MockRNIterableAPI } from '../__mocks__/MockRNIterableAPI'; - -import { Iterable, EventName } from '../Iterable'; -import { IterableConfig } from '../IterableConfig'; -import { IterableInAppMessage } from '../IterableInAppMessage'; -import { - IterableInAppLocation, - IterableInAppTrigger, - IterableInAppTriggerType, - IterableInboxMetadata, - IterableInAppCloseSource, - IterableInAppShowResponse, - IterableInAppDeleteSource, -} from '../IterableInAppClasses'; -import { IterableLogger } from '../IterableLogger'; - -beforeEach(() => { - jest.clearAllMocks(); - Iterable.logger = new IterableLogger(new IterableConfig()); -}); - -test('trackInAppOpen_params_methodCalledWithParams', () => { - // GIVEN an in-app message and a location - const msg: IterableInAppMessage = new IterableInAppMessage( - 'someMessageId', - 123, - new IterableInAppTrigger(IterableInAppTriggerType.event), - new Date(1234), - new Date(123123), - true, - new IterableInboxMetadata('title', 'subtitle', 'iconURL'), - { CustomPayloadKey: 'CustomPayloadValue' }, - false, - 300.5 - ); - const location: IterableInAppLocation = IterableInAppLocation.inApp; - - // WHEN Iterable.trackInAppOpen is called - Iterable.trackInAppOpen(msg, location); - - // THEN corresponding method is called on MockIterableAPI with appropriate parameters - expect(MockRNIterableAPI.trackInAppOpen).toBeCalledWith( - msg.messageId, - location - ); -}); - -test('trackInAppClick_params_methodCalledWithParams', () => { - // GIVEN an in-app message, a location, and a url - const msg: IterableInAppMessage = new IterableInAppMessage( - 'someMessageId', - 123, - new IterableInAppTrigger(IterableInAppTriggerType.event), - new Date(1234), - new Date(123123), - true, - new IterableInboxMetadata('title', 'subtitle', 'iconURL'), - { CustomPayloadKey: 'CustomPayloadValue' }, - false, - 300.5 - ); - const location: IterableInAppLocation = IterableInAppLocation.inApp; - const url: string = 'URLClicked'; - - // WHEN Iterable.trackInAppClick is called - Iterable.trackInAppClick(msg, location, url); - - // THEN corresponding method is called on MockIterableAPI with appropriate parameters - expect(MockRNIterableAPI.trackInAppClick).toBeCalledWith( - msg.messageId, - location, - url - ); -}); - -test('trackInAppClose_params_methodCalledWithParams', () => { - // GIVEN an in-app messsage, a location, a close source, and a url - const msg: IterableInAppMessage = new IterableInAppMessage( - 'someMessageId', - 123, - new IterableInAppTrigger(IterableInAppTriggerType.event), - new Date(1234), - new Date(123123), - true, - new IterableInboxMetadata('title', 'subtitle', 'iconURL'), - { CustomPayloadKey: 'CustomPayloadValue' }, - false, - 300.5 - ); - const location: IterableInAppLocation = IterableInAppLocation.inbox; - const source: IterableInAppCloseSource = IterableInAppCloseSource.link; - const url: string = 'ClickedURL'; - - // WHEN Iterable.trackInAppClose is called - Iterable.trackInAppClose(msg, location, source, url); - - // THEN corresponding method is called on MockIterableAPI with appropriate parameters - expect(MockRNIterableAPI.trackInAppClose).toBeCalledWith( - msg.messageId, - location, - source, - url - ); -}); - -test('inAppConsume_params_methodCalledWithParams', () => { - // GIVEN an in-app messsage, a location, and a delete source - const msg = new IterableInAppMessage( - 'asdf', - 1234, - new IterableInAppTrigger(IterableInAppTriggerType.never), - undefined, - undefined, - false, - undefined, - undefined, - false, - 300.5 - ); - const location: IterableInAppLocation = IterableInAppLocation.inApp; - const source: IterableInAppDeleteSource = IterableInAppDeleteSource.unknown; - - // WHEN Iterable.inAppConsume is called - Iterable.inAppConsume(msg, location, source); - - // THEN corresponding method is called on MockIterableAPI with appropriate parameters - expect(MockRNIterableAPI.inAppConsume).toBeCalledWith( - msg.messageId, - location, - source - ); -}); - -test('inAppHandler_messageAndEventEmitted_methodCalledWithMessage', () => { - // sets up event emitter - const nativeEmitter = new NativeEventEmitter(); - nativeEmitter.removeAllListeners(EventName.handleInAppCalled); - - // sets up config file and inAppHandler function - const config = new IterableConfig(); - config.inAppHandler = jest.fn((_message: IterableInAppMessage) => { - return IterableInAppShowResponse.show; - }); - - // initialize Iterable object - - Iterable.initialize('apiKey', config); - - // GIVEN an in-app message - const messageDict = { - messageId: 'message1', - campaignId: 1234, - trigger: { type: IterableInAppTriggerType.immediate }, - priorityLevel: 300.5, - }; - const expectedMessage = new IterableInAppMessage( - 'message1', - 1234, - new IterableInAppTrigger(IterableInAppTriggerType.immediate), - undefined, - undefined, - false, - undefined, - undefined, - false, - 300.5 - ); - - // WHEN handleInAppCalled event is emitted - nativeEmitter.emit(EventName.handleInAppCalled, messageDict); - - // THEN inAppHandler and MockRNIterableAPI.setInAppShowResponse is called with message - expect(config.inAppHandler).toBeCalledWith(expectedMessage); - expect(MockRNIterableAPI.setInAppShowResponse).toBeCalledWith( - IterableInAppShowResponse.show - ); -}); - -test('getMessages_noParams_returnsMessages', async () => { - // GIVEN a list of in-app messages representing the local queue - const messageDicts = [ - { - messageId: 'message1', - campaignId: 1234, - trigger: { type: IterableInAppTriggerType.immediate }, - }, - { - messageId: 'message2', - campaignId: 2345, - trigger: { type: IterableInAppTriggerType.never }, - }, - ]; - const messages = messageDicts.map((message) => - IterableInAppMessage.fromDict(message) - ); - - // WHEN the simulated local queue is set to the in-app messages - MockRNIterableAPI.setMessages(messages); - - // THEN Iterable,inAppManager.getMessages returns the list of in-app messages - return await Iterable.inAppManager.getMessages().then((messagesObtained) => { - expect(messagesObtained).toEqual(messages); - }); -}); - -test('showMessage_messageAndConsume_returnsClickedUrl', async () => { - // GIVEN an in-app message and a clicked url - const messageDict = { - messageId: 'message1', - campaignId: 1234, - trigger: { type: IterableInAppTriggerType.immediate }, - }; - const message: IterableInAppMessage = - IterableInAppMessage.fromDict(messageDict); - const consume: boolean = true; - const clickedUrl: string = 'testUrl'; - - // WHEN the simulated clicked url is set to the clicked url - MockRNIterableAPI.setClickedUrl(clickedUrl); - - // THEN Iterable,inAppManager.showMessage returns the simulated clicked url - return await Iterable.inAppManager - .showMessage(message, consume) - .then((url) => { - expect(url).toEqual(clickedUrl); - }); -}); - -test('removeMessage_params_methodCalledWithParams', () => { - // GIVEN an in-app message - const messageDict = { - messageId: 'message1', - campaignId: 1234, - trigger: { type: IterableInAppTriggerType.immediate }, - }; - const message = IterableInAppMessage.fromDict(messageDict); - const location: IterableInAppLocation = IterableInAppLocation.inApp; - const source: IterableInAppDeleteSource = - IterableInAppDeleteSource.deleteButton; - - // WHEN Iterable.inAppManager.removeMessage is called - Iterable.inAppManager.removeMessage(message, location, source); - - // THEN corresponding method is called on MockIterableAPI with appropriate parameters - expect(MockRNIterableAPI.removeMessage).toBeCalledWith( - message.messageId, - location, - source - ); -}); - -test('setReadForMessage_params_methodCalledWithParams', () => { - // GIVEN an in-app message - const messageDict = { - messageId: 'message1', - campaignId: 1234, - trigger: { type: IterableInAppTriggerType.immediate }, - }; - const message = IterableInAppMessage.fromDict(messageDict); - const read: boolean = true; - - // WHEN Iterable.inAppManager.setReadForMessage is called - Iterable.inAppManager.setReadForMessage(message, read); - - // THEN corresponding method is called on MockRNIterableAPI with appropriate parameters - expect(MockRNIterableAPI.setReadForMessage).toBeCalledWith( - message.messageId, - read - ); -}); - -test('setAutoDisplayPaused_params_methodCalledWithParams', () => { - // GIVEN paused flag - const paused: boolean = true; - - // WHEN Iterable.inAppManager.setAutoDisplayPaused is called - Iterable.inAppManager.setAutoDisplayPaused(paused); - - // THEN corresponding method is called on MockRNIterableAPI with appropriate parameters - expect(MockRNIterableAPI.setAutoDisplayPaused).toBeCalledWith(paused); -}); diff --git a/src/__tests__/IterableInApp.test.ts b/src/__tests__/IterableInApp.test.ts new file mode 100644 index 000000000..110bcb47f --- /dev/null +++ b/src/__tests__/IterableInApp.test.ts @@ -0,0 +1,287 @@ +import { NativeEventEmitter } from 'react-native'; + +import { MockRNIterableAPI } from '../__mocks__/MockRNIterableAPI'; + +import { Iterable, EventName } from '../Iterable'; +import { IterableConfig } from '../IterableConfig'; +import { IterableInAppMessage } from '../IterableInAppMessage'; +import { + IterableInAppLocation, + IterableInAppTrigger, + IterableInAppTriggerType, + IterableInboxMetadata, + IterableInAppCloseSource, + IterableInAppShowResponse, + IterableInAppDeleteSource, +} from '../IterableInAppClasses'; +import { IterableLogger } from '../IterableLogger'; + +describe('Iterable In App', () => { + beforeEach(() => { + jest.clearAllMocks(); + Iterable.logger = new IterableLogger(new IterableConfig()); + }); + + test('trackInAppOpen_params_methodCalledWithParams', () => { + // GIVEN an in-app message and a location + const msg: IterableInAppMessage = new IterableInAppMessage( + 'someMessageId', + 123, + new IterableInAppTrigger(IterableInAppTriggerType.event), + new Date(1234), + new Date(123123), + true, + new IterableInboxMetadata('title', 'subtitle', 'iconURL'), + { CustomPayloadKey: 'CustomPayloadValue' }, + false, + 300.5 + ); + const location: IterableInAppLocation = IterableInAppLocation.inApp; + + // WHEN Iterable.trackInAppOpen is called + Iterable.trackInAppOpen(msg, location); + + // THEN corresponding method is called on MockIterableAPI with appropriate parameters + expect(MockRNIterableAPI.trackInAppOpen).toBeCalledWith( + msg.messageId, + location + ); + }); + + test('trackInAppClick_params_methodCalledWithParams', () => { + // GIVEN an in-app message, a location, and a url + const msg: IterableInAppMessage = new IterableInAppMessage( + 'someMessageId', + 123, + new IterableInAppTrigger(IterableInAppTriggerType.event), + new Date(1234), + new Date(123123), + true, + new IterableInboxMetadata('title', 'subtitle', 'iconURL'), + { CustomPayloadKey: 'CustomPayloadValue' }, + false, + 300.5 + ); + const location: IterableInAppLocation = IterableInAppLocation.inApp; + const url: string = 'URLClicked'; + + // WHEN Iterable.trackInAppClick is called + Iterable.trackInAppClick(msg, location, url); + + // THEN corresponding method is called on MockIterableAPI with appropriate parameters + expect(MockRNIterableAPI.trackInAppClick).toBeCalledWith( + msg.messageId, + location, + url + ); + }); + + test('trackInAppClose_params_methodCalledWithParams', () => { + // GIVEN an in-app messsage, a location, a close source, and a url + const msg: IterableInAppMessage = new IterableInAppMessage( + 'someMessageId', + 123, + new IterableInAppTrigger(IterableInAppTriggerType.event), + new Date(1234), + new Date(123123), + true, + new IterableInboxMetadata('title', 'subtitle', 'iconURL'), + { CustomPayloadKey: 'CustomPayloadValue' }, + false, + 300.5 + ); + const location: IterableInAppLocation = IterableInAppLocation.inbox; + const source: IterableInAppCloseSource = IterableInAppCloseSource.link; + const url: string = 'ClickedURL'; + + // WHEN Iterable.trackInAppClose is called + Iterable.trackInAppClose(msg, location, source, url); + + // THEN corresponding method is called on MockIterableAPI with appropriate parameters + expect(MockRNIterableAPI.trackInAppClose).toBeCalledWith( + msg.messageId, + location, + source, + url + ); + }); + + test('inAppConsume_params_methodCalledWithParams', () => { + // GIVEN an in-app messsage, a location, and a delete source + const msg = new IterableInAppMessage( + 'asdf', + 1234, + new IterableInAppTrigger(IterableInAppTriggerType.never), + undefined, + undefined, + false, + undefined, + undefined, + false, + 300.5 + ); + const location: IterableInAppLocation = IterableInAppLocation.inApp; + const source: IterableInAppDeleteSource = IterableInAppDeleteSource.unknown; + + // WHEN Iterable.inAppConsume is called + Iterable.inAppConsume(msg, location, source); + + // THEN corresponding method is called on MockIterableAPI with appropriate parameters + expect(MockRNIterableAPI.inAppConsume).toBeCalledWith( + msg.messageId, + location, + source + ); + }); + + test('inAppHandler_messageAndEventEmitted_methodCalledWithMessage', () => { + // sets up event emitter + const nativeEmitter = new NativeEventEmitter(); + nativeEmitter.removeAllListeners(EventName.handleInAppCalled); + + // sets up config file and inAppHandler function + const config = new IterableConfig(); + config.inAppHandler = jest.fn((_message: IterableInAppMessage) => { + return IterableInAppShowResponse.show; + }); + + // initialize Iterable object + + Iterable.initialize('apiKey', config); + + // GIVEN an in-app message + const messageDict = { + messageId: 'message1', + campaignId: 1234, + trigger: { type: IterableInAppTriggerType.immediate }, + priorityLevel: 300.5, + }; + const expectedMessage = new IterableInAppMessage( + 'message1', + 1234, + new IterableInAppTrigger(IterableInAppTriggerType.immediate), + undefined, + undefined, + false, + undefined, + undefined, + false, + 300.5 + ); + + // WHEN handleInAppCalled event is emitted + nativeEmitter.emit(EventName.handleInAppCalled, messageDict); + + // THEN inAppHandler and MockRNIterableAPI.setInAppShowResponse is called with message + expect(config.inAppHandler).toBeCalledWith(expectedMessage); + expect(MockRNIterableAPI.setInAppShowResponse).toBeCalledWith( + IterableInAppShowResponse.show + ); + }); + + test('getMessages_noParams_returnsMessages', async () => { + // GIVEN a list of in-app messages representing the local queue + const messageDicts = [ + { + messageId: 'message1', + campaignId: 1234, + trigger: { type: IterableInAppTriggerType.immediate }, + }, + { + messageId: 'message2', + campaignId: 2345, + trigger: { type: IterableInAppTriggerType.never }, + }, + ]; + const messages = messageDicts.map((message) => + IterableInAppMessage.fromDict(message) + ); + + // WHEN the simulated local queue is set to the in-app messages + MockRNIterableAPI.setMessages(messages); + + // THEN Iterable,inAppManager.getMessages returns the list of in-app messages + return await Iterable.inAppManager + .getMessages() + .then((messagesObtained) => { + expect(messagesObtained).toEqual(messages); + }); + }); + + test('showMessage_messageAndConsume_returnsClickedUrl', async () => { + // GIVEN an in-app message and a clicked url + const messageDict = { + messageId: 'message1', + campaignId: 1234, + trigger: { type: IterableInAppTriggerType.immediate }, + }; + const message: IterableInAppMessage = + IterableInAppMessage.fromDict(messageDict); + const consume: boolean = true; + const clickedUrl: string = 'testUrl'; + + // WHEN the simulated clicked url is set to the clicked url + MockRNIterableAPI.setClickedUrl(clickedUrl); + + // THEN Iterable,inAppManager.showMessage returns the simulated clicked url + return await Iterable.inAppManager + .showMessage(message, consume) + .then((url) => { + expect(url).toEqual(clickedUrl); + }); + }); + + test('removeMessage_params_methodCalledWithParams', () => { + // GIVEN an in-app message + const messageDict = { + messageId: 'message1', + campaignId: 1234, + trigger: { type: IterableInAppTriggerType.immediate }, + }; + const message = IterableInAppMessage.fromDict(messageDict); + const location: IterableInAppLocation = IterableInAppLocation.inApp; + const source: IterableInAppDeleteSource = + IterableInAppDeleteSource.deleteButton; + + // WHEN Iterable.inAppManager.removeMessage is called + Iterable.inAppManager.removeMessage(message, location, source); + + // THEN corresponding method is called on MockIterableAPI with appropriate parameters + expect(MockRNIterableAPI.removeMessage).toBeCalledWith( + message.messageId, + location, + source + ); + }); + + test('setReadForMessage_params_methodCalledWithParams', () => { + // GIVEN an in-app message + const messageDict = { + messageId: 'message1', + campaignId: 1234, + trigger: { type: IterableInAppTriggerType.immediate }, + }; + const message = IterableInAppMessage.fromDict(messageDict); + const read: boolean = true; + + // WHEN Iterable.inAppManager.setReadForMessage is called + Iterable.inAppManager.setReadForMessage(message, read); + + // THEN corresponding method is called on MockRNIterableAPI with appropriate parameters + expect(MockRNIterableAPI.setReadForMessage).toBeCalledWith( + message.messageId, + read + ); + }); + + test('setAutoDisplayPaused_params_methodCalledWithParams', () => { + // GIVEN paused flag + const paused: boolean = true; + + // WHEN Iterable.inAppManager.setAutoDisplayPaused is called + Iterable.inAppManager.setAutoDisplayPaused(paused); + + // THEN corresponding method is called on MockRNIterableAPI with appropriate parameters + expect(MockRNIterableAPI.setAutoDisplayPaused).toBeCalledWith(paused); + }); +}); diff --git a/src/__tests__/index.test.tsx b/src/__tests__/index.test.tsx index bf84291a5..0f7012a87 100644 --- a/src/__tests__/index.test.tsx +++ b/src/__tests__/index.test.tsx @@ -1 +1,3 @@ -it.todo('write a test'); +describe('index', () => { + it.todo('write a test'); +}); diff --git a/yarn.lock b/yarn.lock index 5eda06f3c..342f94f80 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3041,6 +3041,8 @@ __metadata: "@react-native/eslint-config": ^0.73.1 "@react-navigation/native": ^6.1.18 "@release-it/conventional-changelog": ^5.0.0 + "@testing-library/jest-native": ^5.4.3 + "@testing-library/react-native": ^12.7.2 "@types/jest": ^29.5.5 "@types/react": ^18.2.44 "@types/react-native-vector-icons": ^6.4.18 @@ -4212,6 +4214,42 @@ __metadata: languageName: node linkType: hard +"@testing-library/jest-native@npm:^5.4.3": + version: 5.4.3 + resolution: "@testing-library/jest-native@npm:5.4.3" + dependencies: + chalk: ^4.1.2 + jest-diff: ^29.0.1 + jest-matcher-utils: ^29.0.1 + pretty-format: ^29.0.3 + redent: ^3.0.0 + peerDependencies: + react: ">=16.0.0" + react-native: ">=0.59" + react-test-renderer: ">=16.0.0" + checksum: 2a4ebfeff09523860771cfddac6fcc3faa2f855dc63255b9efc016e727132320f16f935cec9717d6d79cfa6715fce6ded877215c8ec85d236a5c3136a65b1020 + languageName: node + linkType: hard + +"@testing-library/react-native@npm:^12.7.2": + version: 12.7.2 + resolution: "@testing-library/react-native@npm:12.7.2" + dependencies: + jest-matcher-utils: ^29.7.0 + pretty-format: ^29.7.0 + redent: ^3.0.0 + peerDependencies: + jest: ">=28.0.0" + react: ">=16.8.0" + react-native: ">=0.59" + react-test-renderer: ">=16.8.0" + peerDependenciesMeta: + jest: + optional: true + checksum: 7e3d8ab7d549823fcf438c17353e6c40386da88bbb1edfbd0747282a28c673597be27fdc2fa1f3a7d8786b77c72bb2e37f67ad2c9134225e9b68db97838f77e2 + languageName: node + linkType: hard + "@tsconfig/node10@npm:^1.0.7": version: 1.0.11 resolution: "@tsconfig/node10@npm:1.0.11" @@ -9409,7 +9447,7 @@ __metadata: languageName: node linkType: hard -"jest-diff@npm:^29.7.0": +"jest-diff@npm:^29.0.1, jest-diff@npm:^29.7.0": version: 29.7.0 resolution: "jest-diff@npm:29.7.0" dependencies: @@ -9497,7 +9535,7 @@ __metadata: languageName: node linkType: hard -"jest-matcher-utils@npm:^29.7.0": +"jest-matcher-utils@npm:^29.0.1, jest-matcher-utils@npm:^29.7.0": version: 29.7.0 resolution: "jest-matcher-utils@npm:29.7.0" dependencies: @@ -11824,7 +11862,7 @@ __metadata: languageName: node linkType: hard -"pretty-format@npm:^29.0.0, pretty-format@npm:^29.7.0": +"pretty-format@npm:^29.0.0, pretty-format@npm:^29.0.3, pretty-format@npm:^29.7.0": version: 29.7.0 resolution: "pretty-format@npm:29.7.0" dependencies: