diff --git a/jest.components.jsx b/jest.components.jsx index 34ed4d9b2..adf0154c4 100644 --- a/jest.components.jsx +++ b/jest.components.jsx @@ -2,6 +2,11 @@ import React from 'react'; import { jest } from '@jest/globals'; +jest.mock('@app/shared/lib/constants', () => ({ + ...jest.requireActual('@app/shared/lib/constants'), + STORE_ENCRYPTION_KEY: '12345', +})); + jest.mock('@georstat/react-native-image-cache', () => { return { CachedImage: () => <>, diff --git a/jest.config.ts b/jest.config.ts index 38715656c..10fcc0eef 100644 --- a/jest.config.ts +++ b/jest.config.ts @@ -43,6 +43,8 @@ const jestConfig: JestConfigWithTsJest = { '/assetsTransformer.js', '^uuid$': require.resolve('uuid'), }, + restoreMocks: true, + clearMocks: true, }; export default jestConfig; diff --git a/jest.setup.js b/jest.setup.js index 77417f338..29c2c1b01 100644 --- a/jest.setup.js +++ b/jest.setup.js @@ -52,8 +52,6 @@ jest.mock('@react-native-community/geolocation', () => jest.mock('react-native-sensors', () => jest.mock('react-native-sensors')); -jest.mock('mixpanel-react-native', () => jest.mock('mixpanel-react-native')); - jest.mock('react-native-tcp-socket', () => { return { createConnection: () => { @@ -210,11 +208,6 @@ jest.mock('react-native-webview', () => { }; }); -jest.mock('@app/shared/lib/constants', () => ({ - ...jest.requireActual('@app/shared/lib/constants'), - STORE_ENCRYPTION_KEY: '12345', -})); - jest.mock('react-i18next', () => ({ useTranslation: jest.fn().mockImplementation(() => ({ t: jest.fn().mockImplementation(key => key), @@ -241,6 +234,39 @@ jest.mock('@react-navigation/native', () => ({ useFocusEffect: jest.fn(), })); +class MockMixpanel { + constructor() {} + init() {} + track() {} + identify() {} + getPeople() {} + reset() {} +} + +jest.mock('mixpanel-react-native', () => jest.mock('mixpanel-react-native')); + +jest.mock('mixpanel-react-native', () => ({ + Mixpanel: MockMixpanel, +})); + +class MockReactNativeLDClient { + constructor() {} + init() {} + identify() {} + boolVariation() {} + on() {} +} + +const MockAutoEnvAttributes = { + Disabled: 0, + Enabled: 1, +}; + jest.mock('@launchdarkly/react-native-client-sdk', () => jest.mock('@launchdarkly/react-native-client-sdk'), ); + +jest.mock('@launchdarkly/react-native-client-sdk', () => ({ + ReactNativeLDClient: MockReactNativeLDClient, + AutoEnvAttributes: MockAutoEnvAttributes, +})); diff --git a/src/app/ui/AppProvider/AnalyticsProvider.tsx b/src/app/ui/AppProvider/AnalyticsProvider.tsx index 350231427..634c115cd 100644 --- a/src/app/ui/AppProvider/AnalyticsProvider.tsx +++ b/src/app/ui/AppProvider/AnalyticsProvider.tsx @@ -1,6 +1,6 @@ import { FC, PropsWithChildren, useEffect } from 'react'; -import { AnalyticsService } from '@app/shared/lib/analytics/AnalyticsService'; +import { getDefaultAnalyticsService } from '@app/shared/lib/analytics/analyticsServiceInstance'; import { useSystemBootUp } from '@app/shared/lib/contexts/SplashContext'; import { getDefaultLogger } from '@app/shared/lib/services/loggerInstance'; @@ -8,11 +8,14 @@ export const AnalyticsProvider: FC = ({ children }) => { const { onModuleInitialized } = useSystemBootUp(); useEffect(() => { - AnalyticsService.init().then(() => { - getDefaultLogger().log('[AnalyticsProvider]: Initialized'); + getDefaultAnalyticsService() + .init() + .then(() => { + getDefaultLogger().log('[AnalyticsProvider]: Initialized'); - onModuleInitialized('analytics'); - }); + onModuleInitialized('analytics'); + }) + .catch(console.error); }, [onModuleInitialized]); return <>{children}; diff --git a/src/app/ui/AppProvider/FeatureFlagsProvider.tsx b/src/app/ui/AppProvider/FeatureFlagsProvider.tsx index 1b8114f21..a68c51a76 100644 --- a/src/app/ui/AppProvider/FeatureFlagsProvider.tsx +++ b/src/app/ui/AppProvider/FeatureFlagsProvider.tsx @@ -6,7 +6,7 @@ import { } from '@launchdarkly/react-native-client-sdk'; import { useSystemBootUp } from '@app/shared/lib/contexts/SplashContext'; -import { FeatureFlagsService } from '@app/shared/lib/featureFlags/FeatureFlagsService'; +import { getDefaultFeatureFlagsService } from '@app/shared/lib/featureFlags/featureFlagsServiceInstance'; import { getDefaultLogger } from '@app/shared/lib/services/loggerInstance'; export const FeatureFlagsProvider: FC = ({ children }) => { @@ -14,18 +14,19 @@ export const FeatureFlagsProvider: FC = ({ children }) => { const [ldClient, setClient] = useState(); useEffect(() => { - FeatureFlagsService.init() - .then(client => { - getDefaultLogger().log('[FeatureFlagsProvider]: Initialized'); - - setClient(client); - onModuleInitialized('featureFlags'); - }) - .catch(error => { - getDefaultLogger().error( - `[FeatureFlagsProvider]: Failed to initialize\n${error}`, - ); - }); + let client: ReactNativeLDClient | undefined; + try { + client = getDefaultFeatureFlagsService().init(); + } catch (err) { + getDefaultLogger().error( + `[FeatureFlagsProvider]: Failed to initialize\n${err as never}`, + ); + } + if (client) { + getDefaultLogger().log('[FeatureFlagsProvider]: Initialized'); + setClient(client); + onModuleInitialized('featureFlags'); + } }, [onModuleInitialized, setClient]); return ( diff --git a/src/app/ui/AppProvider/index.tsx b/src/app/ui/AppProvider/index.tsx index a71280920..3c6270fa1 100644 --- a/src/app/ui/AppProvider/index.tsx +++ b/src/app/ui/AppProvider/index.tsx @@ -9,10 +9,8 @@ import { SafeAreaProvider } from 'react-native-safe-area-context'; import Toast from 'react-native-toast-message'; import { LocalizationProvider } from '@app/entities/localization/ui/LocalizationProvider'; -import { - AnalyticsService, - MixEvents, -} from '@app/shared/lib/analytics/AnalyticsService'; +import { getDefaultAnalyticsService } from '@app/shared/lib/analytics/analyticsServiceInstance'; +import { MixEvents } from '@app/shared/lib/analytics/IAnalyticsService'; import { getDefaultLogger } from '@app/shared/lib/services/loggerInstance'; import { AnalyticsProvider } from './AnalyticsProvider'; @@ -40,7 +38,7 @@ export const AppProvider: FC = ({ children }) => { const onLoadingFinished = () => { getDefaultLogger().log('[AppProvider]: App loaded'); - AnalyticsService.track(MixEvents.AppOpen); + getDefaultAnalyticsService().track(MixEvents.AppOpen); setIsBootingUp(false); }; diff --git a/src/entities/activity/lib/hooks/useRetryUpload.ts b/src/entities/activity/lib/hooks/useRetryUpload.ts index a65a8aae8..0e9bebf0e 100644 --- a/src/entities/activity/lib/hooks/useRetryUpload.ts +++ b/src/entities/activity/lib/hooks/useRetryUpload.ts @@ -1,9 +1,7 @@ import { useState } from 'react'; -import { - AnalyticsService, - MixEvents, -} from '@app/shared/lib/analytics/AnalyticsService'; +import { getDefaultAnalyticsService } from '@app/shared/lib/analytics/analyticsServiceInstance'; +import { MixEvents } from '@app/shared/lib/analytics/IAnalyticsService'; import { getDefaultUploadObservable } from '@app/shared/lib/observables/uploadObservableInstance'; import { showUploadErrorAlert } from '../alerts'; @@ -31,7 +29,7 @@ export const useRetryUpload = ({ showUploadErrorAlert({ onRetry: async () => { - AnalyticsService.track(MixEvents.RetryButtonPressed); + getDefaultAnalyticsService().track(MixEvents.RetryButtonPressed); try { setIsAlertOpened(false); diff --git a/src/entities/activity/lib/services/UploadItemPreprocessor.ts b/src/entities/activity/lib/services/UploadItemPreprocessor.ts index af5b72494..6c6193eca 100644 --- a/src/entities/activity/lib/services/UploadItemPreprocessor.ts +++ b/src/entities/activity/lib/services/UploadItemPreprocessor.ts @@ -1,7 +1,7 @@ import { mapAppletDetailsFromDto } from '@app/entities/applet/model/mappers'; import { QueryDataUtils } from '@app/shared/api/services/QueryDataUtils'; import { FeatureFlagsKeys } from '@app/shared/lib/featureFlags/FeatureFlags.types'; -import { FeatureFlagsService } from '@app/shared/lib/featureFlags/FeatureFlagsService'; +import { getDefaultFeatureFlagsService } from '@app/shared/lib/featureFlags/featureFlagsServiceInstance'; import { getDefaultQueryClient } from '@app/shared/lib/queryClient/queryClientInstance'; import { getDefaultLogger } from '@app/shared/lib/services/loggerInstance'; import { IPreprocessor } from '@app/shared/lib/types/service'; @@ -31,7 +31,7 @@ export class UploadItemPreprocessor implements IPreprocessor { if ( appletDetails.consentsCapabilityEnabled && - FeatureFlagsService.evaluateFlag( + getDefaultFeatureFlagsService().evaluateFlag( FeatureFlagsKeys.enableConsentsCapability, ) ) { diff --git a/src/entities/activity/lib/services/tests/AnswersQueueService.test.ts b/src/entities/activity/lib/services/tests/AnswersQueueService.test.ts index fe9d068e6..a731f8670 100644 --- a/src/entities/activity/lib/services/tests/AnswersQueueService.test.ts +++ b/src/entities/activity/lib/services/tests/AnswersQueueService.test.ts @@ -5,38 +5,22 @@ import { AnswersQueueService, UploadItem } from '../AnswersQueueService'; const notifyMock = { notify: () => {} }; -const storageMock = { - addOnValueChangedListener: jest.fn(), - getAllKeys: jest.fn(), - getString: jest.fn(), - set: jest.fn(), - delete: jest.fn(), -} as unknown as MMKV; - const getAllKeysMock = jest.fn(); const getStringMock = jest.fn(); const setMock = jest.fn(); const deleteMock = jest.fn(); -jest.mock('@shared/lib/storages', () => ({ - createStorage: jest.fn(), - createSecureStorage: jest.fn().mockReturnValue({ - addOnValueChangedListener: jest.fn().mockImplementation((f: () => void) => { - f(); - }), - getAllKeys: () => getAllKeysMock() as Array, - getString: (id: string) => getStringMock(id) as string, - set: (key: string, item: UploadItem) => setMock(key, item) as void, - delete: (key: string) => deleteMock(key) as void, +const storageMock = { + addOnValueChangedListener: jest.fn().mockImplementation((f: () => void) => { + f(); }), -})); + getAllKeys: () => getAllKeysMock() as Array, + getString: (id: string) => getStringMock(id) as string, + set: (key: string, item: UploadItem) => setMock(key, item) as void, + delete: (key: string) => deleteMock(key) as void, +} as unknown as MMKV; describe('Test AnswersQueueService', () => { - afterEach(() => { - jest.clearAllMocks(); - jest.resetAllMocks(); - }); - it('Should pick an object when some keys exist', () => { getAllKeysMock.mockReturnValue(['10', '15', '7']); diff --git a/src/entities/activity/lib/services/tests/AnswersUploadService.test.js b/src/entities/activity/lib/services/tests/AnswersUploadService.test.ts similarity index 65% rename from src/entities/activity/lib/services/tests/AnswersUploadService.test.js rename to src/entities/activity/lib/services/tests/AnswersUploadService.test.ts index a864efe99..dc7cf6f5c 100644 --- a/src/entities/activity/lib/services/tests/AnswersUploadService.test.js +++ b/src/entities/activity/lib/services/tests/AnswersUploadService.test.ts @@ -1,53 +1,62 @@ import { FileSystem } from 'react-native-file-access'; -import { getDefaultUserPrivateKeyRecord } from '@app/entities/identity/lib/userPrivateKeyRecord'; -import { AnswerService } from '@shared/api'; - -import AnswersUploadService from '../AnswersUploadService'; -import MediaFilesCleaner from '../MediaFilesCleaner'; - -const MOCK_CREATED_AT = +new Date(); - -jest.mock('@app/shared/lib', () => ({ - createSecureStorage: jest.fn(() => ({ - getString: jest.fn(data => '{}'), - })), - encryption: { - createEncryptionService: jest.fn(() => ({ - encrypt: jest.fn(data => `encrypted_${data}`), - })), - getPublicKey: jest.fn(() => 'mocked_public_key'), - }, - Logger: { - log: jest.fn(), - }, - isLocalFileUrl: jest.fn(), -})); - -jest.mock('@app/shared/api', () => ({ - AnswerService: { - sendActivityAnswers: jest.fn(), - }, - FileService: { - upload: jest.fn(() => ({ - data: { - result: { - url: 'mocked_remote_url', - }, - }, - })), - }, -})); +import { IUserPrivateKeyRecord } from '@app/entities/identity/lib/IUserPrivateKeyRecord'; +import { getDefaultUserPrivateKeyRecord } from '@app/entities/identity/lib/userPrivateKeyRecordInstance'; +import { getDefaultAnswerService } from '@app/shared/api/services/answerServiceInstance'; +import { + ActivityAnswersRequest, + AnswerDto, + IAnswerService, + UserActionDto, +} from '@app/shared/api/services/IAnswerService'; +import { getDefaultEncryptionManager } from '@app/shared/lib/encryption/encryptionManagerInstance'; +import { IEncryptionManager } from '@app/shared/lib/encryption/IEncryptionManager'; +import { getDefaultLogger } from '@app/shared/lib/services/loggerInstance'; +import { ILogger } from '@app/shared/lib/types/logger'; +import { MediaFile } from '@app/shared/ui/survey/MediaItems/types'; + +import { SendAnswersInput } from '../../types/uploadAnswers'; +import { + AnswersUploadService, + IAnswersUploadService, +} from '../AnswersUploadService'; +import { getDefaultAnswersUploadService } from '../answersUploadServiceInstance'; +import { IMediaFilesCleaner } from '../IMediaFilesCleaner'; +import { getDefaultMediaFilesCleaner } from '../mediaFilesCleanerInstance'; + +type ITestAnswersUploadService = IAnswersUploadService & { + createdAt: number | null; + getUploadRecord: AnswersUploadService['getUploadRecord']; + collectFileIds: AnswersUploadService['collectFileIds']; + processFileUpload: AnswersUploadService['processFileUpload']; + uploadAllMediaFiles: AnswersUploadService['uploadAllMediaFiles']; + uploadAnswers: AnswersUploadService['uploadAnswers']; + encryptAnswers: AnswersUploadService['encryptAnswers']; + assignRemoteUrlsToUserActions: AnswersUploadService['assignRemoteUrlsToUserActions']; +}; describe('AnswersUploadService', () => { + const MOCK_CREATED_AT = +new Date(); + + let logger: ILogger; + let userPrivateKeyRecord: IUserPrivateKeyRecord; + let answerService: IAnswerService; + let answersUploadService: ITestAnswersUploadService; + let mediaFileCleaner: IMediaFilesCleaner; + beforeEach(() => { - jest.clearAllMocks(); + logger = getDefaultLogger(); + jest.spyOn(logger, 'log').mockImplementation(() => {}); - AnswersUploadService.createdAt = MOCK_CREATED_AT; - AnswersUploadService.uploadProgressObservable = { - setTotalFilesInActivity: jest.fn(), - setCurrentSecondLevelStepKey: jest.fn(), - }; + userPrivateKeyRecord = getDefaultUserPrivateKeyRecord(); + + answerService = getDefaultAnswerService(); + + answersUploadService = + getDefaultAnswersUploadService() as never as ITestAnswersUploadService; + answersUploadService.createdAt = MOCK_CREATED_AT; + + mediaFileCleaner = getDefaultMediaFilesCleaner(); }); describe('getUploadRecord function', () => { @@ -60,7 +69,7 @@ describe('AnswersUploadService', () => { const fileId = 'fileId2'; - const result = AnswersUploadService.getUploadRecord( + const result = answersUploadService.getUploadRecord( uploadResults, fileId, ); @@ -80,7 +89,7 @@ describe('AnswersUploadService', () => { const fileId = 'fileId3'; - const result = AnswersUploadService.getUploadRecord( + const result = answersUploadService.getUploadRecord( uploadResults, fileId, ); @@ -112,7 +121,7 @@ describe('AnswersUploadService', () => { fileName: 'audio.m4a', }, }, - ]; + ] as AnswerDto[]; const mockIsFileUrl = jest.fn(url => { if ( @@ -125,9 +134,11 @@ describe('AnswersUploadService', () => { return false; } }); - AnswersUploadService.isFileUrl = mockIsFileUrl; + jest + .spyOn(answersUploadService as never, 'isFileUrl') + .mockImplementation(mockIsFileUrl as never); - const result = AnswersUploadService.collectFileIds(answers); + const result = answersUploadService.collectFileIds(answers); expect(result).toEqual([ `${MOCK_CREATED_AT}/image.jpg`, @@ -141,9 +152,9 @@ describe('AnswersUploadService', () => { { value: 'text answer' }, { value: { uri: 'http://example.com/image.jpg' } }, { value: 'another text answer' }, - ]; + ] as AnswerDto[]; - const result = AnswersUploadService.collectFileIds(answers); + const result = answersUploadService.collectFileIds(answers); expect(result).toEqual([]); }); @@ -155,34 +166,44 @@ describe('AnswersUploadService', () => { describe('processFileUpload function', () => { it('should throw an error for non-existing local file', async () => { - FileSystem.exists.mockResolvedValueOnce(false); + jest.spyOn(FileSystem, 'exists').mockResolvedValue(false); - const mediaAnswer = { uri: 'file:///non/existing/file.jpg' }; + const mediaAnswer = { uri: 'file:///non/existing/file.jpg' } as MediaFile; const uploadResults = [ { fileId: 'fileId1', uploaded: false, remoteUrl: null }, ]; await expect( - AnswersUploadService.processFileUpload(mediaAnswer, uploadResults), + answersUploadService.processFileUpload( + mediaAnswer, + uploadResults, + 0, + 'mock-applet-id', + ), ).rejects.toThrow(/does not exist/); }); it('should throw an error for missing upload record', async () => { - FileSystem.exists.mockResolvedValueOnce(true); + jest.spyOn(FileSystem, 'exists').mockResolvedValue(true); - const mediaAnswer = { uri: 'file:///path/to/image.jpg' }; + const mediaAnswer = { uri: 'file:///path/to/image.jpg' } as MediaFile; const uploadResults = [ { fileId: 'fileId1', uploaded: false, remoteUrl: null }, ]; await expect( - AnswersUploadService.processFileUpload(mediaAnswer, uploadResults), + answersUploadService.processFileUpload( + mediaAnswer, + uploadResults, + 0, + 'mock-applet-id', + ), ).rejects.toThrow(/uploadRecord does not exist/); }); }); it('should upload media files and update answers', async () => { - FileSystem.exists.mockResolvedValue(true); + jest.spyOn(FileSystem, 'exists').mockResolvedValue(true); const answers = [ { @@ -200,7 +221,7 @@ describe('AnswersUploadService', () => { type: 'video/mp4', }, }, - ]; + ] as AnswerDto[]; const fakeUploadResults = [ { @@ -218,12 +239,16 @@ describe('AnswersUploadService', () => { const mockCheckIfFilesUploaded = jest .fn() .mockResolvedValue(fakeUploadResults); - AnswersUploadService.checkIfFilesUploaded = mockCheckIfFilesUploaded; + jest + .spyOn(answersUploadService as never, 'checkIfFilesUploaded') + .mockImplementation(mockCheckIfFilesUploaded as never); const mockProcessFileUpload = jest .fn() .mockResolvedValue('https://example.com/modified-answer.jpg'); - AnswersUploadService.processFileUpload = mockProcessFileUpload; + jest + .spyOn(answersUploadService as never, 'processFileUpload') + .mockImplementation(mockProcessFileUpload as never); const mockIsFileUrl = jest.fn(url => { if ( @@ -235,14 +260,16 @@ describe('AnswersUploadService', () => { return false; } }); - AnswersUploadService.isFileUrl = mockIsFileUrl; + jest + .spyOn(answersUploadService as never, 'isFileUrl') + .mockImplementation(mockIsFileUrl as never); const modifiedBody = { answers, appletId: '1e631d7f-2ce8-4ec0-be52-c77092bd203e', - }; + } as SendAnswersInput; - const result = await AnswersUploadService.uploadAllMediaFiles(modifiedBody); + const result = await answersUploadService.uploadAllMediaFiles(modifiedBody); expect(mockCheckIfFilesUploaded).toHaveBeenCalledWith( [`${MOCK_CREATED_AT}/image.jpg`, `${MOCK_CREATED_AT}/video.mp4`], @@ -260,18 +287,18 @@ describe('AnswersUploadService', () => { const answers = [ { value: 'text answer' }, { value: 'another text answer' }, - ]; + ] as AnswerDto[]; const modifiedBody = { answers, - }; + } as SendAnswersInput; - const result = await AnswersUploadService.uploadAllMediaFiles(modifiedBody); + const result = await answersUploadService.uploadAllMediaFiles(modifiedBody); expect(result.answers).toEqual(answers); // Answers should remain unchanged }); it('should handle file upload errors', async () => { - FileSystem.exists.mockResolvedValueOnce(true); + jest.spyOn(FileSystem, 'exists').mockResolvedValue(true); const answers = [ { @@ -293,8 +320,9 @@ describe('AnswersUploadService', () => { const mockCheckIfFilesUploaded = jest .fn() .mockResolvedValue(fakeUploadResults); - - AnswersUploadService.checkIfFilesUploaded = mockCheckIfFilesUploaded; + jest + .spyOn(answersUploadService as never, 'checkIfFilesUploaded') + .mockImplementation(mockCheckIfFilesUploaded as never); const mockProcessFileUpload = jest .fn() @@ -303,14 +331,16 @@ describe('AnswersUploadService', () => { '[UploadAnswersService.mockProcessFileUpload]: Error occurred while file uploading', ), ); - AnswersUploadService.processFileUpload = mockProcessFileUpload; + jest + .spyOn(answersUploadService as never, 'processFileUpload') + .mockImplementation(mockProcessFileUpload as never); const modifiedBody = { answers, - }; + } as SendAnswersInput; await expect( - AnswersUploadService.uploadAllMediaFiles(modifiedBody), + answersUploadService.uploadAllMediaFiles(modifiedBody), ).rejects.toThrow( '[UploadAnswersService.mockProcessFileUpload]: Error occurred while file uploading', ); @@ -322,19 +352,23 @@ describe('AnswersUploadService', () => { appletId: 'applet123', flowId: 'flow123', createdAt: 1234567890, - }; + } as ActivityAnswersRequest; const mockCheckIfAnswersUploaded = jest.fn().mockResolvedValueOnce(false); - AnswersUploadService.checkIfAnswersUploaded = mockCheckIfAnswersUploaded; + jest + .spyOn(answersUploadService as never, 'checkIfAnswersUploaded') + .mockImplementation(mockCheckIfAnswersUploaded as never); const mockSendActivityAnswers = jest.fn(() => { - AnswersUploadService.checkIfAnswersUploaded = jest - .fn() - .mockResolvedValueOnce(true); + jest + .spyOn(answersUploadService as never, 'checkIfAnswersUploaded') + .mockResolvedValueOnce(true as never); }); - AnswerService.sendActivityAnswers = mockSendActivityAnswers; + jest + .spyOn(answerService, 'sendActivityAnswers') + .mockImplementation(mockSendActivityAnswers as never); - await AnswersUploadService.uploadAnswers(encryptedData); + await answersUploadService.uploadAnswers(encryptedData); expect(mockCheckIfAnswersUploaded).toHaveBeenCalledWith({ activityId: 'activity123', @@ -351,17 +385,21 @@ describe('AnswersUploadService', () => { appletId: 'applet123', flowId: 'flow123', createdAt: 1234567890, - }; + } as ActivityAnswersRequest; const mockCheckIfAnswersUploaded = jest.fn().mockResolvedValueOnce(false); - AnswersUploadService.checkIfAnswersUploaded = mockCheckIfAnswersUploaded; + jest + .spyOn(answersUploadService as never, 'checkIfAnswersUploaded') + .mockImplementation(mockCheckIfAnswersUploaded as never); const sendError = new Error('Any network layer error'); const mockSendActivityAnswers = jest.fn().mockRejectedValue(sendError); - AnswerService.sendActivityAnswers = mockSendActivityAnswers; + jest + .spyOn(answerService, 'sendActivityAnswers') + .mockImplementation(mockSendActivityAnswers as never); await expect( - AnswersUploadService.uploadAnswers(encryptedData), + answersUploadService.uploadAnswers(encryptedData), ).rejects.toThrow(/Error occurred while sending answers/); expect(mockCheckIfAnswersUploaded).toHaveBeenCalledWith({ @@ -380,15 +418,19 @@ describe('AnswersUploadService', () => { appletId: 'applet123', flowId: 'flow123', createdAt: 1234567890, - }; + } as ActivityAnswersRequest; const mockCheckIfAnswersUploaded = jest.fn().mockResolvedValueOnce(true); - AnswersUploadService.checkIfAnswersUploaded = mockCheckIfAnswersUploaded; + jest + .spyOn(answersUploadService as never, 'checkIfAnswersUploaded') + .mockImplementation(mockCheckIfAnswersUploaded as never); const mockSendActivityAnswers = jest.fn(); - AnswerService.sendActivityAnswers = mockSendActivityAnswers; + jest + .spyOn(answerService, 'sendActivityAnswers') + .mockImplementation(mockSendActivityAnswers as never); - await AnswersUploadService.uploadAnswers(encryptedData); + await answersUploadService.uploadAnswers(encryptedData); expect(mockCheckIfAnswersUploaded).toHaveBeenCalledWith({ activityId: 'activity123', @@ -406,13 +448,13 @@ describe('AnswersUploadService', () => { base: 'base123', }, answers: [{ value: 'answer1' }], - userActions: [], + userActions: [] as UserActionDto[], userIdentifier: 'user123', - }; + } as SendAnswersInput; - getDefaultUserPrivateKeyRecord().get = jest.fn().mockReturnValueOnce(null); + jest.spyOn(userPrivateKeyRecord, 'get').mockReturnValue(undefined); - expect(() => AnswersUploadService.encryptAnswers(data)).toThrow( + expect(() => answersUploadService.encryptAnswers(data)).toThrow( 'Error occurred while preparing answers', ); }); @@ -428,7 +470,7 @@ describe('AnswersUploadService', () => { }, }, { value: 'text answer' }, - ]; + ] as AnswerDto[]; const modifiedBody = { answers: [ @@ -442,18 +484,22 @@ describe('AnswersUploadService', () => { }, { type: 'SET_ANSWER', response: { value: 'text answer' } }, ], - }; + } as SendAnswersInput; const updatedUserActions = - AnswersUploadService.assignRemoteUrlsToUserActions( + answersUploadService.assignRemoteUrlsToUserActions( originalAnswers, modifiedBody, ); - expect(updatedUserActions[0].response.value).toBe( + expect(updatedUserActions[0]?.response).toHaveProperty( + 'value', 'https://example.com/modified-answer.jpg', ); - expect(updatedUserActions[1].response.value).toBe('text answer'); + expect(updatedUserActions[1]?.response).toHaveProperty( + 'value', + 'text answer', + ); }); it('should handle SVG files', () => { @@ -465,7 +511,7 @@ describe('AnswersUploadService', () => { type: 'image/svg', }, }, - ]; + ] as AnswerDto[]; const modifiedBody = { answers: [{ value: 'https://example.com/modified-answer.svg' }], @@ -475,15 +521,16 @@ describe('AnswersUploadService', () => { response: { value: { uri: 'file:///path/to/image.svg' } }, }, ], - }; + } as SendAnswersInput; const updatedUserActions = - AnswersUploadService.assignRemoteUrlsToUserActions( + answersUploadService.assignRemoteUrlsToUserActions( originalAnswers, modifiedBody, ); - expect(updatedUserActions[0].response.value).toBe( + expect(updatedUserActions[0]?.response).toHaveProperty( + 'value', 'https://example.com/modified-answer.svg', ); }); @@ -504,7 +551,7 @@ describe('AnswersUploadService', () => { type: 'image/svg', }, }, - ]; + ] as AnswerDto[]; const modifiedBody = { answers: [ @@ -521,18 +568,20 @@ describe('AnswersUploadService', () => { response: { value: { uri: 'file:///path/to/image.svg' } }, }, ], - }; + } as SendAnswersInput; const updatedUserActions = - AnswersUploadService.assignRemoteUrlsToUserActions( + answersUploadService.assignRemoteUrlsToUserActions( originalAnswers, modifiedBody, ); - expect(updatedUserActions[0].response.value).toBe( + expect(updatedUserActions[0]?.response).toHaveProperty( + 'value', 'https://example.com/modified-answer.jpg', ); - expect(updatedUserActions[1].response.value).toBe( + expect(updatedUserActions[1]?.response).toHaveProperty( + 'value', 'https://example.com/modified-answer.svg', ); }); @@ -553,7 +602,7 @@ describe('AnswersUploadService', () => { type: 'image/svg', }, }, - ]; + ] as AnswerDto[]; const modifiedBody = { answers: [ @@ -575,18 +624,20 @@ describe('AnswersUploadService', () => { }, }, ], - }; + } as SendAnswersInput; const updatedUserActions = - AnswersUploadService.assignRemoteUrlsToUserActions( + answersUploadService.assignRemoteUrlsToUserActions( originalAnswers, modifiedBody, ); - expect(updatedUserActions[0].response.value).toBe( + expect(updatedUserActions[0]?.response).toHaveProperty( + 'value', 'https://example.com/modified-answer.jpg', ); - expect(updatedUserActions[1].response.uri).toBe( + expect(updatedUserActions[1]?.response).toHaveProperty( + 'uri', 'file:///path/to/image.svg', ); }); @@ -600,15 +651,15 @@ describe('AnswersUploadService', () => { type: 'image/jpeg', }, }, - ]; + ] as AnswerDto[]; const modifiedBody = { answers: [{ value: 'https://example.com/modified-answer.jpg' }], userActions: [], - }; + } as never as SendAnswersInput; const updatedUserActions = - AnswersUploadService.assignRemoteUrlsToUserActions( + answersUploadService.assignRemoteUrlsToUserActions( originalAnswers, modifiedBody, ); @@ -623,7 +674,7 @@ describe('AnswersUploadService', () => { answers: [{ value: 'answer1' }, { value: 'answer2' }], userActions: [], itemIds: ['item1', 'item2'], - }; + } as never as SendAnswersInput; const modifiedBody = { createdAt: 1234567890, @@ -641,28 +692,35 @@ describe('AnswersUploadService', () => { itemIds: ['item1', 'item2'], }; - AnswersUploadService.uploadAllMediaFiles = jest - .fn() - .mockResolvedValue(modifiedBody); + jest + .spyOn(answersUploadService as never, 'uploadAllMediaFiles') + .mockResolvedValue(modifiedBody as never); const mockAssignRemoteUrlsToUserActions = jest .fn() .mockReturnValue(modifiedBody.userActions); - AnswersUploadService.assignRemoteUrlsToUserActions = - mockAssignRemoteUrlsToUserActions; + jest + .spyOn(answersUploadService as never, 'assignRemoteUrlsToUserActions') + .mockImplementation(mockAssignRemoteUrlsToUserActions as never); const mockEncryptAnswers = jest.fn().mockReturnValue('encrypted-data'); - AnswersUploadService.encryptAnswers = mockEncryptAnswers; + jest + .spyOn(answersUploadService as never, 'encryptAnswers') + .mockImplementation(mockEncryptAnswers as never); const mockUploadAnswers = jest.fn(); - AnswersUploadService.uploadAnswers = mockUploadAnswers; + jest + .spyOn(answersUploadService as never, 'uploadAnswers') + .mockImplementation(mockUploadAnswers as never); const mockCleanUpByAnswers = jest.fn(); - MediaFilesCleaner.cleanUpByAnswers = mockCleanUpByAnswers; + jest + .spyOn(mediaFileCleaner, 'cleanUpByAnswers') + .mockImplementation(mockCleanUpByAnswers); - await AnswersUploadService.sendAnswers(body); + await answersUploadService.sendAnswers(body); - expect(AnswersUploadService.uploadAllMediaFiles).toHaveBeenCalledWith(body); + expect(answersUploadService.uploadAllMediaFiles).toHaveBeenCalledWith(body); expect(mockAssignRemoteUrlsToUserActions).toHaveBeenCalledWith( body.answers, modifiedBody, @@ -678,21 +736,48 @@ describe('AnswersUploadService', () => { answers: [{ value: 'answer1' }, { value: 'answer2' }], userActions: [], itemIds: ['item1', 'item2'], - }; + } as never as SendAnswersInput; const uploadError = new Error('Uploading media files failed'); - AnswersUploadService.uploadAllMediaFiles = jest - .fn() - .mockRejectedValue(uploadError); + jest + .spyOn(answersUploadService as never, 'uploadAllMediaFiles') + .mockImplementation((() => { + throw uploadError; + }) as never); - await expect(AnswersUploadService.sendAnswers(body)).rejects.toThrow( + await expect(answersUploadService.sendAnswers(body)).rejects.toThrow( uploadError, ); }); }); describe('AnswersUploadService real example', () => { - AnswersUploadService.createdAt = MOCK_CREATED_AT; + const MOCK_CREATED_AT = +new Date(); + + let logger: ILogger; + let userPrivateKeyRecord: IUserPrivateKeyRecord; + let encryptionManager: IEncryptionManager; + let answerService: IAnswerService; + let answersUploadService: ITestAnswersUploadService; + let mediaFileCleaner: IMediaFilesCleaner; + + beforeEach(() => { + logger = getDefaultLogger(); + jest.spyOn(logger, 'log').mockImplementation(() => {}); + + userPrivateKeyRecord = getDefaultUserPrivateKeyRecord(); + + encryptionManager = getDefaultEncryptionManager(); + + answerService = getDefaultAnswerService(); + + answersUploadService = + getDefaultAnswersUploadService() as never as ITestAnswersUploadService; + answersUploadService.createdAt = MOCK_CREATED_AT; + + mediaFileCleaner = getDefaultMediaFilesCleaner(); + }); + describe('sendAnswers function', () => { it('should successfully upload answers with media files', async () => { const body = { @@ -811,32 +896,56 @@ describe('AnswersUploadService real example', () => { height: 844, }, alerts: [], - }; - - AnswersUploadService.uploadAllMediaFiles = jest - .fn() - .mockResolvedValue(body); - - AnswersUploadService.assignRemoteUrlsToUserActions = jest - .fn() - .mockResolvedValue(body.userActions); - - AnswersUploadService.createEncryptionService = jest - .fn() - .mockResolvedValue(() => {}); + } as never as SendAnswersInput; + + jest + .spyOn(answersUploadService as never, 'uploadAllMediaFiles') + .mockResolvedValue(body as never); + + jest + .spyOn(answersUploadService as never, 'assignRemoteUrlsToUserActions') + .mockResolvedValue(body.userActions as never); + + jest.spyOn(userPrivateKeyRecord, 'get').mockReturnValue({} as never); + + jest + .spyOn(encryptionManager as never, 'createEncryptionService') + .mockReturnValue({ + encrypt: jest.fn((data: never) => `encrypted_${data}`), + } as never); + + jest + .spyOn(encryptionManager as never, 'getPublicKey') + .mockReturnValue('mocked_public_key' as never); + + jest + .spyOn(mediaFileCleaner, 'cleanUpByAnswers') + .mockImplementation(() => {}); + + jest + .spyOn(answerService, 'sendActivityAnswers') + .mockResolvedValue({} as never); + + let checkIfAnswersUploadedCallCount = 0; + jest + .spyOn(answersUploadService as never, 'checkIfAnswersUploaded') + .mockImplementation((() => { + checkIfAnswersUploadedCallCount += 1; + if (checkIfAnswersUploadedCallCount <= 1) { + return false; + } + return true; + }) as never); - MediaFilesCleaner.cleanUpByAnswers = jest - .fn() - .mockResolvedValue(() => {}); + await answersUploadService.sendAnswers(body); - await AnswersUploadService.sendAnswers(body); - expect(AnswersUploadService.uploadAllMediaFiles).toHaveBeenCalledWith( + expect(answersUploadService.uploadAllMediaFiles).toHaveBeenCalledWith( body, ); expect( - AnswersUploadService.assignRemoteUrlsToUserActions, + answersUploadService.assignRemoteUrlsToUserActions, ).toHaveBeenCalledWith(body.answers, body); - expect(MediaFilesCleaner.cleanUpByAnswers).toHaveBeenCalledWith( + expect(mediaFileCleaner.cleanUpByAnswers).toHaveBeenCalledWith( body.answers, ); }); diff --git a/src/entities/applet/model/hooks/startEntityHelpers.ts b/src/entities/applet/model/hooks/startEntityHelpers.ts index b5f182818..0b890a88e 100644 --- a/src/entities/applet/model/hooks/startEntityHelpers.ts +++ b/src/entities/applet/model/hooks/startEntityHelpers.ts @@ -1,8 +1,8 @@ +import { getDefaultAnalyticsService } from '@app/shared/lib/analytics/analyticsServiceInstance'; import { - AnalyticsService, MixEvents, MixProperties, -} from '@app/shared/lib/analytics/AnalyticsService'; +} from '@app/shared/lib/analytics/IAnalyticsService'; import { getDefaultLogger } from '@app/shared/lib/services/loggerInstance'; export type LogActivityActionParams = { @@ -23,7 +23,7 @@ export const logStartActivity = (params: LogActivityActionParams) => { getDefaultLogger().log( `[useStartEntity.startActivity]: Activity "${params.entityName}|${params.activityId}" started, applet "${params.appletName}|${params.appletId}"`, ); - AnalyticsService.track(MixEvents.AssessmentStarted, { + getDefaultAnalyticsService().track(MixEvents.AssessmentStarted, { [MixProperties.AppletId]: params.appletId, }); }; @@ -32,10 +32,10 @@ export const logRestartActivity = (params: LogActivityActionParams) => { getDefaultLogger().log( `[useStartEntity.startActivity]: Activity "${params.entityName}|${params.activityId}" restarted, applet "${params.appletName}|${params.appletId}"`, ); - AnalyticsService.track(MixEvents.ActivityRestart, { + getDefaultAnalyticsService().track(MixEvents.ActivityRestart, { [MixProperties.AppletId]: params.appletId, }); - AnalyticsService.track(MixEvents.AssessmentStarted, { + getDefaultAnalyticsService().track(MixEvents.AssessmentStarted, { [MixProperties.AppletId]: params.appletId, }); }; @@ -44,7 +44,7 @@ export const logResumeActivity = (params: LogActivityActionParams) => { getDefaultLogger().log( `[useStartEntity.startActivity]: Activity "${params.entityName}|${params.activityId}" resumed, applet "${params.appletName}|${params.appletId}"`, ); - AnalyticsService.track(MixEvents.ActivityResume, { + getDefaultAnalyticsService().track(MixEvents.ActivityResume, { [MixProperties.AppletId]: params.appletId, }); }; @@ -53,7 +53,7 @@ export const logStartFlow = (params: LogFlowActionParams) => { getDefaultLogger().log( `[useStartEntity.startFlow]: Flow "${params.entityName}|${params.flowId}" started, applet "${params.appletName}|${params.appletId}"`, ); - AnalyticsService.track(MixEvents.AssessmentStarted, { + getDefaultAnalyticsService().track(MixEvents.AssessmentStarted, { [MixProperties.AppletId]: params.appletId, }); }; @@ -62,10 +62,10 @@ export const logRestartFlow = (params: LogFlowActionParams) => { getDefaultLogger().log( `[useStartEntity.startFlow]: Flow "${params.entityName}|${params.flowId}" restarted, applet "${params.appletName}|${params.appletId}"`, ); - AnalyticsService.track(MixEvents.ActivityRestart, { + getDefaultAnalyticsService().track(MixEvents.ActivityRestart, { [MixProperties.AppletId]: params.appletId, }); - AnalyticsService.track(MixEvents.AssessmentStarted, { + getDefaultAnalyticsService().track(MixEvents.AssessmentStarted, { [MixProperties.AppletId]: params.appletId, }); }; @@ -74,7 +74,7 @@ export const logResumeFlow = (params: LogFlowActionParams) => { getDefaultLogger().log( `[useStartEntity.startFlow]: Flow "${params.entityName}|${params.flowId}" resumed, applet "${params.appletName}|${params.appletId}"`, ); - AnalyticsService.track(MixEvents.ActivityResume, { + getDefaultAnalyticsService().track(MixEvents.ActivityResume, { [MixProperties.AppletId]: params.appletId, }); }; diff --git a/src/entities/applet/model/services/tests/ProgressDataCollector.test.ts b/src/entities/applet/model/services/tests/ProgressDataCollector.test.ts index 1b2b16f26..f537d8fa2 100644 --- a/src/entities/applet/model/services/tests/ProgressDataCollector.test.ts +++ b/src/entities/applet/model/services/tests/ProgressDataCollector.test.ts @@ -1,3 +1,4 @@ +import { getDefaultEventsService } from '@app/shared/api/services/eventsServiceInstance'; import { EntitiesCompletionsDto } from '@app/shared/api/services/IEventsService'; import { ILogger } from '@app/shared/lib/types/logger'; @@ -11,13 +12,6 @@ type MockedResponse = { const getAllCompletedEntitiesMock = jest.fn(); -jest.mock('@app/shared/api', () => ({ - EventsService: { - getAllCompletedEntities: () => - getAllCompletedEntitiesMock() as MockedResponse, - }, -})); - const warnMock = jest.fn(); const loggerMock = { @@ -25,9 +19,12 @@ const loggerMock = { } as unknown as ILogger; describe('Test ProgressDataCollector', () => { - afterEach(() => { - jest.clearAllMocks(); - jest.resetAllMocks(); + beforeEach(() => { + jest + .spyOn(getDefaultEventsService(), 'getAllCompletedEntities') + .mockImplementation(() => { + return getAllCompletedEntitiesMock() as never; + }); }); it('Should return 2 completions when api response is fulfilled with 2 items', async () => { @@ -86,6 +83,7 @@ describe('Test ProgressDataCollector', () => { localEndDate: '2021-10-07', localEndTime: '01:02:03', scheduledEventId: 'mock-scheduledEventId-10', + targetSubjectId: 'mock-subject-1', submitId: 'mock-submitId-10', }, ], @@ -102,6 +100,7 @@ describe('Test ProgressDataCollector', () => { localEndDate: '2021-10-08', localEndTime: '01:02:05', scheduledEventId: 'mock-scheduledEventId-11', + targetSubjectId: 'mock-subject-1', submitId: 'mock-submitId-11', }, ], diff --git a/src/entities/event/model/operations/ScheduledDateCalculator.test.js b/src/entities/event/model/operations/ScheduledDateCalculator.test.ts similarity index 69% rename from src/entities/event/model/operations/ScheduledDateCalculator.test.js rename to src/entities/event/model/operations/ScheduledDateCalculator.test.ts index eaa6c428d..32164161d 100644 --- a/src/entities/event/model/operations/ScheduledDateCalculator.test.js +++ b/src/entities/event/model/operations/ScheduledDateCalculator.test.ts @@ -1,19 +1,28 @@ import { addDays, startOfDay, subDays } from 'date-fns'; -import ScheduledDateCalculator from './ScheduledDateCalculator'; - -const now = new Date(2024, 0, 25); +import { IScheduledDateCalculator } from './IScheduledDateCalculator'; +import { ScheduledDateCalculator } from './ScheduledDateCalculator'; +import { getDefaultScheduledDateCalculator } from './scheduledDateCalculatorInstance'; +import { + EventAvailability, + ScheduleEvent, +} from '../../lib/types/scheduledDateCalculator'; + +type TestScheduledDateCalculator = IScheduledDateCalculator & { + getNow: ScheduledDateCalculator['getNow']; +}; describe('ScheduledDateCalculator', () => { - let tempGetNow; + let now: Date; + let calculator: TestScheduledDateCalculator; - beforeAll(() => { - tempGetNow = ScheduledDateCalculator.getNow; - ScheduledDateCalculator.getNow = jest.fn().mockReturnValue(new Date(now)); - }); + beforeEach(() => { + now = new Date(2024, 0, 25, 0, 0, 0, 0); - afterAll(() => { - ScheduledDateCalculator.getNow = tempGetNow; + calculator = + getDefaultScheduledDateCalculator() as never as TestScheduledDateCalculator; + + jest.spyOn(calculator, 'getNow').mockReturnValue(now); }); describe('Test Always Available events', () => { @@ -25,7 +34,7 @@ describe('ScheduledDateCalculator', () => { timeTo: null, startDate: null, endDate: null, - }; + } as EventAvailability; const scheduleEvent = { id: 'eventTestId', @@ -33,18 +42,17 @@ describe('ScheduledDateCalculator', () => { availability: eventAvailability, selectedDate: null, scheduledAt: null, - }; + } as ScheduleEvent; - let resultDate = ScheduledDateCalculator.calculate(scheduleEvent, false); + let resultDate = calculator.calculate(scheduleEvent, false); let expectedDate = startOfDay(now); expect(resultDate).toEqual(expectedDate); - // sub-test 2 - with timeFrom eventAvailability.timeFrom = { hours: 16, minutes: 15 }; - resultDate = ScheduledDateCalculator.calculate(scheduleEvent, false); + resultDate = calculator.calculate(scheduleEvent, false); expectedDate = startOfDay(now); expectedDate.setHours(16); @@ -63,7 +71,7 @@ describe('ScheduledDateCalculator', () => { timeTo: { hours: 18, minutes: 30 }, startDate: null, endDate: null, - }; + } as EventAvailability; const scheduleEvent = { id: 'eventTestId', @@ -71,7 +79,7 @@ describe('ScheduledDateCalculator', () => { availability: eventAvailability, selectedDate: null, scheduledAt: null, - }; + } as ScheduleEvent; return scheduleEvent; }; @@ -80,7 +88,7 @@ describe('ScheduledDateCalculator', () => { const onceEvent = getOnceEvent(); onceEvent.selectedDate = subDays(startOfDay(now), 2); - const resultDate = ScheduledDateCalculator.calculate(onceEvent, false); + const resultDate = calculator.calculate(onceEvent, false); expect(resultDate).toEqual(null); }); @@ -89,11 +97,11 @@ describe('ScheduledDateCalculator', () => { const onceEvent = getOnceEvent(); onceEvent.selectedDate = subDays(startOfDay(now), 1); - const resultDate = ScheduledDateCalculator.calculate(onceEvent, false); + const resultDate = calculator.calculate(onceEvent, false); const expectedDate = new Date(onceEvent.selectedDate); - expectedDate.setHours(onceEvent.availability.timeFrom.hours); - expectedDate.setMinutes(onceEvent.availability.timeFrom.minutes); + expectedDate.setHours(onceEvent.availability.timeFrom!.hours); + expectedDate.setMinutes(onceEvent.availability.timeFrom!.minutes); expect(resultDate).toEqual(expectedDate); }); @@ -102,11 +110,11 @@ describe('ScheduledDateCalculator', () => { const onceEvent = getOnceEvent(); onceEvent.selectedDate = addDays(startOfDay(now), 1); - const resultDate = ScheduledDateCalculator.calculate(onceEvent, false); + const resultDate = calculator.calculate(onceEvent, false); const expectedDate = addDays(startOfDay(now), 1); - expectedDate.setHours(onceEvent.availability.timeFrom.hours); - expectedDate.setMinutes(onceEvent.availability.timeFrom.minutes); + expectedDate.setHours(onceEvent.availability.timeFrom!.hours); + expectedDate.setMinutes(onceEvent.availability.timeFrom!.minutes); expect(resultDate).toEqual(expectedDate); }); @@ -116,7 +124,7 @@ describe('ScheduledDateCalculator', () => { onceEvent.selectedDate = startOfDay(now); onceEvent.availability.timeFrom = { hours: 0, minutes: 0 }; - const resultDate = ScheduledDateCalculator.calculate(onceEvent, false); + const resultDate = calculator.calculate(onceEvent, false); const expectedDate = startOfDay(now); @@ -128,7 +136,7 @@ describe('ScheduledDateCalculator', () => { onceEvent.selectedDate = startOfDay(now); onceEvent.availability.timeFrom = { hours: 16, minutes: 30 }; - const resultDate = ScheduledDateCalculator.calculate(onceEvent, false); + const resultDate = calculator.calculate(onceEvent, false); const expectedDate = startOfDay(now); expectedDate.setHours(16); @@ -147,7 +155,7 @@ describe('ScheduledDateCalculator', () => { timeTo: { hours: 18, minutes: 30 }, startDate: null, endDate: null, - }; + } as EventAvailability; const scheduleEvent = { id: 'eventTestId', @@ -155,7 +163,7 @@ describe('ScheduledDateCalculator', () => { availability: eventAvailability, selectedDate: null, scheduledAt: null, - }; + } as ScheduleEvent; return scheduleEvent; }; @@ -166,7 +174,7 @@ describe('ScheduledDateCalculator', () => { event.availability.endDate = addDays(startOfDay(now), 2); event.availability.timeFrom = { hours: 0, minutes: 0 }; - const resultDate = ScheduledDateCalculator.calculate(event, false); + const resultDate = calculator.calculate(event, false); const expectedDate = startOfDay(now); @@ -178,11 +186,11 @@ describe('ScheduledDateCalculator', () => { event.availability.startDate = subDays(startOfDay(now), 2); event.availability.endDate = addDays(startOfDay(now), 2); - const resultDate = ScheduledDateCalculator.calculate(event, false); + const resultDate = calculator.calculate(event, false); const expectedDate = startOfDay(now); - expectedDate.setHours(event.availability.timeFrom.hours); - expectedDate.setMinutes(event.availability.timeFrom.minutes); + expectedDate.setHours(event.availability.timeFrom!.hours); + expectedDate.setMinutes(event.availability.timeFrom!.minutes); expect(resultDate).toEqual(expectedDate); }); @@ -192,11 +200,11 @@ describe('ScheduledDateCalculator', () => { event.availability.startDate = subDays(startOfDay(now), 2); event.availability.endDate = startOfDay(now); - const resultDate = ScheduledDateCalculator.calculate(event, false); + const resultDate = calculator.calculate(event, false); const expectedDate = startOfDay(now); - expectedDate.setHours(event.availability.timeFrom.hours); - expectedDate.setMinutes(event.availability.timeFrom.minutes); + expectedDate.setHours(event.availability.timeFrom!.hours); + expectedDate.setMinutes(event.availability.timeFrom!.minutes); expect(resultDate).toEqual(expectedDate); }); @@ -206,11 +214,11 @@ describe('ScheduledDateCalculator', () => { event.availability.startDate = startOfDay(now); event.availability.endDate = addDays(startOfDay(now), 2); - const resultDate = ScheduledDateCalculator.calculate(event, false); + const resultDate = calculator.calculate(event, false); const expectedDate = startOfDay(now); - expectedDate.setHours(event.availability.timeFrom.hours); - expectedDate.setMinutes(event.availability.timeFrom.minutes); + expectedDate.setHours(event.availability.timeFrom!.hours); + expectedDate.setMinutes(event.availability.timeFrom!.minutes); expect(resultDate).toEqual(expectedDate); }); @@ -220,11 +228,11 @@ describe('ScheduledDateCalculator', () => { event.availability.startDate = addDays(startOfDay(now), 2); event.availability.endDate = addDays(startOfDay(now), 5); - const resultDate = ScheduledDateCalculator.calculate(event, false); + const resultDate = calculator.calculate(event, false); const expectedDate = new Date(event.availability.startDate); - expectedDate.setHours(event.availability.timeFrom.hours); - expectedDate.setMinutes(event.availability.timeFrom.minutes); + expectedDate.setHours(event.availability.timeFrom!.hours); + expectedDate.setMinutes(event.availability.timeFrom!.minutes); expect(resultDate).toEqual(expectedDate); }); @@ -234,11 +242,11 @@ describe('ScheduledDateCalculator', () => { event.availability.startDate = subDays(startOfDay(now), 5); event.availability.endDate = subDays(startOfDay(now), 2); - const resultDate = ScheduledDateCalculator.calculate(event, false); + const resultDate = calculator.calculate(event, false); const expectedDate = new Date(event.availability.endDate); - expectedDate.setHours(event.availability.timeFrom.hours); - expectedDate.setMinutes(event.availability.timeFrom.minutes); + expectedDate.setHours(event.availability.timeFrom!.hours); + expectedDate.setMinutes(event.availability.timeFrom!.minutes); expect(resultDate).toEqual(expectedDate); }); @@ -253,7 +261,7 @@ describe('ScheduledDateCalculator', () => { timeTo: { hours: 18, minutes: 30 }, startDate: null, endDate: null, - }; + } as EventAvailability; const scheduleEvent = { id: 'eventTestId', @@ -261,7 +269,7 @@ describe('ScheduledDateCalculator', () => { availability: eventAvailability, selectedDate: null, scheduledAt: null, - }; + } as ScheduleEvent; return scheduleEvent; }; @@ -273,7 +281,7 @@ describe('ScheduledDateCalculator', () => { event.availability.timeFrom = { hours: 0, minutes: 0 }; event.selectedDate = startOfDay(now); - const resultDate = ScheduledDateCalculator.calculate(event, false); + const resultDate = calculator.calculate(event, false); const expectedDate = startOfDay(now); @@ -286,11 +294,11 @@ describe('ScheduledDateCalculator', () => { event.availability.endDate = addDays(startOfDay(now), 2); event.selectedDate = startOfDay(now); - const resultDate = ScheduledDateCalculator.calculate(event, false); + const resultDate = calculator.calculate(event, false); const expectedDate = startOfDay(now); - expectedDate.setHours(event.availability.timeFrom.hours); - expectedDate.setMinutes(event.availability.timeFrom.minutes); + expectedDate.setHours(event.availability.timeFrom!.hours); + expectedDate.setMinutes(event.availability.timeFrom!.minutes); expect(resultDate).toEqual(expectedDate); }); @@ -301,17 +309,17 @@ describe('ScheduledDateCalculator', () => { event.availability.endDate = addDays(startOfDay(now), 2); event.selectedDate = addDays(startOfDay(now), 7); - let resultDate = ScheduledDateCalculator.calculate(event, false); + let resultDate = calculator.calculate(event, false); const expectedDate = startOfDay(now); - expectedDate.setHours(event.availability.timeFrom.hours); - expectedDate.setMinutes(event.availability.timeFrom.minutes); + expectedDate.setHours(event.availability.timeFrom!.hours); + expectedDate.setMinutes(event.availability.timeFrom!.minutes); expect(resultDate).toEqual(expectedDate); event.selectedDate = subDays(startOfDay(now), 7); - resultDate = ScheduledDateCalculator.calculate(event, false); + resultDate = calculator.calculate(event, false); expect(resultDate).toEqual(expectedDate); }); @@ -322,17 +330,17 @@ describe('ScheduledDateCalculator', () => { event.availability.endDate = addDays(startOfDay(now), 5); event.selectedDate = addDays(startOfDay(now), 1); - let resultDate = ScheduledDateCalculator.calculate(event, false); + let resultDate = calculator.calculate(event, false); let expectedDate = addDays(startOfDay(now), 1); - expectedDate.setHours(event.availability.timeFrom.hours); - expectedDate.setMinutes(event.availability.timeFrom.minutes); + expectedDate.setHours(event.availability.timeFrom!.hours); + expectedDate.setMinutes(event.availability.timeFrom!.minutes); expect(resultDate).toEqual(expectedDate); event.availability.timeFrom = { hours: 0, minutes: 0 }; - resultDate = ScheduledDateCalculator.calculate(event, false); + resultDate = calculator.calculate(event, false); expectedDate = addDays(startOfDay(now), 1); @@ -345,17 +353,17 @@ describe('ScheduledDateCalculator', () => { event.availability.endDate = subDays(startOfDay(now), 1); event.selectedDate = subDays(startOfDay(now), 1); - let resultDate = ScheduledDateCalculator.calculate(event, false); + let resultDate = calculator.calculate(event, false); let expectedDate = subDays(startOfDay(now), 1); - expectedDate.setHours(event.availability.timeFrom.hours); - expectedDate.setMinutes(event.availability.timeFrom.minutes); + expectedDate.setHours(event.availability.timeFrom!.hours); + expectedDate.setMinutes(event.availability.timeFrom!.minutes); expect(resultDate).toEqual(expectedDate); event.availability.timeFrom = { hours: 0, minutes: 0 }; - resultDate = ScheduledDateCalculator.calculate(event, false); + resultDate = calculator.calculate(event, false); expectedDate = subDays(startOfDay(now), 1); @@ -368,11 +376,11 @@ describe('ScheduledDateCalculator', () => { event.availability.endDate = addDays(startOfDay(now), 10); event.selectedDate = addDays(startOfDay(now), 4); - const resultDate = ScheduledDateCalculator.calculate(event, false); + const resultDate = calculator.calculate(event, false); const expectedDate = subDays(startOfDay(now), 3); - expectedDate.setHours(event.availability.timeFrom.hours); - expectedDate.setMinutes(event.availability.timeFrom.minutes); + expectedDate.setHours(event.availability.timeFrom!.hours); + expectedDate.setMinutes(event.availability.timeFrom!.minutes); expect(resultDate).toEqual(expectedDate); }); @@ -383,11 +391,11 @@ describe('ScheduledDateCalculator', () => { event.availability.endDate = addDays(startOfDay(now), 10); event.selectedDate = subDays(startOfDay(now), 4); - const resultDate = ScheduledDateCalculator.calculate(event, false); + const resultDate = calculator.calculate(event, false); const expectedDate = subDays(startOfDay(now), 4); - expectedDate.setHours(event.availability.timeFrom.hours); - expectedDate.setMinutes(event.availability.timeFrom.minutes); + expectedDate.setHours(event.availability.timeFrom!.hours); + expectedDate.setMinutes(event.availability.timeFrom!.minutes); expect(resultDate).toEqual(expectedDate); }); @@ -398,18 +406,18 @@ describe('ScheduledDateCalculator', () => { event.availability.endDate = addDays(startOfDay(now), 10); event.selectedDate = subDays(startOfDay(now), 4); - let resultDate = ScheduledDateCalculator.calculate(event, false); + let resultDate = calculator.calculate(event, false); const expectedDate = addDays(subDays(startOfDay(now), 4), 7); - expectedDate.setHours(event.availability.timeFrom.hours); - expectedDate.setMinutes(event.availability.timeFrom.minutes); + expectedDate.setHours(event.availability.timeFrom!.hours); + expectedDate.setMinutes(event.availability.timeFrom!.minutes); expect(resultDate).toEqual(expectedDate); event.availability.startDate = addDays(startOfDay(now), 2); event.availability.endDate = addDays(startOfDay(now), 10); - resultDate = ScheduledDateCalculator.calculate(event, false); + resultDate = calculator.calculate(event, false); expect(resultDate).toEqual(expectedDate); }); @@ -420,7 +428,7 @@ describe('ScheduledDateCalculator', () => { event.availability.endDate = subDays(startOfDay(now), 3); event.selectedDate = subDays(startOfDay(now), 1); - const resultDate = ScheduledDateCalculator.calculate(event, false); + const resultDate = calculator.calculate(event, false); expect(resultDate).toEqual(null); }); @@ -431,7 +439,7 @@ describe('ScheduledDateCalculator', () => { event.availability.endDate = addDays(startOfDay(now), 5); event.selectedDate = subDays(startOfDay(now), 1); - const resultDate = ScheduledDateCalculator.calculate(event, false); + const resultDate = calculator.calculate(event, false); expect(resultDate).toEqual(null); }); @@ -446,7 +454,7 @@ describe('ScheduledDateCalculator', () => { timeTo: { hours: 18, minutes: 30 }, startDate: null, endDate: null, - }; + } as EventAvailability; const scheduleEvent = { id: 'eventTestId', @@ -454,22 +462,11 @@ describe('ScheduledDateCalculator', () => { availability: eventAvailability, selectedDate: null, scheduledAt: null, - }; + } as ScheduleEvent; return scheduleEvent; }; - let tempGetNow; - - beforeEach(() => { - tempGetNow = ScheduledDateCalculator.getNow; - }); - - afterEach(() => { - jest.clearAllMocks(); - ScheduledDateCalculator.getNow = tempGetNow; - }); - it('Should return Mon, Wed, Fri when getNow returns Mon, Wed, Fri accordingly and start-end dates are -/+ 30 days and timeFrom is reset or is set', () => { const event = getWeekdaysEvent(); @@ -481,25 +478,17 @@ describe('ScheduledDateCalculator', () => { event.availability.endDate = addDays(monday, 30); event.availability.timeFrom = { hours: 0, minutes: 0 }; - let getNowMock = jest.fn(() => { - return new Date(monday); - }); - - ScheduledDateCalculator.getNow = getNowMock; + jest.spyOn(calculator, 'getNow').mockReturnValue(new Date(monday)); - let resultDate = ScheduledDateCalculator.calculate(event, false); + let resultDate = calculator.calculate(event, false); expect(resultDate).toEqual(monday); event.availability.timeFrom = { hours: 15, minutes: 56 }; - getNowMock = jest.fn(() => { - return new Date(wednesday); - }); - - ScheduledDateCalculator.getNow = getNowMock; + jest.spyOn(calculator, 'getNow').mockReturnValue(new Date(wednesday)); - resultDate = ScheduledDateCalculator.calculate(event, false); + resultDate = calculator.calculate(event, false); const expected = new Date(wednesday); expected.setHours(15); @@ -509,13 +498,9 @@ describe('ScheduledDateCalculator', () => { event.availability.timeFrom = { hours: 0, minutes: 0 }; - getNowMock = jest.fn(() => { - return new Date(friday); - }); - - ScheduledDateCalculator.getNow = getNowMock; + jest.spyOn(calculator, 'getNow').mockReturnValue(new Date(friday)); - resultDate = ScheduledDateCalculator.calculate(event, false); + resultDate = calculator.calculate(event, false); expect(resultDate).toEqual(friday); }); @@ -530,29 +515,21 @@ describe('ScheduledDateCalculator', () => { event.availability.startDate = subDays(saturday, 30); event.availability.endDate = addDays(saturday, 30); - let getNowMock = jest.fn(() => { - return new Date(sunday); - }); + jest.spyOn(calculator, 'getNow').mockReturnValue(new Date(sunday)); - ScheduledDateCalculator.getNow = getNowMock; - - let resultDate = ScheduledDateCalculator.calculate(event, false); + let resultDate = calculator.calculate(event, false); const expectedDate = new Date(friday); - expectedDate.setHours(event.availability.timeFrom.hours); - expectedDate.setMinutes(event.availability.timeFrom.minutes); + expectedDate.setHours(event.availability.timeFrom!.hours); + expectedDate.setMinutes(event.availability.timeFrom!.minutes); expect(resultDate).toEqual(expectedDate); event.availability.timeFrom = { hours: 0, minutes: 0 }; - getNowMock = jest.fn(() => { - return new Date(saturday); - }); - - ScheduledDateCalculator.getNow = getNowMock; + jest.spyOn(calculator, 'getNow').mockReturnValue(new Date(saturday)); - resultDate = ScheduledDateCalculator.calculate(event, false); + resultDate = calculator.calculate(event, false); expect(resultDate).toEqual(friday); }); @@ -569,18 +546,18 @@ describe('ScheduledDateCalculator', () => { return new Date(wednesday); }); - ScheduledDateCalculator.getNow = getNowMock; + jest.spyOn(calculator, 'getNow').mockReturnValue(new Date(wednesday)); - let resultDate = ScheduledDateCalculator.calculate(event, false); + let resultDate = calculator.calculate(event, false); const expectedDate = new Date(wednesday); - expectedDate.setHours(event.availability.timeFrom.hours); - expectedDate.setMinutes(event.availability.timeFrom.minutes); + expectedDate.setHours(event.availability.timeFrom!.hours); + expectedDate.setMinutes(event.availability.timeFrom!.minutes); expect(resultDate).toEqual(expectedDate); event.availability.timeFrom = { hours: 0, minutes: 0 }; - resultDate = ScheduledDateCalculator.calculate(event, false); + resultDate = calculator.calculate(event, false); expect(resultDate).toEqual(wednesday); }); @@ -590,25 +567,21 @@ describe('ScheduledDateCalculator', () => { const wednesday = startOfDay(new Date(2023, 8, 27)); - event.availability.startDate = new subDays(new Date(wednesday), 30); + event.availability.startDate = subDays(wednesday, 30); event.availability.endDate = new Date(wednesday); - const getNowMock = jest.fn(() => { - return new Date(wednesday); - }); - - ScheduledDateCalculator.getNow = getNowMock; + jest.spyOn(calculator, 'getNow').mockReturnValue(new Date(wednesday)); - let resultDate = ScheduledDateCalculator.calculate(event, false); + let resultDate = calculator.calculate(event, false); const expectedDate = new Date(wednesday); - expectedDate.setHours(event.availability.timeFrom.hours); - expectedDate.setMinutes(event.availability.timeFrom.minutes); + expectedDate.setHours(event.availability.timeFrom!.hours); + expectedDate.setMinutes(event.availability.timeFrom!.minutes); expect(resultDate).toEqual(expectedDate); event.availability.timeFrom = { hours: 0, minutes: 0 }; - resultDate = ScheduledDateCalculator.calculate(event, false); + resultDate = calculator.calculate(event, false); expect(resultDate).toEqual(wednesday); }); @@ -618,25 +591,21 @@ describe('ScheduledDateCalculator', () => { const wednesday = startOfDay(new Date(2023, 8, 27)); - event.availability.startDate = new addDays(new Date(wednesday), 10); + event.availability.startDate = addDays(wednesday, 10); event.availability.endDate = addDays(wednesday, 30); - const getNowMock = jest.fn(() => { - return new Date(wednesday); - }); + jest.spyOn(calculator, 'getNow').mockReturnValue(new Date(wednesday)); - ScheduledDateCalculator.getNow = getNowMock; - - let resultDate = ScheduledDateCalculator.calculate(event, false); + let resultDate = calculator.calculate(event, false); let expectedDate = new Date(2023, 9, 9); - expectedDate.setHours(event.availability.timeFrom.hours); - expectedDate.setMinutes(event.availability.timeFrom.minutes); + expectedDate.setHours(event.availability.timeFrom!.hours); + expectedDate.setMinutes(event.availability.timeFrom!.minutes); expect(resultDate).toEqual(expectedDate); event.availability.timeFrom = { hours: 0, minutes: 0 }; - resultDate = ScheduledDateCalculator.calculate(event, false); + resultDate = calculator.calculate(event, false); expectedDate = new Date(2023, 9, 9); expect(resultDate).toEqual(expectedDate); @@ -647,25 +616,21 @@ describe('ScheduledDateCalculator', () => { const wednesday = startOfDay(new Date(2023, 8, 27)); - event.availability.startDate = new subDays(new Date(wednesday), 30); + event.availability.startDate = subDays(wednesday, 30); event.availability.endDate = subDays(wednesday, 10); - const getNowMock = jest.fn(() => { - return new Date(wednesday); - }); - - ScheduledDateCalculator.getNow = getNowMock; + jest.spyOn(calculator, 'getNow').mockReturnValue(new Date(wednesday)); - let resultDate = ScheduledDateCalculator.calculate(event, false); + let resultDate = calculator.calculate(event, false); let expectedDate = new Date(2023, 8, 15); - expectedDate.setHours(event.availability.timeFrom.hours); - expectedDate.setMinutes(event.availability.timeFrom.minutes); + expectedDate.setHours(event.availability.timeFrom!.hours); + expectedDate.setMinutes(event.availability.timeFrom!.minutes); expect(resultDate).toEqual(expectedDate); event.availability.timeFrom = { hours: 0, minutes: 0 }; - resultDate = ScheduledDateCalculator.calculate(event, false); + resultDate = calculator.calculate(event, false); expectedDate = new Date(2023, 8, 15); diff --git a/src/entities/notification/model/factory/tests/NotificationBuilder.processEvent.test.ts b/src/entities/notification/model/factory/tests/NotificationBuilder.processEvent.test.ts index 28e99e096..89b12c97b 100644 --- a/src/entities/notification/model/factory/tests/NotificationBuilder.processEvent.test.ts +++ b/src/entities/notification/model/factory/tests/NotificationBuilder.processEvent.test.ts @@ -76,7 +76,7 @@ const getMockNotification = (index = 1) => { appletId: 'mock-applet-id', entityName: 'mock-entity-name', eventId: 'mock-event-id', - targetSubjectId: 'mock-target-subject-id', + targetSubjectId: null, isActive: true, notificationBody: 'mock-notification-body', notificationHeader: 'mock-notification-header', diff --git a/src/entities/notification/model/factory/tests/NotificationUtility.cornerCases.test.ts b/src/entities/notification/model/factory/tests/NotificationUtility.cornerCases.test.ts index 5b97ab78c..82bf8d4ff 100644 --- a/src/entities/notification/model/factory/tests/NotificationUtility.cornerCases.test.ts +++ b/src/entities/notification/model/factory/tests/NotificationUtility.cornerCases.test.ts @@ -93,11 +93,7 @@ describe('NotificationUtility: test corner cases, rest of small functions', () = //@ts-expect-error utility.getProgressionCompletedAt = getProgressionCompletedAtMock; - utility.isCompleted( - 'mock-entity-id-1', - 'mock-event-id-1', - 'mock-target-subject-1', - ); + utility.isCompleted('mock-entity-id-1', 'mock-event-id-1', null); expect(getProgressionCompletedAtMock).toHaveBeenCalledTimes(1); }); diff --git a/src/entities/notification/model/factory/tests/NotificationUtility.markAsInactive.test.ts b/src/entities/notification/model/factory/tests/NotificationUtility.markAsInactive.test.ts index fce57a91b..6c16ae4e4 100644 --- a/src/entities/notification/model/factory/tests/NotificationUtility.markAsInactive.test.ts +++ b/src/entities/notification/model/factory/tests/NotificationUtility.markAsInactive.test.ts @@ -26,7 +26,7 @@ const getTestNotification = (): NotificationDescriber => { appletId: AppletId, entityName: 'mock-entity-name', eventId: 'mock-event-id', - targetSubjectId: 'mock-target-subject-id', + targetSubjectId: null, isActive: true, notificationBody: 'mock-n-body', notificationHeader: 'mock-n-header', @@ -61,7 +61,7 @@ describe('NotificationUtility: mark-as-inactive methods tests', () => { utility.markNotificationIfActivityCompleted( 'mock-entity-id', 'mock-event-id', - 'mock-target-subject-1', + null, notification, interval, ); @@ -99,7 +99,7 @@ describe('NotificationUtility: mark-as-inactive methods tests', () => { utility.markNotificationIfActivityCompleted( 'mock-entity-id', 'mock-event-id', - 'mock-target-subject-1', + null, notification, interval, ); @@ -141,7 +141,7 @@ describe('NotificationUtility: mark-as-inactive methods tests', () => { utility.markNotificationIfActivityCompleted( 'mock-entity-id', 'mock-event-id', - 'mock-target-subject-1', + null, notification, interval, ); @@ -183,7 +183,7 @@ describe('NotificationUtility: mark-as-inactive methods tests', () => { utility.markNotificationIfActivityCompleted( 'mock-entity-id', 'mock-event-id', - 'mock-target-subject-1', + null, notification, interval, ); @@ -221,7 +221,7 @@ describe('NotificationUtility: mark-as-inactive methods tests', () => { utility.markNotificationIfActivityCompleted( 'mock-entity-id', 'mock-event-id', - 'mock-target-subject-1', + null, notification, interval, ); @@ -260,7 +260,7 @@ describe('NotificationUtility: mark-as-inactive methods tests', () => { utility.markNotificationIfActivityCompleted( 'mock-entity-id', 'mock-event-id', - 'mock-target-subject-1', + null, notification, interval, ); diff --git a/src/entities/notification/model/factory/tests/testHelpers.ts b/src/entities/notification/model/factory/tests/testHelpers.ts index 5f229bf9b..8ecebeab8 100644 --- a/src/entities/notification/model/factory/tests/testHelpers.ts +++ b/src/entities/notification/model/factory/tests/testHelpers.ts @@ -53,11 +53,7 @@ export const getEventEntity = (event: ScheduleEvent): EventEntity => { isVisible: true, pipelineType: ActivityPipelineType.Regular, }, - assignment: { - target: { - id: 'mock-target-subject-id', - } as never, - } as never, + assignment: null, }; }; @@ -71,7 +67,7 @@ export const createBuilder = (eventEntity: EventEntity, completedAt?: Date) => { entityType: 'activity', entityId: 'mock-entity-id', eventId: 'mock-event-id', - targetSubjectId: 'mock-target-subject-id', + targetSubjectId: null, endedAtTimestamp: completedAt.getTime(), }; diff --git a/src/features/enter-foreground/model/hooks/useAnalyticsEventTrack.ts b/src/features/enter-foreground/model/hooks/useAnalyticsEventTrack.ts index dcdaf3a7c..934f27173 100644 --- a/src/features/enter-foreground/model/hooks/useAnalyticsEventTrack.ts +++ b/src/features/enter-foreground/model/hooks/useAnalyticsEventTrack.ts @@ -1,11 +1,9 @@ -import { - AnalyticsService, - MixEvents, -} from '@app/shared/lib/analytics/AnalyticsService'; +import { getDefaultAnalyticsService } from '@app/shared/lib/analytics/analyticsServiceInstance'; +import { MixEvents } from '@app/shared/lib/analytics/IAnalyticsService'; import { useOnForeground } from '@app/shared/lib/hooks/useOnForeground'; export function useAnalyticsEventTrack() { useOnForeground(() => { - AnalyticsService.track(MixEvents.AppReOpen); + getDefaultAnalyticsService().track(MixEvents.AppReOpen); }); } diff --git a/src/features/login/model/useAnalyticsAutoLogin.ts b/src/features/login/model/useAnalyticsAutoLogin.ts index 3e78a08c1..919097245 100644 --- a/src/features/login/model/useAnalyticsAutoLogin.ts +++ b/src/features/login/model/useAnalyticsAutoLogin.ts @@ -1,7 +1,7 @@ import { useEffect } from 'react'; import { selectUserId } from '@app/entities/identity/model/selectors'; -import { AnalyticsService } from '@app/shared/lib/analytics/AnalyticsService'; +import { getDefaultAnalyticsService } from '@app/shared/lib/analytics/analyticsServiceInstance'; import { useAppSelector } from '@app/shared/lib/hooks/redux'; export function useAnalyticsAutoLogin() { @@ -9,7 +9,7 @@ export function useAnalyticsAutoLogin() { useEffect(() => { if (id) { - AnalyticsService.login(id); + getDefaultAnalyticsService().login(id).catch(console.error); } }, [id]); } diff --git a/src/features/login/model/useFeatureFlagsAutoLogin.ts b/src/features/login/model/useFeatureFlagsAutoLogin.ts index c5676fb1b..d56c8b132 100644 --- a/src/features/login/model/useFeatureFlagsAutoLogin.ts +++ b/src/features/login/model/useFeatureFlagsAutoLogin.ts @@ -2,7 +2,7 @@ import { useEffect } from 'react'; import { selectUserId } from '@app/entities/identity/model/selectors'; import { useHasSession } from '@app/entities/session/model/hooks/useHasSession'; -import { FeatureFlagsService } from '@app/shared/lib/featureFlags/FeatureFlagsService'; +import { getDefaultFeatureFlagsService } from '@app/shared/lib/featureFlags/featureFlagsServiceInstance'; import { useAppSelector } from '@app/shared/lib/hooks/redux'; export function useFeatureFlagsAutoLogin() { @@ -13,6 +13,6 @@ export function useFeatureFlagsAutoLogin() { if (!hasSession || !id) { return; } - FeatureFlagsService.login(id); + getDefaultFeatureFlagsService().login(id); }, [id, hasSession]); } diff --git a/src/features/login/ui/LoginForm.tsx b/src/features/login/ui/LoginForm.tsx index e71c6f262..a845cd019 100644 --- a/src/features/login/ui/LoginForm.tsx +++ b/src/features/login/ui/LoginForm.tsx @@ -11,12 +11,10 @@ import { getDefaultUserPrivateKeyRecord } from '@app/entities/identity/lib/userP import { selectUserId } from '@app/entities/identity/model/selectors'; import { identityActions } from '@app/entities/identity/model/slice'; import { storeSession } from '@app/entities/session/model/operations'; -import { - AnalyticsService, - MixEvents, -} from '@app/shared/lib/analytics/AnalyticsService'; +import { getDefaultAnalyticsService } from '@app/shared/lib/analytics/analyticsServiceInstance'; +import { MixEvents } from '@app/shared/lib/analytics/IAnalyticsService'; import { getDefaultEncryptionManager } from '@app/shared/lib/encryption/encryptionManagerInstance'; -import { FeatureFlagsService } from '@app/shared/lib/featureFlags/FeatureFlagsService'; +import { getDefaultFeatureFlagsService } from '@app/shared/lib/featureFlags/featureFlagsServiceInstance'; import { useAppDispatch, useAppSelector } from '@app/shared/lib/hooks/redux'; import { useAppForm } from '@app/shared/lib/hooks/useAppForm'; import { useFormChanges } from '@app/shared/lib/hooks/useFormChanges'; @@ -79,11 +77,14 @@ export const LoginForm: FC = props => { storeSession(session); - AnalyticsService.login(user.id).then(() => { - AnalyticsService.track(MixEvents.LoginSuccessful); - }); + getDefaultAnalyticsService() + .login(user.id) + .then(() => { + getDefaultAnalyticsService().track(MixEvents.LoginSuccessful); + }) + .catch(console.error); - FeatureFlagsService.login(user.id); + getDefaultFeatureFlagsService().login(user.id).catch(console.error); props.onLoginSuccess(); }, diff --git a/src/features/logout/model/hooks.ts b/src/features/logout/model/hooks.ts index 7429fd7d7..71dab8426 100644 --- a/src/features/logout/model/hooks.ts +++ b/src/features/logout/model/hooks.ts @@ -8,8 +8,8 @@ import { getDefaultUserPrivateKeyRecord } from '@app/entities/identity/lib/userP import { getDefaultNotificationManager } from '@app/entities/notification/model/notificationManagerInstance'; import { getDefaultSessionService } from '@app/entities/session/lib/sessionServiceInstance'; import { getDefaultIdentityService } from '@app/shared/api/services/identityServiceInstance'; -import { AnalyticsService } from '@app/shared/lib/analytics/AnalyticsService'; -import { FeatureFlagsService } from '@app/shared/lib/featureFlags/FeatureFlagsService'; +import { getDefaultAnalyticsService } from '@app/shared/lib/analytics/analyticsServiceInstance'; +import { getDefaultFeatureFlagsService } from '@app/shared/lib/featureFlags/featureFlagsServiceInstance'; import { getDefaultSystemRecord } from '@app/shared/lib/records/systemRecordInstance'; import { Emitter } from '@app/shared/lib/services/Emitter'; import { getDefaultLogger } from '@app/shared/lib/services/loggerInstance'; @@ -38,9 +38,9 @@ export function useLogout() { ); } - AnalyticsService.logout(); + getDefaultAnalyticsService().logout(); - FeatureFlagsService.logout(); + getDefaultFeatureFlagsService().logout(); CacheManager.clearCache(); diff --git a/src/features/pass-survey/lib/hooks/useActivityStorageRecord.ts b/src/features/pass-survey/lib/hooks/useActivityStorageRecord.ts index 750f218f7..7df52b074 100644 --- a/src/features/pass-survey/lib/hooks/useActivityStorageRecord.ts +++ b/src/features/pass-survey/lib/hooks/useActivityStorageRecord.ts @@ -20,9 +20,9 @@ type UseActivityStorageArgs = { export type Answer = PipelineItemAnswer['value']; -type ZeroBasedIndex = string; +type ZeroBasedIndex = string | number; -export type Answers = Record; +export type Answers = Record | Array; type Timers = Record; diff --git a/src/features/pass-survey/lib/markdownVariableReplacer.test.js b/src/features/pass-survey/lib/markdownVariableReplacer.test.ts similarity index 87% rename from src/features/pass-survey/lib/markdownVariableReplacer.test.js rename to src/features/pass-survey/lib/markdownVariableReplacer.test.ts index 0c7be5cea..219e75955 100644 --- a/src/features/pass-survey/lib/markdownVariableReplacer.test.js +++ b/src/features/pass-survey/lib/markdownVariableReplacer.test.ts @@ -1,18 +1,22 @@ +import { Answers } from './hooks/useActivityStorageRecord'; import { MarkdownVariableReplacer } from './markdownVariableReplacer'; +import { PipelineItem } from './types/payload'; describe('MarkdownVariableReplacer', () => { it('should return the same markdown string when no variables are present', () => { - const activityItems = []; - const answers = []; - const replacer = new MarkdownVariableReplacer(activityItems, answers); + const activityItems = [] as PipelineItem[]; + const answers = {} as Answers; + const replacer = new MarkdownVariableReplacer(activityItems, answers, null); const markdown = 'This is some text'; expect(replacer.process(markdown)).toEqual(markdown); }); it('should return the markdown string with the variable replaced when only one variable is present', () => { - const activityItems = [{ name: 'foo', type: 'TextInput' }]; - const answers = [{ answer: 'bar' }]; - const replacer = new MarkdownVariableReplacer(activityItems, answers); + const activityItems = [ + { name: 'foo', type: 'TextInput' }, + ] as PipelineItem[]; + const answers = [{ answer: 'bar' }] as Answers; + const replacer = new MarkdownVariableReplacer(activityItems, answers, null); const markdown = 'This is some text [[foo]]'; const expected = 'This is some text bar'; expect(replacer.process(markdown)).toEqual(expected); @@ -32,48 +36,54 @@ describe('MarkdownVariableReplacer', () => { ], }, }, - ]; + ] as PipelineItem[]; const answers = [ { answer: 'abc' }, { answer: '2' }, { answer: { id: '1', text: 'Option 1' } }, - ]; - const replacer = new MarkdownVariableReplacer(activityItems, answers); + ] as Answers; + const replacer = new MarkdownVariableReplacer(activityItems, answers, null); const markdown = 'This is some text [[foo]] and [[bar]] and [[baz]]'; const expected = 'This is some text abc and 2 and Option 1'; expect(replacer.process(markdown)).toEqual(expected); }); it('should escape special characters in the answer', () => { - const activityItems = [{ name: 'foo', type: 'TextInput' }]; - const answers = [{ answer: '$10' }]; - const replacer = new MarkdownVariableReplacer(activityItems, answers); + const activityItems = [ + { name: 'foo', type: 'TextInput' }, + ] as PipelineItem[]; + const answers = [{ answer: '$10' }] as Answers; + const replacer = new MarkdownVariableReplacer(activityItems, answers, null); const markdown = 'This is some text [[foo]]'; const expected = 'This is some text \\$10'; expect(replacer.process(markdown)).toEqual(expected); }); it('should return the markdown string with the variable name when no answer is present', () => { - const activityItems = [{ name: 'foo', type: 'TextInput' }]; - const answers = []; - const replacer = new MarkdownVariableReplacer(activityItems, answers); + const activityItems = [ + { name: 'foo', type: 'TextInput' }, + ] as PipelineItem[]; + const answers = [] as Answers; + const replacer = new MarkdownVariableReplacer(activityItems, answers, null); const markdown = 'This is some text [[foo]]'; const expected = 'This is some text [[foo]]'; expect(replacer.process(markdown)).toEqual(expected); }); it('should return the markdown string with the variable name when the activity item is not found', () => { - const activityItems = [{ name: 'foo', type: 'TextInput' }]; - const answers = [{ answer: 'bar' }]; - const replacer = new MarkdownVariableReplacer(activityItems, answers); + const activityItems = [ + { name: 'foo', type: 'TextInput' }, + ] as PipelineItem[]; + const answers = [{ answer: 'bar' }] as Answers; + const replacer = new MarkdownVariableReplacer(activityItems, answers, null); const markdown = 'This is some text [[baz]]'; const expected = 'This is some text [[baz]]'; expect(replacer.process(markdown)).toEqual(expected); }); describe('process', () => { - let activityItems; - let answers; - let replacer; + let activityItems: PipelineItem[]; + let answers: Answers; + let replacer: MarkdownVariableReplacer; beforeEach(() => { activityItems = [ @@ -107,7 +117,8 @@ describe('MarkdownVariableReplacer', () => { type: 'TimeRange', payload: {}, }, - ]; + ] as never as PipelineItem[]; + answers = { 0: { answer: 'John Doe' }, 1: { @@ -137,8 +148,9 @@ describe('MarkdownVariableReplacer', () => { }, }, }, - }; - replacer = new MarkdownVariableReplacer(activityItems, answers); + } as never as Answers; + + replacer = new MarkdownVariableReplacer(activityItems, answers, null); }); it('should replace checkbox variable with selected options', () => { @@ -163,7 +175,7 @@ describe('MarkdownVariableReplacer', () => { }); it('should leave checkbox variable as is if answer not found', () => { - delete answers[1]; + delete answers[1 as never]; const markdown = 'Hello [[name1]], you selected [[name2]].'; const expectedOutput = 'Hello John Doe, you selected [[name2]].'; const processedMarkdown = replacer.process(markdown); @@ -178,7 +190,7 @@ describe('MarkdownVariableReplacer', () => { it('should leave markdown as is if no answers found', () => { answers = {}; - replacer = new MarkdownVariableReplacer(activityItems, answers); + replacer = new MarkdownVariableReplacer(activityItems, answers, null); const markdown = 'Hello [[name1]], you selected [[name2]].'; const expectedOutput = 'Hello [[name1]], you selected [[name2]].'; const processedMarkdown = replacer.process(markdown); @@ -249,7 +261,8 @@ describe('MarkdownVariableReplacer', () => { type: 'TextInput', payload: {}, }, - ]; + ] as PipelineItem[]; + const nestedAnswers = { 0: { answer: 'My name is John doe' }, 1: { @@ -270,11 +283,12 @@ describe('MarkdownVariableReplacer', () => { }, 3: { answer: 3 }, 4: { answer: '1' }, - }; + } as never as Answers; replacer = new MarkdownVariableReplacer( nestedActivityItems, nestedAnswers, + null, ); const markdown = `**ItemText content:** diff --git a/src/features/pass-survey/lib/markdownVariableReplacer.ts b/src/features/pass-survey/lib/markdownVariableReplacer.ts index a65197d79..293c39eb2 100644 --- a/src/features/pass-survey/lib/markdownVariableReplacer.ts +++ b/src/features/pass-survey/lib/markdownVariableReplacer.ts @@ -2,8 +2,13 @@ import { format, intervalToDuration, isSameDay, addDays } from 'date-fns'; import { Item } from '@app/shared/ui/survey/CheckBox/types'; -import { Answers } from './hooks/useActivityStorageRecord'; -import { PipelineItem, PipelineItemResponse } from './types/payload'; +import { Answer, Answers } from './hooks/useActivityStorageRecord'; +import { + PipelineItem, + PipelineItemResponse, + RadioResponse, + TimeRangeResponse, +} from './types/payload'; type Time = { hours: number; @@ -153,8 +158,10 @@ export class MarkdownVariableReplacer { const foundIndex = this.activityItems.findIndex( item => item.name === variableName, ); - const answerNotFound = foundIndex < 0 || !this.answers[foundIndex]; + const foundAnswerItem = this.answers[foundIndex as never] as Answer; + const foundAnswer = foundAnswerItem?.answer; + const answerNotFound = foundIndex < 0 || !foundAnswerItem || !foundAnswer; if (answerNotFound) { return `[[${variableName}]]`; } @@ -162,42 +169,38 @@ export class MarkdownVariableReplacer { const activityItem = this.activityItems[foundIndex]; let updated = ''; - const answer = // @ts-ignore - this.answers[foundIndex].answer as PipelineItemResponse['answer']; - - switch (activityItem.type) { - case 'Slider': - case 'NumberSelect': - case 'TextInput': - updated = this.escapeSpecialChars(answer); - break; - case 'Radio': - const filteredItem = activityItem.payload.options.find( - ({ id }) => id === answer.id, - ); - if (filteredItem) { - updated = filteredItem.text; - } - break; - case 'Checkbox': - const selectedIds = answer.map((o: Item) => o.id); - const filteredItems = activityItem.payload.options - .filter(({ id }) => selectedIds.includes(id)) - .map(({ text }) => text); - - if (filteredItems) { - updated = filteredItems.join(', '); - } - break; - case 'TimeRange': - updated = `${this.formatTime(answer?.startTime)} - ${this.formatTime( - answer?.endTime, - )}`; - break; - case 'Date': - updated = answer; - break; + if ( + activityItem.type === 'Slider' || + activityItem.type === 'NumberSelect' || + activityItem.type === 'TextInput' + ) { + updated = this.escapeSpecialChars(foundAnswer); + } else if (activityItem.type === 'Radio') { + const filteredItem = activityItem.payload.options.find( + ({ id }) => id === (foundAnswer as RadioResponse).id, + ); + if (filteredItem) { + updated = filteredItem.text; + } + } else if (activityItem.type === 'Checkbox') { + const selectedIds = (foundAnswer as Item[]).map((o: Item) => o.id); + const filteredItems = activityItem.payload.options + .filter(({ id }) => selectedIds.includes(id)) + .map(({ text }) => text); + + if (filteredItems) { + updated = filteredItems.join(', '); + } + } else if (activityItem.type === 'TimeRange') { + const startTime = + (foundAnswer as TimeRangeResponse).startTime || undefined; + const endTime = (foundAnswer as TimeRangeResponse).endTime || undefined; + + updated = `${this.formatTime(startTime)} - ${this.formatTime(endTime)}`; + } else if (activityItem.type === 'Date') { + updated = foundAnswer as string; } + const variablesLeftToProcess = this.extractVariables(updated); if (variablesLeftToProcess?.length) { diff --git a/src/features/pass-survey/model/streamEventMapper.ts b/src/features/pass-survey/model/streamEventMapper.ts index e7aad6c6c..356d1085d 100644 --- a/src/features/pass-survey/model/streamEventMapper.ts +++ b/src/features/pass-survey/model/streamEventMapper.ts @@ -68,8 +68,15 @@ const mapStabilityTrackerStreamEventToDto = ( return dto; }; -const mapFlankerStreamEventToDto = (streamEvent: FlankerLiveEvent) => { - if (IS_ANDROID) { +type MappingFlankerStreamEventToDtoOptions = { + isAndroid: boolean; +}; + +const mapFlankerStreamEventToDto = ( + streamEvent: FlankerLiveEvent, + options: MappingFlankerStreamEventToDtoOptions, +) => { + if (options.isAndroid) { let screenCountPerTrial = 1; if (streamEvent.showFeedback) { @@ -115,8 +122,19 @@ const mapFlankerStreamEventToDto = (streamEvent: FlankerLiveEvent) => { return dto; }; -export const mapStreamEventToDto = (streamEvent: LiveEvent): LiveEventDto => { +type MapStreamEventToDtoOptions = { + isAndroid?: boolean; +}; + +export const mapStreamEventToDto = ( + streamEvent: LiveEvent, + options?: MapStreamEventToDtoOptions, +): LiveEventDto => { const type = streamEvent.type; + let isAndroid = options?.isAndroid; + if (isAndroid === null || isAndroid === undefined) { + isAndroid = IS_ANDROID; + } switch (type) { case 'AbTest': @@ -124,7 +142,7 @@ export const mapStreamEventToDto = (streamEvent: LiveEvent): LiveEventDto => { case 'DrawingTest': return mapDrawingStreamEventToDto(streamEvent); case 'Flanker': - return mapFlankerStreamEventToDto(streamEvent); + return mapFlankerStreamEventToDto(streamEvent, { isAndroid }); case 'StabilityTracker': return mapStabilityTrackerStreamEventToDto(streamEvent); default: diff --git a/src/features/pass-survey/model/tests/streamEventMapper.test.ts b/src/features/pass-survey/model/tests/streamEventMapper.test.ts index 880d0efa4..441e89125 100644 --- a/src/features/pass-survey/model/tests/streamEventMapper.test.ts +++ b/src/features/pass-survey/model/tests/streamEventMapper.test.ts @@ -16,74 +16,67 @@ import { } from './streamEventMapper.input.mock.ts'; import { mapStreamEventToDto } from '../streamEventMapper.ts'; -jest.mock('@shared/lib', () => { - let IS_ANDROID = false; - - return { - get IS_ANDROID() { - return IS_ANDROID; - }, - set IS_ANDROID(value) { - IS_ANDROID = value; - }, - }; -}); - describe('Pass survey mapStreamEventToDto', () => { + let isAndroid: boolean; + + beforeEach(() => { + isAndroid = false; + }); + it('Should return mapped result for AbTrails item Live event', () => { - const result = mapStreamEventToDto(abTrailsInput); + const result = mapStreamEventToDto(abTrailsInput, { isAndroid }); expect(result).toEqual(abTrailsOutput); }); it('Should return mapped result for AbTrails item Live event for wrong input', () => { - const result = mapStreamEventToDto(abTrailsWrongInput); + const result = mapStreamEventToDto(abTrailsWrongInput, { isAndroid }); expect(result).toEqual(abTrailsWrongOutput); }); it('Should return mapped result for Drawing item Live event', () => { - const result = mapStreamEventToDto(drawingInput); + const result = mapStreamEventToDto(drawingInput, { isAndroid }); expect(result).toEqual(drawingOutput); }); it('Should return mapped result for StabilityTracker item Live event', () => { - const result = mapStreamEventToDto(CSTInput); + const result = mapStreamEventToDto(CSTInput, { isAndroid }); expect(result).toEqual(CSTOutput); }); it('Should return mapped result for Flanker android item Live event', () => { - require('@shared/lib').IS_ANDROID = true; + isAndroid = true; - const result = mapStreamEventToDto(FlankerAndroidInput); + const result = mapStreamEventToDto(FlankerAndroidInput, { + isAndroid, + }); expect(result).toEqual(FlankerAndroidOutput); }); it('Should return mapped result for Flanker android item Live event with empty duration field', () => { - require('@shared/lib').IS_ANDROID = true; + isAndroid = true; const result = mapStreamEventToDto( FlankerAndroidInputWithEmptyDurationInput, + { isAndroid }, ); expect(result).toEqual(FlankerAndroidInputWithEmptyDurationOutput); }); it('Should return mapped result for Flanker IOS item Live event', () => { - require('@shared/lib').IS_ANDROID = false; - - const result = mapStreamEventToDto(FlankerIOSInput); + const result = mapStreamEventToDto(FlankerIOSInput, { isAndroid }); expect(result).toEqual(FlankerIOSOutput); }); it('Should return mapped result for Undefined item type', () => { - const result = mapStreamEventToDto({ - // @ts-expect-error - type: 'Undefined', + const result = mapStreamEventToDto({ type: 'Undefined' } as never, { + isAndroid, }); expect(result).toEqual({ type: 'Undefined' }); diff --git a/src/features/send-application-logs/ui/SendApplicationLogsForm.test.tsx b/src/features/send-application-logs/ui/SendApplicationLogsForm.test.tsx index 563340115..855a2b97b 100644 --- a/src/features/send-application-logs/ui/SendApplicationLogsForm.test.tsx +++ b/src/features/send-application-logs/ui/SendApplicationLogsForm.test.tsx @@ -1,6 +1,8 @@ import { render, fireEvent, screen } from '@testing-library/react-native'; import { TamaguiProvider } from '@app/app/ui/AppProvider/TamaguiProvider'; +import { getDefaultLogger } from '@app/shared/lib/services/loggerInstance'; +import { ILogger } from '@app/shared/lib/types/logger'; import { wait } from '@app/shared/lib/utils/common'; import { SendApplicationLogsForm } from './SendApplicationLogsForm'; @@ -11,24 +13,23 @@ jest.mock('react-i18next', () => ({ })), })); -// eslint-disable-next-line @typescript-eslint/no-unsafe-return -jest.mock('@shared/lib/analytics', () => ({ - ...jest.requireActual('@shared/lib/analytics'), - AnalyticsService: { - track: jest.fn(), - }, -})); +describe('Test SendApplicationLogsForm (basic positive tests)', () => { + let logger: ILogger; -jest.mock('@shared/lib/services', () => ({ - Logger: { - send: jest.fn().mockImplementation(async () => { + beforeEach(() => { + logger = getDefaultLogger(); + + jest.spyOn(logger, 'send').mockImplementation(async () => { await wait(100); return true; - }), - }, -})); + }); + + jest.spyOn(logger, 'log').mockReturnValue(undefined); + jest.spyOn(logger, 'info').mockReturnValue(undefined); + jest.spyOn(logger, 'warn').mockReturnValue(undefined); + jest.spyOn(logger, 'error').mockReturnValue(undefined); + }); -describe('Test SendApplicationLogsForm (basic positive tests)', () => { afterEach(() => { screen.unmount(); }); diff --git a/src/features/send-application-logs/ui/SendApplicationLogsForm.tsx b/src/features/send-application-logs/ui/SendApplicationLogsForm.tsx index c6fdec7b2..a2bcfb179 100644 --- a/src/features/send-application-logs/ui/SendApplicationLogsForm.tsx +++ b/src/features/send-application-logs/ui/SendApplicationLogsForm.tsx @@ -2,10 +2,8 @@ import { FC, useState } from 'react'; import { useTranslation } from 'react-i18next'; -import { - AnalyticsService, - MixEvents, -} from '@app/shared/lib/analytics/AnalyticsService'; +import { getDefaultAnalyticsService } from '@app/shared/lib/analytics/analyticsServiceInstance'; +import { MixEvents } from '@app/shared/lib/analytics/IAnalyticsService'; import { getDefaultLogger } from '@app/shared/lib/services/loggerInstance'; import { ActivityIndicator } from '@app/shared/ui/ActivityIndicator'; import { Box } from '@app/shared/ui/base'; @@ -22,14 +20,14 @@ export const SendApplicationLogsForm: FC = () => { const onSendLogs = async () => { setIsLoading(true); - AnalyticsService.track(MixEvents.UploadLogsPressed); + getDefaultAnalyticsService().track(MixEvents.UploadLogsPressed); const result = await getDefaultLogger().send(); if (result) { - AnalyticsService.track(MixEvents.UploadedLogsSuccessfully); + getDefaultAnalyticsService().track(MixEvents.UploadedLogsSuccessfully); } else { - AnalyticsService.track(MixEvents.UploadLogsError); + getDefaultAnalyticsService().track(MixEvents.UploadLogsError); } setUploadStatus(result); diff --git a/src/features/sign-up/model/hooks/useRegistrationMutation.ts b/src/features/sign-up/model/hooks/useRegistrationMutation.ts index 68b5f274e..aa90e1af7 100644 --- a/src/features/sign-up/model/hooks/useRegistrationMutation.ts +++ b/src/features/sign-up/model/hooks/useRegistrationMutation.ts @@ -4,10 +4,8 @@ import { getDefaultUserInfoRecord } from '@app/entities/identity/lib/userInfoRec import { getDefaultUserPrivateKeyRecord } from '@app/entities/identity/lib/userPrivateKeyRecordInstance'; import { identityActions } from '@app/entities/identity/model/slice'; import { storeSession } from '@app/entities/session/model/operations'; -import { - AnalyticsService, - MixEvents, -} from '@app/shared/lib/analytics/AnalyticsService'; +import { getDefaultAnalyticsService } from '@app/shared/lib/analytics/analyticsServiceInstance'; +import { MixEvents } from '@app/shared/lib/analytics/IAnalyticsService'; import { getDefaultEncryptionManager } from '@app/shared/lib/encryption/encryptionManagerInstance'; import { useAppDispatch } from '@app/shared/lib/hooks/redux'; import { getDefaultSystemRecord } from '@app/shared/lib/records/systemRecordInstance'; @@ -49,9 +47,12 @@ export const useRegistrationMutation = ( storeSession(session); - AnalyticsService.login(user.id).then(() => { - AnalyticsService.track(MixEvents.SignupSuccessful); - }); + getDefaultAnalyticsService() + .login(user.id) + .then(() => { + getDefaultAnalyticsService().track(MixEvents.SignupSuccessful); + }) + .catch(console.error); if (onSuccess) { onSuccess(); diff --git a/src/features/tap-on-notification/model/hooks/useOnNotificationTap.ts b/src/features/tap-on-notification/model/hooks/useOnNotificationTap.ts index 51bd3ea73..e9d1aac91 100644 --- a/src/features/tap-on-notification/model/hooks/useOnNotificationTap.ts +++ b/src/features/tap-on-notification/model/hooks/useOnNotificationTap.ts @@ -35,11 +35,11 @@ import { import { getDefaultNotificationRefreshService } from '@app/entities/notification/model/notificationRefreshServiceInstance'; import { LogTrigger } from '@app/shared/api/services/INotificationService'; import { QueryDataUtils } from '@app/shared/api/services/QueryDataUtils'; +import { getDefaultAnalyticsService } from '@app/shared/lib/analytics/analyticsServiceInstance'; import { - AnalyticsService, MixEvents, MixProperties, -} from '@app/shared/lib/analytics/AnalyticsService'; +} from '@app/shared/lib/analytics/IAnalyticsService'; import { useAppSelector } from '@app/shared/lib/hooks/redux'; import { useCurrentRoute } from '@app/shared/lib/hooks/useCurrentRoute'; import { useToast } from '@app/shared/lib/hooks/useToast'; @@ -143,7 +143,7 @@ export function useOnNotificationTap({ const isAutocompletionWorking = getCurrentRoute() === 'Autocompletion'; - AnalyticsService.track(MixEvents.NotificationTap, { + getDefaultAnalyticsService().track(MixEvents.NotificationTap, { [MixProperties.AppletId]: appletId, }); diff --git a/src/screens/model/checkEntityAvailability.ts b/src/screens/model/checkEntityAvailability.ts index 33de60b8d..f56cb6a26 100644 --- a/src/screens/model/checkEntityAvailability.ts +++ b/src/screens/model/checkEntityAvailability.ts @@ -107,10 +107,10 @@ const checkEntityAvailabilityInternal = ({ return; } - const isAvailable = new AvailableGroupEvaluator(appletId).isEventInGroup( - event, - targetSubjectId, - ); + const isAvailable = new AvailableGroupEvaluator( + appletId, + entityProgressions, + ).isEventInGroup(event, targetSubjectId); const isScheduled = new ScheduledGroupEvaluator( appletId, diff --git a/src/screens/ui/ActivityListScreen.tsx b/src/screens/ui/ActivityListScreen.tsx index 2e30ca75c..42197932b 100644 --- a/src/screens/ui/ActivityListScreen.tsx +++ b/src/screens/ui/ActivityListScreen.tsx @@ -8,11 +8,11 @@ import { AutocompletionEventOptions } from '@app/abstract/lib/types/autocompleti import { EntityPath } from '@app/abstract/lib/types/entity'; import { selectAppletsEntityProgressions } from '@app/entities/applet/model/selectors'; import { ConnectionStatusBar } from '@app/features/streaming/ui/ConnectionStatusBar'; +import { getDefaultAnalyticsService } from '@app/shared/lib/analytics/analyticsServiceInstance'; import { - AnalyticsService, MixEvents, MixProperties, -} from '@app/shared/lib/analytics/AnalyticsService'; +} from '@app/shared/lib/analytics/IAnalyticsService'; import { useAppSelector } from '@app/shared/lib/hooks/redux'; import { useOnFocus } from '@app/shared/lib/hooks/useOnFocus'; import { Emitter } from '@app/shared/lib/services/Emitter'; @@ -33,7 +33,7 @@ export const ActivityListScreen: FC = props => { const isFocused = useIsFocused(); useOnFocus(() => { - AnalyticsService.track(MixEvents.AppletView, { + getDefaultAnalyticsService().track(MixEvents.AppletView, { [MixProperties.AppletId]: appletId, }); }); diff --git a/src/screens/ui/AppletDataScreen.tsx b/src/screens/ui/AppletDataScreen.tsx index fd24fb1cd..4a5a7d028 100644 --- a/src/screens/ui/AppletDataScreen.tsx +++ b/src/screens/ui/AppletDataScreen.tsx @@ -4,11 +4,11 @@ import { BottomTabScreenProps } from '@react-navigation/bottom-tabs'; import { useAppletAnalytics } from '@app/entities/applet/lib/hooks/useAppletAnalytics'; import { ActivityAnalyticsList } from '@app/entities/applet/ui/ActivityAnalyticsList'; +import { getDefaultAnalyticsService } from '@app/shared/lib/analytics/analyticsServiceInstance'; import { - AnalyticsService, MixEvents, MixProperties, -} from '@app/shared/lib/analytics/AnalyticsService'; +} from '@app/shared/lib/analytics/IAnalyticsService'; import { useOnFocus } from '@app/shared/lib/hooks/useOnFocus'; import { ActivityIndicator } from '@app/shared/ui/ActivityIndicator'; import { Box } from '@app/shared/ui/base'; @@ -27,7 +27,7 @@ export const AppletDataScreen: FC = ({ route }) => { const { analytics, isLoading } = useAppletAnalytics(appletId); useOnFocus(() => { - AnalyticsService.track(MixEvents.DataView, { + getDefaultAnalyticsService().track(MixEvents.DataView, { [MixProperties.AppletId]: appletId, }); }); diff --git a/src/screens/ui/AppletsScreen.tsx b/src/screens/ui/AppletsScreen.tsx index e3fce6e04..d0c5ce6da 100644 --- a/src/screens/ui/AppletsScreen.tsx +++ b/src/screens/ui/AppletsScreen.tsx @@ -15,11 +15,11 @@ import { getDefaultNotificationRefreshService } from '@app/entities/notification import { useAutomaticRefreshOnMount } from '@app/features/applets-refresh/model/hooks/useAutomaticRefreshOnMount'; import { AppletsRefresh } from '@app/features/applets-refresh/ui/AppletsRefresh'; import { LogTrigger } from '@app/shared/api/services/INotificationService'; +import { getDefaultAnalyticsService } from '@app/shared/lib/analytics/analyticsServiceInstance'; import { - AnalyticsService, MixEvents, MixProperties, -} from '@app/shared/lib/analytics/AnalyticsService'; +} from '@app/shared/lib/analytics/IAnalyticsService'; import { useAppSelector } from '@app/shared/lib/hooks/redux'; import { useOnFocus } from '@app/shared/lib/hooks/useOnFocus'; import { getDefaultLogger } from '@app/shared/lib/services/loggerInstance'; @@ -44,7 +44,7 @@ export const AppletsScreen: FC = () => { }, [t, userFirstName, setOptions]); useOnFocus(() => { - AnalyticsService.track(MixEvents.HomeView); + getDefaultAnalyticsService().track(MixEvents.HomeView); }); const queryClient = useQueryClient(); @@ -57,7 +57,7 @@ export const AppletsScreen: FC = () => { ({ id, displayName }) => { navigate('AppletDetails', { appletId: id, title: displayName }); - AnalyticsService.track(MixEvents.AppletSelected, { + getDefaultAnalyticsService().track(MixEvents.AppletSelected, { [MixProperties.AppletId]: id, }); }, diff --git a/src/shared/lib/analytics/AnalyticsService.test.ts b/src/shared/lib/analytics/AnalyticsService.test.ts index 859dd5890..62bd51b35 100644 --- a/src/shared/lib/analytics/AnalyticsService.test.ts +++ b/src/shared/lib/analytics/AnalyticsService.test.ts @@ -1,181 +1,204 @@ -const constructorMock = jest.fn(); -const initMock = jest.fn(); -const trackMock = jest.fn(); -const identifyMock = jest.fn().mockResolvedValue(0); -const resetMock = jest.fn(); - -export class MixpanelMockClass { - constructor(...args: any) { - constructorMock(args); +import { MMKV } from 'react-native-mmkv'; + +import { AnalyticsService } from './AnalyticsService'; +import { getDefaultAnalyticsService } from './analyticsServiceInstance'; +import { + IAnalyticsService, + MixEvents, + MixProperties, +} from './IAnalyticsService'; +import { MixpanelAnalytics } from './MixpanelAnalytics'; +import { ILogger } from '../types/logger'; + +class TestMixpanelClient { + static constructorSpy: jest.Mock; + static initSpy: jest.Mock; + static identifySpy: jest.Mock; + static trackSpy: jest.Mock; + static resetSpy: jest.Mock; + + constructor(...args: unknown[]) { + if (TestMixpanelClient.constructorSpy) { + TestMixpanelClient.constructorSpy(...args); + } } - public init(...args: any[]) { - initMock(args); + init(...args: unknown[]) { + if (TestMixpanelClient.initSpy) { + TestMixpanelClient.initSpy(...args); + } } - public track(...args: any[]) { - trackMock(args); + identify(...args: unknown[]) { + if (TestMixpanelClient.identifySpy) { + TestMixpanelClient.identifySpy(...args); + } } - public identify(...args: any[]) { - return identifyMock(args); + track(...args: unknown[]) { + if (TestMixpanelClient.trackSpy) { + TestMixpanelClient.trackSpy(...args); + } } - public getPeople() { - return { set: jest.fn() }; + getPeople() { + return { + set: () => {}, + }; } - public reset() { - resetMock(); + reset(...args: unknown[]) { + if (TestMixpanelClient.resetSpy) { + TestMixpanelClient.resetSpy(...args); + } } } -jest.mock('mixpanel-react-native', () => ({ - Mixpanel: MixpanelMockClass, -})); +type TestAnalyticsService = IAnalyticsService & { + logger: ILogger; + provider: MixpanelAnalytics | undefined; + analyticsStorage: MMKV; + shouldEnableMixpanel: AnalyticsService['shouldEnableMixpanel']; + getMixpanelToken: AnalyticsService['getMixpanelToken']; +}; const MOCK_MIXPANEL_TOKEN: string | undefined = 'MOCK_MIXPANEL_TOKEN_123'; - const MOCK_APP_VERSION = 'MOCK_APP_VERSION_456'; -jest.mock('../constants', () => ({ - MIXPANEL_TOKEN: MOCK_MIXPANEL_TOKEN, - APP_VERSION: MOCK_APP_VERSION, -})); - -jest.mock('../services', () => ({ - Logger: { - log: jest.fn(), - warn: jest.fn(), - error: jest.fn(), - info: jest.fn(), - }, -})); - -const storageSetMock = jest.fn(); -const clearAllMock = jest.fn(); - -jest.mock('../storages', () => ({ - createStorage: jest.fn().mockReturnValue({ - getBoolean: jest.fn().mockReturnValue(false), - set: storageSetMock, - clearAll: clearAllMock, - }), -})); - -import { AnalyticsService, MixEvents, MixProperties } from './AnalyticsService'; - describe('Test AnalyticsService and MixpanelAnalytics', () => { - beforeAll(() => { - AnalyticsService.shouldEnableMixpanel = jest.fn().mockReturnValue(true); - }); + let service: TestAnalyticsService; beforeEach(() => { - constructorMock.mockReset(); - initMock.mockReset(); - identifyMock.mockReset().mockResolvedValue(0); - trackMock.mockReset(); - storageSetMock.mockReset(); - resetMock.mockReset(); + TestMixpanelClient.constructorSpy = jest.fn(); + TestMixpanelClient.initSpy = jest.fn(); + TestMixpanelClient.identifySpy = jest.fn(); + TestMixpanelClient.trackSpy = jest.fn(); + TestMixpanelClient.resetSpy = jest.fn(); + + jest + .spyOn(MixpanelAnalytics.prototype as never, 'getMixpanelClientClass') + .mockReturnValue(TestMixpanelClient as never); + + jest + .spyOn(MixpanelAnalytics.prototype as never, 'getAppVersion') + .mockReturnValue(MOCK_APP_VERSION as never); + + service = getDefaultAnalyticsService() as never as TestAnalyticsService; + service.provider = undefined; + + jest.spyOn(service, 'shouldEnableMixpanel').mockReturnValue(true); + + jest + .spyOn(service, 'getMixpanelToken') + .mockReturnValue(MOCK_MIXPANEL_TOKEN); + + jest.spyOn(service.logger, 'log').mockReturnValue(undefined); }); it('Should pass MIXPANEL_TOKEN into constructor of Mixpanel class', async () => { - await AnalyticsService.init(); + await service.init(); - expect(constructorMock).toHaveBeenCalledTimes(1); - expect(constructorMock).toHaveBeenCalledWith([MOCK_MIXPANEL_TOKEN, false]); + expect(TestMixpanelClient.constructorSpy).toHaveBeenCalledTimes(1); + expect(TestMixpanelClient.constructorSpy).toHaveBeenCalledWith( + MOCK_MIXPANEL_TOKEN, + false, + ); }); it('Should pass APP_VERSION into init of Mixpanel instance', async () => { - await AnalyticsService.init(); + await service.init(); - expect(initMock).toHaveBeenCalledTimes(1); - expect(initMock).toHaveBeenCalledWith([ - undefined, - { 'MindLogger Version': `${MOCK_APP_VERSION}` }, - ]); + expect(TestMixpanelClient.initSpy).toHaveBeenCalledTimes(1); + expect(TestMixpanelClient.initSpy).toHaveBeenCalledWith(undefined, { + 'MindLogger Version': MOCK_APP_VERSION, + }); }); it('Should login', async () => { - await AnalyticsService.init(); + const setSpy = jest.spyOn(service.analyticsStorage, 'set'); + + await service.init(); - await AnalyticsService.login('mock-user-id'); + await service.login('mock-user-id'); - expect(identifyMock).toHaveBeenCalledTimes(1); - expect(storageSetMock).toHaveBeenCalledTimes(1); - expect(storageSetMock).toHaveBeenCalledWith('IS_LOGGED_IN', true); + expect(TestMixpanelClient.identifySpy).toHaveBeenCalledTimes(1); + expect(TestMixpanelClient.identifySpy).toHaveBeenCalledWith('mock-user-id'); + expect(setSpy).toHaveBeenCalledTimes(1); + expect(setSpy).toHaveBeenCalledWith('IS_LOGGED_IN', true); }); it('Should track without params', async () => { - await AnalyticsService.init(); + await service.init(); - await AnalyticsService.track(MixEvents.AssessmentStarted); + service.track(MixEvents.AssessmentStarted); - expect(trackMock).toBeCalledTimes(1); - expect(trackMock).toBeCalledWith([ + expect(TestMixpanelClient.trackSpy).toHaveBeenCalledTimes(1); + expect(TestMixpanelClient.trackSpy).toHaveBeenCalledWith( '[Mobile] Assessment started', undefined, - ]); + ); }); it('Should track with two params', async () => { - await AnalyticsService.init(); + await service.init(); - await AnalyticsService.track(MixEvents.AssessmentStarted, { + service.track(MixEvents.AssessmentStarted, { [MixProperties.AppletId]: 'mock-applet-id', [MixProperties.MindLoggerVersion]: 'mock-version', }); - expect(trackMock).toBeCalledTimes(1); - expect(trackMock).toBeCalledWith([ + expect(TestMixpanelClient.trackSpy).toHaveBeenCalledTimes(1); + expect(TestMixpanelClient.trackSpy).toHaveBeenCalledWith( '[Mobile] Assessment started', { 'Applet ID': 'mock-applet-id', 'MindLogger Version': 'mock-version' }, - ]); + ); }); it('Should logout', async () => { - await AnalyticsService.init(); + const clearAllSpy = jest.spyOn(service.analyticsStorage, 'clearAll'); - await AnalyticsService.logout(); + await service.init(); - expect(trackMock).toBeCalledTimes(1); - expect(trackMock).toBeCalledWith(['[Mobile] Logout', undefined]); + service.logout(); - expect(resetMock).toBeCalledTimes(1); - expect(clearAllMock).toBeCalledTimes(1); - }); -}); + expect(TestMixpanelClient.trackSpy).toHaveBeenCalledTimes(1); + expect(TestMixpanelClient.trackSpy).toHaveBeenCalledWith( + '[Mobile] Logout', + undefined, + ); -describe('Test AnalyticsService and MixpanelAnalytics when Mixpanel instance is not created', () => { - beforeAll(() => { - AnalyticsService.shouldEnableMixpanel = jest.fn().mockReturnValue(false); + expect(TestMixpanelClient.resetSpy).toHaveBeenCalledTimes(1); + expect(clearAllSpy).toHaveBeenCalledTimes(1); }); - beforeEach(() => { - constructorMock.mockReset(); - initMock.mockReset(); - identifyMock.mockReset().mockResolvedValue(0); - trackMock.mockReset(); - storageSetMock.mockReset(); - resetMock.mockReset(); - }); + describe('when Mixpanel instance is not created', () => { + beforeEach(() => { + jest.spyOn(service, 'shouldEnableMixpanel').mockReturnValue(false); + }); - it('Should not login', async () => { - await AnalyticsService.login('mock-user-id'); + it('Should not login', async () => { + await service.init(); - expect(identifyMock).toHaveBeenCalledTimes(0); - }); + await service.login('mock-user-id'); - it('Should not track', async () => { - await AnalyticsService.track(MixEvents.AssessmentStarted); + expect(TestMixpanelClient.identifySpy).toHaveBeenCalledTimes(0); + }); - expect(trackMock).toBeCalledTimes(0); - }); + it('Should not track', async () => { + await service.init(); + + service.track(MixEvents.AssessmentStarted); + + expect(TestMixpanelClient.trackSpy).toHaveBeenCalledTimes(0); + }); + + it('Should not logout', async () => { + await service.init(); - it('Should not logout', async () => { - await AnalyticsService.logout(); + service.logout(); - expect(trackMock).toBeCalledTimes(0); - expect(resetMock).toBeCalledTimes(0); + expect(TestMixpanelClient.trackSpy).toHaveBeenCalledTimes(0); + expect(TestMixpanelClient.trackSpy).toHaveBeenCalledTimes(0); + }); }); }); diff --git a/src/shared/lib/analytics/AnalyticsService.ts b/src/shared/lib/analytics/AnalyticsService.ts index 033b59350..0a5fdbdfd 100644 --- a/src/shared/lib/analytics/AnalyticsService.ts +++ b/src/shared/lib/analytics/AnalyticsService.ts @@ -1,85 +1,75 @@ -import { getDefaultStorageInstanceManager } from '@app/shared/lib/storages/storageInstanceManagerInstance'; +import { MMKV } from 'react-native-mmkv'; import { IAnalyticsService } from './IAnalyticsService'; import { MixpanelAnalytics } from './MixpanelAnalytics'; import { MIXPANEL_TOKEN } from '../constants'; -import { getDefaultLogger } from '../services/loggerInstance'; +import { ILogger } from '../types/logger'; -let service: IAnalyticsService; +export class AnalyticsService implements IAnalyticsService { + private logger: ILogger; + private analyticsStorage: MMKV; + private provider: MixpanelAnalytics | undefined; -export const MixProperties = { - AppletId: 'Applet ID', - MindLoggerVersion: 'MindLogger Version', - SubmitId: 'Submit ID', -}; + constructor(logger: ILogger, analyticsStorage: MMKV) { + this.logger = logger; + this.analyticsStorage = analyticsStorage; + this.provider = undefined; + } -export const MixEvents = { - DataView: 'Data View', - AppletView: 'Applet View', - HomeView: 'Home Page View', - AssessmentStarted: 'Assessment started', - AssessmentCompleted: 'Assessment completed', - RetryButtonPressed: 'Retry button pressed', - LoginSuccessful: 'Login Successful', - SignupSuccessful: 'Signup Successful', - AppOpen: 'App Open', - AppReOpen: 'App Re-Open', - ActivityRestart: 'Activity Restart Button Pressed', - ActivityResume: 'Activity Resume Button Pressed', - AppletSelected: 'Applet Selected', - ReturnToActivitiesPressed: 'Return to Activities pressed', - UploadLogsPressed: 'Upload Logs Pressed', - UploadedLogsSuccessfully: 'Uploaded Logs Successfully', - UploadLogsError: 'Upload Logs Error Occurred', - NotificationTap: 'Notification tap', -}; - -export const AnalyticsService = { - shouldEnableMixpanel() { - return !!MIXPANEL_TOKEN; - }, async init(): Promise { if (this.shouldEnableMixpanel()) { - getDefaultLogger().log( - '[AnalyticsService]: Create and init MixpanelAnalytics object', - ); - service = new MixpanelAnalytics(MIXPANEL_TOKEN!); - return service.init(); + if (!this.provider) { + this.logger.log('[AnalyticsService]: Create MixpanelAnalytics object'); + this.provider = new MixpanelAnalytics(this.getMixpanelToken()); + } + await this.provider.init(); + } + } + + track(action: string, payload?: Record) { + if (!this.provider) { + return; } - }, - track(action: string, payload?: Record) { + if (payload) { - getDefaultLogger().log( + this.logger.log( `[AnalyticsService]: Action: ${action}, payload: ${JSON.stringify( payload, )}`, ); } else { - getDefaultLogger().log('[AnalyticsService]: Action: ' + action); + this.logger.log('[AnalyticsService]: Action: ' + action); } - if (this.shouldEnableMixpanel()) { - service.track(`[Mobile] ${action}`, payload); - } - }, + this.provider.track(`[Mobile] ${action}`, payload); + } + async login(userId: string) { - const isLoggedIn = getDefaultStorageInstanceManager() - .getAnalyticsStorage() - .getBoolean('IS_LOGGED_IN'); + if (!this.provider) { + return; + } - if (this.shouldEnableMixpanel() && !isLoggedIn) { - return service.login(userId).then(() => { - getDefaultStorageInstanceManager() - .getAnalyticsStorage() - .set('IS_LOGGED_IN', true); - }); + if (!this.analyticsStorage.getBoolean('IS_LOGGED_IN')) { + await this.provider.login(userId); + this.analyticsStorage.set('IS_LOGGED_IN', true); } - }, + } + logout() { - if (this.shouldEnableMixpanel()) { - this.track('Logout'); - service.logout(); - getDefaultStorageInstanceManager().getAnalyticsStorage().clearAll(); + if (!this.provider) { + return; } - }, -}; + + this.track('Logout'); + this.provider.logout(); + this.analyticsStorage.clearAll(); + } + + private shouldEnableMixpanel() { + return !!MIXPANEL_TOKEN; + } + + private getMixpanelToken(): string { + return MIXPANEL_TOKEN as string; + } +} diff --git a/src/shared/lib/analytics/IAnalyticsService.ts b/src/shared/lib/analytics/IAnalyticsService.ts index 05fad65b5..88b9a9bc7 100644 --- a/src/shared/lib/analytics/IAnalyticsService.ts +++ b/src/shared/lib/analytics/IAnalyticsService.ts @@ -1,5 +1,32 @@ +export const MixProperties = { + AppletId: 'Applet ID', + MindLoggerVersion: 'MindLogger Version', + SubmitId: 'Submit ID', +}; + +export const MixEvents = { + DataView: 'Data View', + AppletView: 'Applet View', + HomeView: 'Home Page View', + AssessmentStarted: 'Assessment started', + AssessmentCompleted: 'Assessment completed', + RetryButtonPressed: 'Retry button pressed', + LoginSuccessful: 'Login Successful', + SignupSuccessful: 'Signup Successful', + AppOpen: 'App Open', + AppReOpen: 'App Re-Open', + ActivityRestart: 'Activity Restart Button Pressed', + ActivityResume: 'Activity Resume Button Pressed', + AppletSelected: 'Applet Selected', + ReturnToActivitiesPressed: 'Return to Activities pressed', + UploadLogsPressed: 'Upload Logs Pressed', + UploadedLogsSuccessfully: 'Uploaded Logs Successfully', + UploadLogsError: 'Upload Logs Error Occurred', + NotificationTap: 'Notification tap', +}; + export interface IAnalyticsService { - track(action: string, payload?: Record): void; + track(action: string, payload?: Record): void; login(id: string): Promise; logout(): void; init(): Promise; diff --git a/src/shared/lib/analytics/MixpanelAnalytics.ts b/src/shared/lib/analytics/MixpanelAnalytics.ts index b2959dc13..321693612 100644 --- a/src/shared/lib/analytics/MixpanelAnalytics.ts +++ b/src/shared/lib/analytics/MixpanelAnalytics.ts @@ -1,33 +1,40 @@ import { Mixpanel } from 'mixpanel-react-native'; -import { MixProperties } from './AnalyticsService'; -import { IAnalyticsService } from './IAnalyticsService'; +import { IAnalyticsService, MixProperties } from './IAnalyticsService'; import { APP_VERSION } from '../constants'; export class MixpanelAnalytics implements IAnalyticsService { private mixpanel: Mixpanel; constructor(projectToken: string) { - this.mixpanel = new Mixpanel(projectToken, false); + const MixpanelClient = this.getMixpanelClientClass(); + this.mixpanel = new MixpanelClient(projectToken, false); } - init(): Promise { - return this.mixpanel.init(undefined, { - [MixProperties.MindLoggerVersion]: APP_VERSION, + async init(): Promise { + await this.mixpanel.init(undefined, { + [MixProperties.MindLoggerVersion]: this.getAppVersion(), }); } - track(action: string, payload?: Record | undefined): void { - this.mixpanel?.track(action, payload); + track(action: string, payload?: Record | undefined): void { + this.mixpanel.track(action, payload); } - login(id: string): Promise { - return this.mixpanel?.identify(id).then(() => { - this.mixpanel?.getPeople()?.set('User ID', id); - }); + async login(id: string): Promise { + await this.mixpanel.identify(id); + this.mixpanel.getPeople().set('User ID', id); } logout(): void { - this.mixpanel?.reset(); + this.mixpanel.reset(); + } + + private getMixpanelClientClass() { + return Mixpanel; + } + + private getAppVersion() { + return APP_VERSION; } } diff --git a/src/shared/lib/analytics/analyticsServiceInstance.ts b/src/shared/lib/analytics/analyticsServiceInstance.ts new file mode 100644 index 000000000..9d3c15a62 --- /dev/null +++ b/src/shared/lib/analytics/analyticsServiceInstance.ts @@ -0,0 +1,14 @@ +import { AnalyticsService } from './AnalyticsService'; +import { getDefaultLogger } from '../services/loggerInstance'; +import { getDefaultStorageInstanceManager } from '../storages/storageInstanceManagerInstance'; + +let instance: AnalyticsService; +export const getDefaultAnalyticsService = () => { + if (!instance) { + instance = new AnalyticsService( + getDefaultLogger(), + getDefaultStorageInstanceManager().getAnalyticsStorage(), + ); + } + return instance; +}; diff --git a/src/shared/lib/encryption/EncryptionManager.test.js b/src/shared/lib/encryption/EncryptionManager.test.ts similarity index 95% rename from src/shared/lib/encryption/EncryptionManager.test.js rename to src/shared/lib/encryption/EncryptionManager.test.ts index 54e4bde1c..6c1db066a 100644 --- a/src/shared/lib/encryption/EncryptionManager.test.js +++ b/src/shared/lib/encryption/EncryptionManager.test.ts @@ -1,36 +1,36 @@ -import { Buffer } from 'buffer'; - -import { encryption } from './encryption'; +import { EncryptionManager } from './EncryptionManager'; +import { getDefaultEncryptionManager } from './encryptionManagerInstance'; +import { IEncryptionManager } from './IEncryptionManager'; import { answerRequestExample } from './mockData'; -jest.mock('@shared/lib', () => ({ IV_LENGTH: 16 })); +type TestEncryptionManager = IEncryptionManager & { + getRandomBytes: EncryptionManager['getRandomBytes']; +}; describe('Encryption', () => { + let encryptionManager: TestEncryptionManager; + const privateKey = [1, 2, 3]; const publicKey = [4, 5, 6]; const appletPrime = [7, 8, 9]; const appletBase = [10, 11, 12]; - let tempGetRandomBytes; + beforeEach(() => { + encryptionManager = + getDefaultEncryptionManager() as never as TestEncryptionManager; - beforeAll(() => { - tempGetRandomBytes = encryption.getRandomBytes; - encryption.getRandomBytes = jest - .fn() + jest + .spyOn(encryptionManager, 'getRandomBytes') .mockReturnValue(Buffer.alloc(16, 'Mock generate string')); }); - afterAll(() => { - encryption.getRandomBytes = tempGetRandomBytes; - }); - describe('getPrivateKey', () => { it('should return a valid private key', () => { const userId = '123'; const email = 'test@example.com'; const password = 'password123'; - const generatedPrivateKey = encryption.getPrivateKey({ + const generatedPrivateKey = encryptionManager.getPrivateKey({ userId, email, password, @@ -45,7 +45,7 @@ describe('Encryption', () => { describe('getPublicKey', () => { it('should return a valid public key', () => { - const generatedPublicKey = encryption.getPublicKey({ + const generatedPublicKey = encryptionManager.getPublicKey({ appletPrime, appletBase, privateKey, @@ -60,7 +60,7 @@ describe('Encryption', () => { describe('getAESKey', () => { it('should return a valid AES key', () => { - const aesKey = encryption.getAESKey({ + const aesKey = encryptionManager.getAESKey({ privateKey, publicKey, appletPrime, @@ -76,14 +76,17 @@ describe('Encryption', () => { it('should return an encrypted string', () => { const text = 'Hello, world!'; - const aesKey = encryption.getAESKey({ + const aesKey = encryptionManager.getAESKey({ privateKey, publicKey, appletPrime, appletBase, }); - const encryptedText = encryption.encryptData({ text, key: aesKey }); + const encryptedText = encryptionManager.encryptData({ + text, + key: aesKey, + }); expect(typeof encryptedText).toBe('string'); expect(encryptedText.length).toBeGreaterThan(0); @@ -95,14 +98,14 @@ describe('Encryption', () => { const encryptedText = 'cdde8547e1b8a14d0dbefc25dd686aa9:4a0ba43bee825386d40dcc2685cccf2f'; - const aesKey = encryption.getAESKey({ + const aesKey = encryptionManager.getAESKey({ privateKey, publicKey, appletPrime, appletBase, }); - const decryptedText = encryption.decryptData({ + const decryptedText = encryptionManager.decryptData({ text: encryptedText, key: aesKey, }); @@ -130,7 +133,7 @@ describe('Encryption', () => { userId: '2923f4a9-20ef-4995-a340-251d96ff3082', }; - const generatedPrivateKey = encryption.getPrivateKey(userParams); + const generatedPrivateKey = encryptionManager.getPrivateKey(userParams); expect(generatedPrivateKey).toStrictEqual(result); }); @@ -140,16 +143,19 @@ describe('Encryption', () => { it('should return an encrypted string of real answer request', () => { const text = JSON.stringify(answerRequestExample); - const aesKey = encryption.getAESKey({ + const aesKey = encryptionManager.getAESKey({ privateKey, publicKey, appletPrime, appletBase, }); - const encryptedText = encryption.encryptData({ text, key: aesKey }); + const encryptedText = encryptionManager.encryptData({ + text, + key: aesKey, + }); - const decryptedText = encryption.decryptData({ + const decryptedText = encryptionManager.decryptData({ text: encryptedText, key: aesKey, }); @@ -165,14 +171,14 @@ describe('Encryption', () => { const encryptedText = '2bf257dd17948962747bbed2a15a9c7f:3db25b79ca490246db2e5ca3b6b014a78b01cf9dee3bb81a5b0d3b585d1b4f610cdba6ea024030aadc1f390752178a3600d34b852fae19976fff9058f3f8a59b0f9f20be8e68c3cc49c8629c7626033189ab2cea2621e2b9417a0ea9d1eb23687c41a2cf67ad017b595326dc69fc0f2adb0325308fe498227563b92996d602a2080fbc4a76e52261d173f92f9e44ef2667b878d886b27ec06be615cbd6a02dd2bd45147677a0f2925e01941e81a6140656e0ab7e1c035c632f6b67e1b5c6ec0c921c4d5e3e5c4b85b5a236c3058d1bbd75629280d7d7748a7c8b251b05c2f6c853e4453a48737ffe26236e1355bd8d2c7996ab280d40992db2ce4c12de98fc1fc4fe24b0ed8b48553bbc18c7c838c7f665a78927db5b92d5a65d0fd277b98178d91b060c86ecd7997620970765ba8b8ccac6afc8de2b207d8d36569bbe48f65b3d297734ed02ebccadc46a507b27b8432d7fe008ee0fbfcff889d9f75cb16a7c8b29db806b3f31d6810966ff61f205f8c82aae21b33754d935a42531d908148b4089b2bf3f335de5769b4ee607b7ba942f8190f0ff1483fdd301eb82e21b571efa7d5b8a64200148286dbb02a3dd5f7c3943751f8ad773976a30677a472337ded507f387b55ba40092f953717b5586c7e076488574de97b1cef22ea72fa480f813a67dc3447f8bef4a95eb1214303070393bb9790759c8e8682cd5aa31791d038ef0841147f630c4825b0473e765200235410badfa941988a4c769acfd8baf596b178ece7f9cbbb747cb83ed01733deaf3d586378931ac11389099a785840a9b2358346c36035f1c58c00d346087708158ccc8f361e24ea0ba6a2f77f50e8a27528162853ff5b483f69180b55c21e6e153ca2699c8bae14569aea509dfda6d5a11f911f285fec6b9bb2e61948d8021410bfcbf5301443838a2d4eda24c42b3723cbf838acb991c6180a971ba47ff17a350006385a5d76401ebdcba1e048375da42043a22ad0c2e4a64b713ed79207f1d6a17bb120f3639a82fba33be02285f18bb8ad3fe1b2e88f1bf089fd7c6cb37811b0c3a33f48e377b377554a7b193a1ce621c3bec7af5faee8f1c0c3ae4d7e30b112badf6687a7811941cff2b48c24a9e0e43639f14f76bc30df8663e11524b887138d8699b0ccab63bfe45a3a263c4414cd908c208f059a88786333fefbd5a1d882f548ddb57083a5630ee22d84e7e89e1bd79f296fc570c5c7d6dc6b70fd580891ecb92cd420785c4b211ff316b01f9708fee45d42e108863290a9c7ef106684813034db5093bf111c072c92ef913fcc42ccef03b9f9aa0845326ae2c5f45e3b52e75a0e41afb2110109c141fc6672a63426114fcc86e5e1738cb1541ec5b2d1e6bea4c33970d5192ff78a35dd335b44e6523dc91535f69c411500bfb27dfd1e98783bac30dac64cd29a3874429e04839e971fbb80d2a5876780c6e9097196e6e0c6eb28c51ec43c03a26b4be18479d64ed823fad827c825dd431416ea8d658c55387aafffba873bea1f6e70e176a31db65e93f4d2ffc0aa0516f4de8a4202134d3d3e551ec6004e128556f47428d27f60ccc18a2846756b37cf4e1a5e0eb16d8ad611a99543a5dc7673790ab58f67d7daae18886aaad1c9deb80e93af4e9595c0f8eb8581fe1647608787c07066e2e4d90661b276d66958f3599edd2e6e893c7ad6e569bb63d956d2914507763ec1924ee0b962fc0acade023cc85b66f213930ca047cc554acd3d300e4c3ff8db5e3a20a43934c4a054187a59a8f8e463d0b35777f08a7ac2783c8892a14b8657bd7c49442719ac69c4bf9de029af4eb7be2c92832382a472ed4d05023d928a03b61ab142e71374b0f06cad6105948c9ade32e48a548d52bcc5de7f0a9d379b7cf0d00475b3f6f89a26880f4c514dff4fd0d3d95d85c61b4038c7e8eb32adb6d77780d1b856ae2453d601a47e07f8c0e636f6c48d0f980ee854c8addca173c3ce9cd72d9dba1b4d80f6384b1c8143b47936b1b8e8dc16c4a272adbdf70e3bcf9a8545c2689782e184d0432ce0e8d10e30d78a2d4724eb1d114031a95037ef16266c84259173c6732fb531e56407c5b8fd9e64ff5cfe858336d23bda8f65f895a68fa148c03da392e2431b3ec90cd6367576810330aca43ee4ef09c0d897a2ee0284ac053a0eb8fb91a7d62044d750ad7e0010c75266a2bb536a7f64195601e3e207913e4f38183ff6ba8fa86979fa1ddb8622c588294c8fd525964dd1ab427b7e6519852891e9650fba20e26c0a3dd4d115e1faf04cde8ceeb79281754d2a3cea28960ce1d00a796e0cc285abae1155352d35392a19f9cd09a7793546635c802798b44a8f3435a43a6d40148790ff67c74e5d8401d19d755ded19974a88f401c267c14e75cf3e3a7251122def01b89b0bcfdc85f68e1ac79c7cc4219b4b9ec5a629c951a27028d921356b963d0607a8106d7b1737aad6f0d0f4caf9d8335f82c19cd4edc9e524e189d9e4c5b33905f735e548afd6bf99667aaee40a59c00e9a3a1ad735b039ab356a141289a7fc100c2ec05c53a4000042e13264063ef7b709a7814d35fc8aa90f3dccf9d244659163e3b7789d1ebebd96edc0b354df786909e668fb1664f0d8c2bd6e27fef8419346f0ee45b5eeb2a7de117b7fa2f6fa4b561cafa6b626fbdd28fe2c9049312f188f09a67483f74b9da4ddb92215263c865c3ae4d695d2d3ad1ae5d7ed67436d43e45fd1ce2053829e42c63f705d5383ff768da48a248bd8c2623b8d34e1e33060e22c684f2569c54c6fe1b8b408c6189395cac8963ce422cc3be7d14a701cb33f0827eb085e2811fd574cf9f80373c638c2fd5e0008c7d4fcb6424f6f4630e9787c47be26f7ad8daf50d4c205e5c7796cf8887322fd82a73cd6bf019289b4ca1519539b464f7e4c703a89d0b67c84cb74235299b79990d43633b2a58f2b5e54e322f1d9accee6ca4f70ab6a0dbccf9cbffbf2d78f73bf6e13cd38491bf2c599f1b77db07ee73199aa579d512835ce12ac682711d72e6cb4c000d797727c5bfbf41ea34731fc373d90bb377d55c2b8b8ab338a65ca8a6787fac526aa5d99d19f8ce67cef9b65cf14633d1641633f721e478826445600b6cbe1e4eb30d839039552381c84233829a79469a36bdc511ddf8fd2d5da6b40d709e5c179737bb1a56a4f6ae2a05412c1394e9f2cfcece2411bdd43bca42af0a3e91fb41969bd039e5be510297a9a215a09b6d837c2c51325e39c3fd32227906a8d8a9f72e06f4bb6089493a2885bd95a3c9e9104a634b164224b39aa6cc833ca7588a2a18efa1afa168ebb551310cf90286370b569414c8ff5578555a8f79fe8354950d5ecb140b1b9f27cb88e97e40bda5d946aa08dfbbd7b4c3cacb1d2baff6da8739ed93f699d96ed2e3d8651a6811e8ebbb8fbdf6165525878b06f4d869d65064e4f3fa09c8fbec8a107e9a9bec16fa1cc467db59667db3e77c4ea58618e123ba8f4c1d4d1ed265bc50fdf9d4365d47910982e0af479342bd772dcf38fdca8ff8aec6ae21800902cf1da5d347f270d2da5b3e06ed2a7c4890a13ac928289b32a632eeb2fee21541dd16d0988e460f4e04119a5c7d6a3c31089cd0947e8e47231cd0093c0eee79ae7cf970d2889e085af805136cd9236e037b213c1dbded8cdeed9d0434d702309c3662ea2cc7998de84ed3471a36c568e23acbfa4afda9ffd26e1b1012c2fcc108c3f19ed31a610893f776f2188e347280590aa0b4f4a1bb722f60f08a58995cad46cab8ab6960a77489cf8fcbd3551e215db4b645c6c298c454d605c5e74f64a5117d4ac372f7382d2808a5a0a03eff46058fc27a966f87bb182d67ecc32d44c35e5a399f3da84fa2d7ca27ee1baf2581b7b44cfc40325cb4ff86214c54355920a0ea9d0ee1e1f45ff16ff2f6a0289d302398a25dc955b2b3d939c1f050fc70e7202c3206822ab3346307452bc62aea8e64f661c369b2ef3174061b3a561fc595c1a6ed0b7a2e3e4833fe2163d9576f2e53649eaa66f3f245b77435316ac175dfb08496f1aeceb56c0d94b5edb099ebddce3a821d437b0e9326de45917906b868c2fa8568c765ec5c05a278c26d97869d1a8efcea72fbbca17861f24133fb1f553df99aa6b55c3a62c519479857eeb097cb849229fb7de40a23dfcf062ecdb19bffa5f913f421bf56dc1498634fe69494d07af6e6a29a10bf8df40fb4fba000528841bc547c39c3adcbe3b52c557fbc8054cf0958ee3d5d3795616e17190ba481164a5a682a1d288c71399bbb1c7615b73414493e26cc754238cfb7a985faf76dec5e7eacf9fb58301a8d67b309a82f18b2a043fcbebdeee7a14871bb495ff8a26bfe486c9c4acb35fd0a0d5f24a4333078d6341fe74535167b0b4cbd39651d6f3d51cb802f470a10064cf339a71e5e170371332672a191c92d769ba83c06d5d3ec56ee4f82f5f417037c4106cab66a3e8d6758e36775cf0f9fd627e3a77d6dbec644eb414950b132b10a27e97ba94981c6f9b331e09a7d3a688b71e97cf8b5c1bd1e204ef773b266a9f63620fc0b5049d58704e3f550019422f1fba8ea31bfa3d4011f92dce45fd64d50e88ad36a7870dc824c10c6300e0b4aede62c422f997ff88cee344b1d94dd57bb6c6f77f6c4d61c3168727582d5facde92a455e4c51a34899a23158129d68a5d2629bb318f7d95fe23156078517eba9dd6f7a72614981b890e398e2ea5eb1fa159deba0040b892cc58d3f48bfa35859e9b823b3da1db717a5074f8e58488bde63d5e186996c081c389ffede8839bb4fff47540291db8528a6c483270d508beebb36fe47ad3e0f448784592aad8aeee3073df9ce1c0769cb45cb9e10819bc2cb8f475a802517d3f03c011235b5b52fd6e64edfeea712a3c5db2d34399e950b0aa09f5649343346196a24ad89024f831ecd9d6fce6b769c631446cbef9b975cd6bc6b46fb40c9114fe5d0a7a949a17b7f161f6cef9d5a215112fb13028e92ac41e09c78f03c031382bd331b9ba3ab19c49cb68698f3866f6c94372bd01b50e76435f204c5c9cabe6a3ef0054eb2c15f29eace80022f7a256dda53b42b46605fbcde6e9b0a57fcff7d6d97a60caf4c3b31e469828a26df4dbd7b3448100f7e3babdb95fb147020e68794152ed833df70870834a028bd25fe275677cba328fd96620312303d727d6794968b0fb623703950b31a3ad33d0ec12232b0b30ae573444fc3953c235eb6c051eadf448ea2b613dedc4603dea53201bf660e8bd67023ca7b005cef1928642f743f474ed824652bd9837644424a65e606262e61434f1a9338411d0478e5eedf943c5e7871d1192098be52baf9fe2bf1a0766c058621f984b39f55aa3f90a55f9b8e806a77af170222d6f5623c71c8ce51aa794554d28fd63e3725bcfd55c2a58dbe8049725c74b35d40ab3046dc57f79baeb7bf12912734d86c7596d99ebba133c397f153a646ed009816474f1bf09c97fbd3883fc56bebebda894ff912eeab44182a37b5f2e8d8879e76c555806c0cd68b6f7dddd98faa0927e6e5ae8d63419f220476873fe6f1051f8443d5b50d7bc827f8956e58ee3fc4f2ba117970ff04b498db8a4cf97d46c481ff665787d5e20ccbdf808ed4cb897b1dbe4a577c1ef70b924851684370999a76b733cef1a268e98e8e05fa0957255ebed55eef0dffb08cb4d38dda9f7331e90577326102326f7e758b3316b69d070155ad314d435c9a8ab06e000a70a958d2ea5d18db1e1e572776c340479ce82641b7b7243e5a464efe57321493058e02b2073a43088b219300483a1e477a1b3ae416c6b97b624ade0499b2b94b19e00998360caca7c60dd81ada23061a61a26599515e1781342e006e5c102d168de59e2a3bf163c6d94d414847aacb34b773a825d2d6d0ab8aa29537cc96aa9ff117506676fb671f89d36d4bd456866eb51f335a2fc6a75cda1fa25d3ea04edd4e17a7d68b80a51feb902feca321d08aa30e32b0c6e9578706098ae4573870c1570a6b0b6743cfd632d03e84f731cf43f4ae87d983889d36cee2e03f1ed2f2f509bf66770401da29ad3785244669284b980ddff60064adabd5a6d8d62f27a79c82326ae3a0247ed5176e1b793e89a6b594f4d49d02184f8dd4101906cfac5ebd00b4b4a66a3937d361abbdc405d23fbf8883805b394fc803b701e336aa295bc4dc83cccb3699dc59f4fd5e7ca0b191246943b017929044388bf430b7bf329cfdfe13ac06920482929997b641b4fddd525c62663df309329737627b7bcc27dd52cd3c8a022d0b169bf9e088dc1188328b79163fb5ec227d1f6b79f953cb620d05c96f18695180d8ad4fcb10c109ed805fc9a895c50b402879d8208bd64c471367af0da14fce2fe5b92b7bae6b91e08ddda90463b76c72444d2db7070dffbf1024b411473debb3c6bdba7dca7e9edcc3c23a6c10e541ac25a323ac4535455cf8c068b1f876a32f7d4963be80997c7a6ccac65915fd6a14bdb1dbc826f99c6d44a328ce7bd777aae30f33a2dc0d9379a8c138560287d06b7420ed3445c6e204b746d45603dfa96c68a77252969d3e383391b6321c442fbde292eacee17df474752971553154cfcb5832cf588bb7c6b3b90bf37632c829fd1e03d603fdee5a75d781f610d420a629c3596d4fa788010df0fda5e571d4caf29c28188a47d1c77509b73e4ae56a9f55ebec1e7788850be4ebe9a8d8eec335ed51538a69eb3e28bbb6fdfed2f82e36182b9b403442ad1ca62d82d8bd98ab8742f9c444d10f2998a40b7738e5b71fda7fcaaa329d0ed1421101e2fddb8d9c951daf6f34ecd4d183fd5ef7d35a7b9220f17fb9e7f493c4786496352f43e1cbe048214d73e967ee4585310f25f2fda1cec5e254bf2be5f4674757158f1b240fb6cd106fb474260d7d16a80fba1d11aed8281b2414eb7afd8b5e98d36590019682b08d89db8c447e4b9044e786f3888854777691b0c7845d49c46a5e1c4cf56072af535acf51cea93a52ef74363bc262a28538f28e675d5cad79e6df85176885e8a71da42ac51965670895d33dbdec401432bb25e98ea14e9569004abed2bff13b4197a87e8d4361eabb279dde526048339d4312a84c5c3cbaf2d798b91396cf9b6ed37185e2224d94f2628a3202169039b160a372662ae1ab626a44575607fa66d2cef04886791980050fbc9c4c5c35720fb61cc3f173c66cbc456f76308dac465e5a2d4244b48b906b509b56603c61495672ae42dcfc83070ae35ac6464a5d9201294749198333c69b33f22ed093e350b45de91316f215dbca7916b03d24f8a17a9bbc68bb688ff7ce69c95a33a08192c39d201954b33e402d74f17c0c9ab293ff59ca63970748ced86a1d651280c8e96c7f20d34896c2dc82923b9f3d2715596acebf0a97598e89e5092b41d3363ad7eb79707bcb035e398b2c69af90e7526008e2fa0874a7d36dbf8aef2689471743221612ac2407e91780d3f2f452fc8e3c03e5192cf45c86cf2629bc286758dfeb0e0019e8f761805fb11d6f0e47bb8089c05145e564381f28074487d175aec44b97fcc1a44369d70bac628462d766e36224de2f2ebafcbe9ff5968c900220d0c7d4ac2747e4b6a60d9f69e37f982e38318e14b8c67bc285b65a21078887e8bf185393ef700bbd56bc3849f62c04f80e0af256fd8e47855d8b1465abdc1e2f0ad3f8d2be048095c4d8e86dd45b0b1b7732e11e297f7fe734bd6cc7c9609de58bef6c08913dee25d8bc1bfe04e6baa69cdeeaf89e9a1a8ff9482dc0b65e8d52fac0795d15403aa6d067794509d5a5c5d41407a215bf790e833c89a8a566a2fbb8e85215607fbfd9c12460a1bae845d1e312022f326a66f4c73bad6283fe79d04a79aaa2402fb72b23c482dd22d5a9b617eacf0bdb5306387f1743672f01c68475006a0ffda9a2f6a526236280abb474b28a2b7e1bedbc7ff74ec0592307d56168dc9d31e389bc950bb0d9ea7f5e229cc7a6867c35fc79a50344ba120586531eb6d1746863b983f3c8dbdd4876e3074edabdb45512a92f124ae4c02c45746e7d360ebff96a015bf5fbce44585ff431911c3c4961dcace716b73a960d8f1e2eb8c15f53d3039d274315026a37c46f3571af20a572fbecafb5482f6a711502e443ea075c0dfdc59b431118abddb8a90b9c1c45bf254a027a447955b0751a6a3fdf7d9aea1394bcd0a496fcf62712128adc5ca691840f56ef8477416c24d93755c0be9636a58118f35bcdbc1af18c1532e2c1b883b44709e00f62247e60b8709203fba9b747a02601d53ce4ac75e624a88094fad6689bf2701f2e8f3fcf6973dce85688eeac3810a410842e094368ac80a4f72465841d09b0d928142677f1f27ea03f9408a22d3fff5b4fd6e1d32b366a9a3be9520a56a45bc79d7e048fa0aef1b56523ba3876c9e403b894f6195834416b41deb6f024a882178c0c4938f8037bd55777a6a0b322c4d897f6a6956d4ad603764152d0a674fe509d91bef78949660f2b2b60fb13acf36c36b94568a7916fa2fff7a600134c2f3342702c79f3fe80e89a6b471043a74d3881b93886a02416ed53ee6157ebf0cef7b335d1321ec18e01309f4999bceef23554d78754c19d75e5a1e546ce40c7c45eb4b426aca93565bffe0495986afa4ac4b84002361e174c6860d4fa1caef27359e2b022d7b6c937b947a996336222bc4a7e2c2b45294209a7be0c05bac1e51455133e76debb9279f1260461af5cb65a557fc6a97328d4473862306f3aac65db1e80148ea11d867f3f20f4b5ba9a602790ad1add269b7fba220f84bdb286e027ce59bb3a645a7f394138d1532468a747018627e2fa3432d81f48dda977e14815d5eb51151f29391d09f61d722337f69ef876cfc1d62e8ba0e505d74eddfcb55f25f515fe924cc8a05f9959e8262a3bf2ba4619012340084eb9251e362af401e9991a25643ecf8f29f32113671132123b6b7ca657fcfa1289fad0317a0af66a863e380f05f266b1dc98c70f1521bc2cc077e4da649cc886be53bcb1b37e4bbe6c98b248fdc907e6160123bac9689696d9257caa3f85b4bbf4084921964683a61f4477937cce3ae26f17876f89be697e8d4a8c4e7aa51fa792d8fb1fd6c3321f65de88e62e8e95eaeaeca05b17fce62a00c791f10b02d84ed17a90870cad7770feaeaf78e8d4f1f7648ca700ab16ce11794f0629d07cf8fdc63c5b5db94efc89d1204bfbe048b7b979653d48996c8b0244e6d4dd3d7d43723cfcec71d87f90494360065254749446ec62f795f33e961a5d2af685f7ebeb6140ab857ba1b05eda6010762d9bc2a89e694ffa3445e7d27fc4dedae655e0735c68b418c48bbca23acee77af969a0f4ecc62d55d12f817088a4800f95cc19aa097bf46589359b816e33cb005ce10df4a41e5f044c1da57d9f391d225243d7823a6f4543d3a373c2986097f10b8ff29b03c311ad9ad391c7b1c8770fc64fc4b9d2fa5ef11e24f4129d197e44f615f98d3fc873083eb3eaee0a5ea7b15981756ee49e063924838dd8a22fa88c9847f37fb7e409bd6dfdad4fa135a9e8dd856118439e7ae38aa56ec91a9f57724e706701e954f42f6e4c5f81a29ea38cd85e62ca7d8af506e145a8b8fe01e0bc272c5f95c538fabdad154f283f3307750717bb4033f0f11206cf8e8c00e441cfe59c4273e10b589e13713219897e171754acc85856c50bfaf691e2964ba2d0cfd1665ede3f0ad63d8b2ed1124fcd64af6cb97f2279cea9bc72e61439f2c8edd867076f85f1cc39a570aa918f25db3a31c56c7979110b22c4fb424cb3f81c75e3c9956e7fcc6dcf67bb691574784cf216eb93b075adde9990a415955ccef116f1e2f586ef62eb917df8a38d363dff16e772abc497334969b1c6cc7bf3789885f80fdd35c2b4817a3757849e01b0d48478b6b9c27c1b7c416053b1f9cb15726a45de8721c68be6ded1f2e0b2656ca500a2684776ee33ead96fd8c834a58bde55744bbf0eed04f29d29c05a60ec64de44d028e54cae62c88fc878b98b3696cc17e6422b24b89e4b9605415d44d5cf40e4924732a8d0eff42b20763ac5bbd1b05f303f0812a2b4e835f39703090740b67dc4212741b5885374071f990c09b87f9572ff1d5d023bab8102a8c764f8a9f33417e693b8ccad3168c8f46769b9b92c108e913e256d602c17103b88b2ea933fc54de480de8a13468f0130801da3e1a6cf6bc835598d829f636b807c7d750ff0e5b00a1076562908e5b09b4fe61a29fd7bcc6e89c1e95851f8a9ee4c233f5b8a18b72b17822f674bcdce919851056213af86c5c6dd51b42074590190d6ddac1c7e5f12383f61d4914074daeb57fae21e67ba20c823ebcf60fb1aec4fa7ada1ef119241d792407778480f927174190df5cfb8903e00d14268fbf24f345329254793574d80fc54ddc0ec99ad51c842075eb03d3050c9dc9b228bf16001506d25e7858ef009fda54b65ec06ecac2db3b3cb6b4c4c0a8741d533a422ce3e88029d79e703fe18cfa083a66d89f89e8993b6d6a60ebc6f1d323d4e34670889ae774617bf7e81683e90f2809fde70721ab98b8fcc17f451d59a7cd896a205e3b1b28170a60bbafb48f92ab2925ee30a584a7e13f9c7e5b466bb4cd10bfb5c67e1156e85e17127490dde38b7c85559cdaeeac692bdd0da2d5b8b9a20eb4917cd24846267ba03123229d1f428e8ac76b508d624797a0eca0c11e24fd982cc71609c029e8dcdcbec04bf7806172987bb7d75269436ac0e35f28255ffc35ae9fba7a3138c349afe5a98ad4b10e852feb36299b20c56909e39a7efaf1d20de1c9429138dfa1b762fa985fbab77b44373d6fb2883d89acb7bf338eaa821eed48f9cae37d56fc74f01eece391e41c6cdf82dcfd1f5701d09194cea78591123ef9a6fd3d80fc0195e5b580e39cfca487ca73b779e11f47126f1d226ec9df0872b235797333bd598a7cf3e2caa36d3240c9846951ab56e02d69a9c7175fa98ab177124d84fa960cf8c289c120f7570361332e43c876eb78b6b85ed0a3fe5965a717a7f05b1f2e563960cf2a033ada4a9664f2d65a8f7207ac9e116c9671931066175ad8c3ddc01777911aec1c6467156e52077275cbbac0ec54b7b1fb8d31c0e28b5efc3c13db2405e8372cb1963fedc704347b7514281761b4c0f08860775c9f001db7f4bd0139abe193ff00ace3988e55fff62dd1aaf71648fea4e1ed416bab46599390f0566ae9ac66bc175698f50cc4016a5844b19e062aea6c8347a6304c9c6059de8484f062a473b4256ad6ba0ad871127e90fbfbc719ecb47c59ee78c14782f9d123795f2fbac9dfd38803a00ffe4b46517182ae7ba1e1c68504173be5d0159d685ed380411c196f1616ea7df9243e2b9c8a3d90706594f0a67732d61c703d3241a0c3185992f3970a962ad50e87f12c744e9056983a2952da73f88101b2e32cc8d2e4636c35e05a3b1ef747ed5cf3c21063e0a7a993af74e07e98e55300490fe7a327f6147f5218e0cf5ca6588cc2d2de9ce73ef4b7c321cae74291fb746ee0f444ef22ad335281beb3c5caad258ac15c72edb9f6957ef6da6313878fbc517ffc940fc6d1fc7491e5581fcd896dcd4b5bdc3586b1be0c4a78d94c58aa961c87532889736f2711de0d0061106017c8701522b4073c6b3520c21db077eb55043c5a7b2568ccf7b497c8291fffc7ef96a1488d949782a8971bb8ad39cd8c3b38de4f40d057fb848e038f76f85b78c6840fd313c139a5aa6408aae243e6ffdfa8d8160b7179d69fb9fcb3c1093c7f48ee77eea61adbea3693256c85ea21640637801199c00ed75e513ae53a8223d673c72c9a107ac72abb08de13e3c96efdd8f68f12cfac6223fbfb16103a4946f1e771dec28641dcf83179313d226ba8c2ea3c1bbd118a1269e1d3404516603715a13a371e61752168f3422463f795eb5b2c4bd654597b92eda542dd8ac9da2a4b603b8cdb0f765133d3967790ef6a736ff4e574b67df646cc3f840f7eccad7ae2f56095acd8a89fb97cf6d00f800bc5c52c69b9f9a0d950ad2db79c31517873d2d928096d2e781b11107c8fd267aa9f89be06ac0d2a2af36e0c607b4945abd8425c2cadb673063a36e255244a1ded1131825675e3701dcaaae805cdda14398ce0ae2814ab4b37f1560f68aa53a8af32d9f1d0e01b573e044d6f7f9ac2c460ee55d7d8fafb3605177785a3abb64dda5f99b51ffd0cbb5fb664d9ee0efd1f24541912a77b2587cd3923894c76cb9507eb40da89fb9bf21400acd301b1b44841cc2da3c51c10e03e6e3d0bb7933848f7abccbda58fc5f481d12f62d736e9966515fa5e4cf9b3fe8b535466b9d3e4cd4753b2732831cf5a70853761d7c0b8909641c7baf7ef64387ab6a6d33492e0af5adefc3f5e5d73c6954587449d0efd32468e9491c5384529c01ac6b73024c889d5207514f7b2037b7e8b94fc29cff08a543121227b411dc07e1882f826a21c245d2cbacf2fc8fe6ff0518927b50f2e399fc0f0a09a43c809bed2ca240680380423a87a7078595f5066fae4ad8845973ef5e5f5a9c0121f80ebbc0f592009e4696907e243c622116b3561dd26adfd0c3bceead89c28665ecfbab575501cb614f301d939c34b5673e9f78713c85e88db5fe8f9c142562fa32512d9246bb80411b6a3977410b3e6006b43148f7d8bb9fbf53de968ce0660f8b47d0a33ff6dcec8feff7caf8c49c39fb902c2eb6b3000e16172346774658c2ad78375ed47a7af9be7b696e5b42555680d27e43b02c27d469ff9b2acec889678766f0140433ba3c98bdac6d4a44ff4080b577ace33addd5ef6e0bea68468ce43c2b8ae0c861f4aab92ecb5dd5522b68bc6cbbe0996c60be50505e5aa7800a1538201588596ad1dde845df99908a257e3151451e3b0f644f8ed11438071405eabf2cb13fe47b3ef5478748502a77e134d274b803d486654a84f3c0e78856d1c7b2af16215bc344c69eca9d820858e393eaff75274f69ca1658a55c0ae33dd605d134b518087cc5bcffeaa9fc10f5857dc758576e46b169b6e1ea7e2537e701bcc9084c926a6b0b9160da60cb687991bb23f68d1afa043ae8fdcddd2d621f21f0b88e660cb13de000ac019c554b7d579390d7b0413c1aea110f6308ae151297deb89c4e581b83428c73c4158d1497a556423b171d694b08d0263ffa7693dcab43ab68459180cdb7e9e0b6f2c6eee8ce9d87cadcedffffc491b81a8eca366e8ae0a77910a16a0f9c6aa8fafc39d8f5e095de2759032994cb3ce2912cad03decaf15156c2adc729de28d4f3c64c7c67e618c5189eeaa20527e89e2f974ec3b085b04f180afb6dd3e7d416e0c026e1a68e593c1c7f054390968bc2c888625c06f48d2a1c11a22e7bad9ebbe03732e2702ba920364dd9b54198949aeb84056e80c255d14c3844de4d15dd8c4b971371e3204081fbd07b8449d6fc9ad28c8e4d1fd932c1b51d640e1a70a2280d5797b873ab1f4d351461730cdf5c08b29934a45a55901112be5ab22e7a12c79f04dd92eeff3f22dbb9ecca62b84ac775307e21eb5bf97bbf82751dc19860c285f6fa9f19a5bd102dcebb04e04ba871a4811e2507010602d0ddbd9f9e2916c35b96c217c1ce1370474d5d5f467fc284a77e7f4b38899fa14faf22dff8eecc6a24f8eb92d57a37f6f03b6e7dcbfb3ccc96fb2eeb40ee3434d19923a83f2828ad2b772f8d647469273ca00dc7e5896d46760d82558a64496c564a1fc9799b047507c04bbf7276233e7e7c0e14416412eab6e2188755749bfc23afa9a7326f910944357509d65b0349bf0f150e18a8bd7b7de9a4046034e5505030476334e01cd4bf3e8b3201b138eaf84060d7baf0ee8cdb4b0faaa3dc47dcef6e3e1d2dd1055dda295f28e825d5a410cc59eba28d44967ec31744b38516b01c2dd3fad7f3177145d398fea86d58d6f2084b5a1cfb31429d9458b8b1a186027617d56447b83ad4ea21eb2aaf5ab32884704c01afb61a87908e52f037c6dffcdb7b6cc62b37c27f5368fafdce416c715207dbe938bf0120039cd6c6ba5e00403bd293bcc101951366914af868b5d284369cace787b5a75694236f8b9d6364830441d1a3983fe232e6dead3f0da8f13731044f399fd9ab1f2c8f22729cf642fe7cd2c51808394d788f3cb8c9cad264b49ae52cdca7ab52d7e59dbeb9181c51721841bbbe18be3ccb4c7e8021f4db8d26f9c5c47412f0802e85168cdfe5ec8148c379cd1b86f6a31c256e16622854ef5e3930381febdafd0b9d7d0673dabdba9821ff3743837a506f2a61319af0a7ccd67ec911d9a84cf3058d039cbfea923a64220a71a9529dc3ed1da83c553f6662f9232c5f03d44e6ecc3efd16410ae03665f5463506d20f2ee191335c9e3a626b6ba9dd3c9717a8f0417f77d49625e431a1da7c52626a332a66cbe03ae75de592b83ca74e6159f4ea179dc50cc984c3a1da7741e02734422e2a57b1bc72eecfbe0c5d0deff1e9fa40a150f77081c6cf324694e5c929d657c0ca44bda640826e66c8c27652eba0e356fdad735fd812dc59741eae7317e884a3bad4feeade2f3f6675b419235776f7fcbc7600eb18550a23696d9cf5b6da38b711a07b8f442aa033d43111dcca0616a0745473bc82c1a93b987b125ab77822e67ca9e90a54f881f5d1df4903bb5e5717851a4c0ee3ee44ef5be2da8953a8df68627219029f8d2824a910693e5abff6973b6f2930691b76230289ade798511de18c46b30cb195d9fd1768ab9c9079f1c33a16d4aad5e88390b0d34d0eb3a38847dd0ebe6a93803984d9deaedd79dac5547cfbc59687ed427b5701c803289b9091d5e86a0cb97ba8404e0efae6b2beef5d33542063ced6721075634289a543085e8df14aeaae02304185600b0a8c19ed9abb6b9a36520e40c5326d0afd2e5d05d129971cedc2d28352409035688101335609d70d918fd9ace0266df5cb932a54fd6fcaeadc08577fabbf7136e96a5afe9d85c1fb84861bf28c964aeec31a064fb07c8c20cd76093ebda187e144e647552d590cfa3ed8663dfbc62f21e495c7c1bebddcc4431369bbfdc3c3b680873ff0e93302c414521cd3a9e91a41543fd9e7396960ccc8b21183fa8a4204a370311dfa9ca67132e98b364b8842a5f61edce46aa9dd7977ac6b504566655ca4545507d605def171ff4d930575462bba8bc15bd1911658f1babea8b5b63ba5e09acca48dec7e23cbbe0bebd58e2420935d7759347cefb2cab4db72179b15268ca3bbfb4feec5d6deeda952f67faa057266ea0765bca97a35e6a8216e58c959b48ba1ff214fc7d3d3760a567c5286527c960485d89ec7174e8f9901ce2c40295a48d846a3c25723e75e82b6dca9494b2f2e10b8d52b692a7944771e445331618370f37150b3e1ee5e7a2ed4fcd601cce796ecffce31586cbedf09dc437c4af63601c3aa4f0202086db1b9976bf2d0a08cce929388b31c26eeeace5b60dc56054006bd9ee60d37eb9a51b3bdfacf039979856a217b8120df63fce116e9a53f4b32464c21e52490e140df18ff445af3ee9a751729a9b4ab94ae242f1b7e2f85425df648c1bf25ac0daf1e46021b8df0607ffe0d4efd34170b9e46fdc647265f283020abb71e58f0316f8025fc05601bd583744740af385498c080993e4a645b12e578b481fc54611f2459ea53da27f0d3ab39300158706f399688d1141fdd684db5a1043bad24ee64c4c08c17b9973a7b3d61ecb93b34c07157d00256999296252f2e17f51667c627f544073e005186d5ca0fd2f87ebe6312f4922120341de07ec705cdcf47401df3d99e61bc17b1d3da7712f693d71c4b10659f3a964c1b52484e38c01d7dd768b34b78348bc45865803dbd3359b48d8dfdfa2557af8d83652c0b7e0f01d733ffbc1727c003ce73280b6f70f7038e41229397e1825c9226bdbbdb4764bc2e1582acb4a09c7c0d2c6342ba8cd61682e93a216d2679241c675d12d36d36188989a5dafa5520164ad85f0601a03a957a36d941c25aba851f7fd85f5244f56abe6e6a72dcd2b61a41ea5ff22633b57521dc566be3bf624d9932e74f179042eb527afa6fd8c6f7e87ae17ef3b60b185c57ebae5ec393dab94755b58735a6cd69901a71f14320979197acc48ad47824da92b82dc0ae8f4c09af278521b39b8bbc32c27fe32e0f794fef06bff957d8f71911bacd0d79e0043c24a8f528d1a6c906e1193c50095e720c20b3f3ef455afbcbd5431614b8a0572757a4d0dbe2deb514288958369dae1ca9693346b4b798214f1adf62651c7d6464116636d3b299cda4ff637b10a38e70e58de60dccfb03562a26439e68d804f0a52311a78ce43f209b53278e53db24145ade8dafbfc679991d944fc2f2aef3664adcecea517aa544d03c085b4bc8b47f8355b860ca340de454b42d53809d9ff01c1ee69d73b6fbf3279c8c5791fdd3203fb50298c0c152b2a34dd7b5a7504c2540ac64d131aa91480a3f944dc59927f46731ab2159f5d037ad13f7dc010762bd65558c5a6077783b6069a5a802581b0142e29ee4aab1c6827a93b02810a1753d63e1bed91b029231391bf826a81adf0ce04211d36eba6facb98ec8fb150510f954733c1bc1981f8671c507524c22dba38396e6bf3a9c0ed98e327e72fcb8b061dc28c249c60871d6411e85914f553b9e75ad4e141a3d54fda54aa312991d6e4e75dedb16b878e9e45c0fcaedce98789734c08ea8650325565857364c9c878b86ce1f45e861048be6c57f6d13ebe58600e4db5773b24bde0a6e14c046fefaaa9b5c6c76b37edb19ed101ad70648a9c84c872c0fee2cd0e0f9fbae47'; - const aesKey = encryption.getAESKey({ + const aesKey = encryptionManager.getAESKey({ privateKey, publicKey, appletPrime, appletBase, }); - const decryptedText = encryption.decryptData({ + const decryptedText = encryptionManager.decryptData({ text: encryptedText, key: aesKey, }); diff --git a/src/shared/lib/encryption/IEncryptionManager.ts b/src/shared/lib/encryption/IEncryptionManager.ts index e67855717..7ee7de5fd 100644 --- a/src/shared/lib/encryption/IEncryptionManager.ts +++ b/src/shared/lib/encryption/IEncryptionManager.ts @@ -6,8 +6,8 @@ export type GetPrivateKeyProps = { export type GetPublicKeyProps = { privateKey: number[]; - appletPrime: string; - appletBase: string; + appletPrime: string | number[] | Uint8Array; + appletBase: string | number[] | Uint8Array; }; export type GetAESKeyProps = { diff --git a/src/shared/lib/featureFlags/FeatureFlagsService.test.ts b/src/shared/lib/featureFlags/FeatureFlagsService.test.ts index 6d09c70e0..fefae2545 100644 --- a/src/shared/lib/featureFlags/FeatureFlagsService.test.ts +++ b/src/shared/lib/featureFlags/FeatureFlagsService.test.ts @@ -1,122 +1,112 @@ -const constructorMock = jest.fn(); -const initMock = jest.fn(); -const identifyMock = jest.fn().mockResolvedValue(0); -const boolVariationMock = jest.fn().mockResolvedValue(true); -const onMock = jest.fn(); +import { ReactNativeLDClient } from '@launchdarkly/react-native-client-sdk'; -class LaunchDarklyMockClass { - constructor(...args: unknown[]) { - constructorMock(args); - } - - public init(...args: unknown[]) { - initMock(args); - } - - public identify(...args: unknown[]) { - return identifyMock(args); - } +import { LD_KIND_PREFIX } from './FeatureFlags.const'; +import { FeatureFlagsService } from './FeatureFlagsService'; +import { getDefaultFeatureFlagsService } from './featureFlagsServiceInstance'; +import { IFeatureFlagsService } from './IFeatureFlagsService'; +import { ILogger } from '../types/logger'; - public boolVariation(...args: unknown[]) { - return boolVariationMock(args); - } +class TestLaunchDarklyClient { + static constructorSpy: jest.Mock; - public on(...args: unknown[]) { - onMock(args); + constructor(...args: unknown[]) { + if (TestLaunchDarklyClient.constructorSpy) { + TestLaunchDarklyClient.constructorSpy(...args); + } } -} -enum AutoEnvAttributesMock { - Disabled = 0, - Enabled = 1, + init() {} + identify() {} + boolVariation() {} + on() {} } -jest.mock('@launchdarkly/react-native-client-sdk', () => ({ - ReactNativeLDClient: LaunchDarklyMockClass, - AutoEnvAttributes: AutoEnvAttributesMock, -})); +type TestFeatureFlagsService = IFeatureFlagsService & { + logger: ILogger; + client: ReactNativeLDClient; + getLaunchDarklyClientClass: FeatureFlagsService['getLaunchDarklyClientClass']; + getLaunchDarklyMobileKey: FeatureFlagsService['getLaunchDarklyMobileKey']; +}; const MOCK_LD_CLIENT_ID: string | undefined = 'MOCK_LD_CLIENT_ID_123'; -jest.mock('../constants', () => ({ - LAUNCHDARKLY_MOBILE_KEY: MOCK_LD_CLIENT_ID, -})); +describe('Test FeatureFlagsService', () => { + let service: TestFeatureFlagsService; -jest.mock('../services', () => ({ - Logger: { - log: jest.fn(), - warn: jest.fn(), - error: jest.fn(), - info: jest.fn(), - }, -})); + beforeEach(() => { + TestLaunchDarklyClient.constructorSpy = jest.fn(); -import { LD_KIND_PREFIX } from './FeatureFlags.const'; -import { FeatureFlagsService } from './FeatureFlagsService'; + service = + getDefaultFeatureFlagsService() as never as TestFeatureFlagsService; -describe('Test FeatureFlagsService', () => { - beforeAll(() => { - FeatureFlagsService.init(); - }); + jest.spyOn(service.logger, 'log').mockReturnValue(undefined); - beforeEach(() => { - constructorMock.mockReset(); - initMock.mockReset(); - identifyMock.mockReset().mockResolvedValue(0); - boolVariationMock.mockReset().mockResolvedValue(true); - onMock.mockReset(); + jest + .spyOn(service, 'getLaunchDarklyClientClass') + .mockReturnValue(TestLaunchDarklyClient as never); + + jest + .spyOn(service, 'getLaunchDarklyMobileKey') + .mockReturnValue(MOCK_LD_CLIENT_ID); }); it('Should pass LAUNCHDARKLY_MOBILE_KEY into constructor of LaunchDarkly class', async () => { - FeatureFlagsService.init(); + service.init(); + + expect(TestLaunchDarklyClient.constructorSpy).toHaveBeenCalledTimes(1); - expect(constructorMock).toHaveBeenCalledTimes(1); - expect(constructorMock).toHaveBeenCalledWith([ + expect(TestLaunchDarklyClient.constructorSpy).toHaveBeenCalledWith( MOCK_LD_CLIENT_ID, - AutoEnvAttributesMock.Disabled, + 0, {}, - ]); + ); }); it('Should login', async () => { - await FeatureFlagsService.login('mock-user-id'); - - expect(identifyMock).toHaveBeenCalledTimes(1); - expect(identifyMock).toHaveBeenCalledWith([ - { - kind: LD_KIND_PREFIX, - key: `${LD_KIND_PREFIX}-mock-user-id`, - }, - ]); + const identifySpy = jest.spyOn(service.client, 'identify'); + + await service.login('mock-user-id'); + + expect(identifySpy).toHaveBeenCalledTimes(1); + expect(identifySpy).toHaveBeenCalledWith({ + kind: LD_KIND_PREFIX, + key: `${LD_KIND_PREFIX}-mock-user-id`, + }); }); it('Should logout', async () => { - FeatureFlagsService.logout(); - - expect(identifyMock).toHaveBeenCalledTimes(1); - expect(identifyMock).toHaveBeenCalledWith([ - { - anonymous: true, - key: '', - kind: 'user', - }, - ]); + const identifySpy = jest.spyOn(service.client, 'identify'); + + await service.logout(); + + expect(identifySpy).toHaveBeenCalledTimes(1); + expect(identifySpy).toHaveBeenCalledWith({ + anonymous: true, + key: '', + kind: 'user', + }); }); - it('Should resolve boolean', async () => { - // await required for mockResolvedValue Promise - const flagValue = await FeatureFlagsService.evaluateFlag('my-flag'); + it('Should resolve boolean', () => { + const boolVariationSpy = jest + .spyOn(service.client, 'boolVariation') + .mockReturnValue(true); + + const flagValue = service.evaluateFlag('my-flag'); - expect(boolVariationMock).toHaveBeenCalledTimes(1); - expect(boolVariationMock).toHaveBeenCalledWith(['my-flag', false]); expect(flagValue).toBe(true); + + expect(boolVariationSpy).toHaveBeenCalledTimes(1); + expect(boolVariationSpy).toHaveBeenCalledWith('my-flag', false); }); - it('Should set onChange handler', async () => { + it('Should set onChange handler', () => { + const onSpy = jest.spyOn(service.client, 'on'); const changeHandler = jest.fn(); - FeatureFlagsService.setChangeHandler(changeHandler); - expect(onMock).toHaveBeenCalledTimes(1); - expect(onMock).toHaveBeenCalledWith(['change', changeHandler]); + service.setChangeHandler(changeHandler); + + expect(onSpy).toHaveBeenCalledTimes(1); + expect(onSpy).toHaveBeenCalledWith('change', changeHandler); }); }); diff --git a/src/shared/lib/featureFlags/FeatureFlagsService.ts b/src/shared/lib/featureFlags/FeatureFlagsService.ts index f3546552c..e9a58c8ae 100644 --- a/src/shared/lib/featureFlags/FeatureFlagsService.ts +++ b/src/shared/lib/featureFlags/FeatureFlagsService.ts @@ -5,64 +5,87 @@ import { } from '@launchdarkly/react-native-client-sdk'; import { LD_KIND_PREFIX } from './FeatureFlags.const'; +import { IFeatureFlagsService } from './IFeatureFlagsService'; import { LAUNCHDARKLY_MOBILE_KEY } from '../constants'; -import { getDefaultLogger } from '../services/loggerInstance'; +import { ILogger } from '../types/logger'; -let ldClient: ReactNativeLDClient; +export class FeatureFlagsService implements IFeatureFlagsService { + private logger: ILogger; + private client: ReactNativeLDClient | undefined; + + constructor(logger: ILogger) { + this.logger = logger; + } + + init(): ReactNativeLDClient { + if (!this.client) { + this.logger.log( + '[FeatureFlagsService]: Create and init LaunchDarkly client', + ); + + const LaunchDarklyClient = this.getLaunchDarklyClientClass(); + this.client = new LaunchDarklyClient( + this.getLaunchDarklyMobileKey(), + AutoEnvAttributes.Disabled, + {}, + ); + } + return this.client; + } -export const FeatureFlagsService = { - async init(): Promise { - getDefaultLogger().log( - '[FeatureFlagsService]: Create and init LaunchDarkly ldClient', - ); - ldClient = new ReactNativeLDClient( - LAUNCHDARKLY_MOBILE_KEY, - AutoEnvAttributes.Disabled, - {}, - ); - return ldClient; - }, async login(userId: string): Promise { - if (!ldClient) { + if (!this.client) { return; } + const context = { kind: LD_KIND_PREFIX, key: `${LD_KIND_PREFIX}-${userId}`, }; - return ldClient.identify(context); - }, + return this.client.identify(context); + } + async logout(): Promise { - if (!ldClient) { + if (!this.client) { return; } - return ldClient.identify({ + + return this.client.identify({ // The key attribute is required and should be empty // The SDK will automatically generate a unique, stable key key: '', kind: 'user', anonymous: true, }); - }, + } + evaluateFlag(flag: string): boolean { - if (!ldClient) { + if (!this.client) { return false; } - return ldClient.boolVariation(flag, false); - }, - setChangeHandler( - changeHandler: (ctx: LDContext, changedKeys: string[]) => void, - ): void { - if (!ldClient) { + return this.client.boolVariation(flag, false); + } + + setChangeHandler(fn: (ctx: LDContext, changedKeys: string[]) => void): void { + if (!this.client) { return; } - ldClient.on('change', changeHandler); - }, - removeChangeHandler(changeHandler: Function): void { - if (!ldClient) { + this.client.on('change', fn); + } + + removeChangeHandler(fn: Function): void { + if (!this.client) { return; } - ldClient.off('change', changeHandler); - }, -}; + this.client.off('change', fn); + } + + private getLaunchDarklyClientClass() { + return ReactNativeLDClient; + } + + private getLaunchDarklyMobileKey() { + return LAUNCHDARKLY_MOBILE_KEY; + } +} diff --git a/src/shared/lib/featureFlags/IFeatureFlagsService.ts b/src/shared/lib/featureFlags/IFeatureFlagsService.ts new file mode 100644 index 000000000..0c6907e2d --- /dev/null +++ b/src/shared/lib/featureFlags/IFeatureFlagsService.ts @@ -0,0 +1,13 @@ +import { + LDContext, + ReactNativeLDClient, +} from '@launchdarkly/react-native-client-sdk'; + +export interface IFeatureFlagsService { + init(): ReactNativeLDClient; + login(userId: string): Promise; + logout(): Promise; + evaluateFlag(flag: string): boolean; + setChangeHandler(fn: (ctx: LDContext, changedKeys: string[]) => void): void; + removeChangeHandler(fn: Function): void; +} diff --git a/src/shared/lib/featureFlags/featureFlagsServiceInstance.ts b/src/shared/lib/featureFlags/featureFlagsServiceInstance.ts new file mode 100644 index 000000000..1f4df7a09 --- /dev/null +++ b/src/shared/lib/featureFlags/featureFlagsServiceInstance.ts @@ -0,0 +1,10 @@ +import { FeatureFlagsService } from './FeatureFlagsService'; +import { getDefaultLogger } from '../services/loggerInstance'; + +let instance: FeatureFlagsService; +export const getDefaultFeatureFlagsService = () => { + if (!instance) { + instance = new FeatureFlagsService(getDefaultLogger()); + } + return instance; +}; diff --git a/src/shared/lib/hooks/useFeatureFlags.ts b/src/shared/lib/hooks/useFeatureFlags.ts index dfda31951..dd9fcaa68 100644 --- a/src/shared/lib/hooks/useFeatureFlags.ts +++ b/src/shared/lib/hooks/useFeatureFlags.ts @@ -4,7 +4,7 @@ import { FeatureFlagsKeys, FeatureFlags, } from '../featureFlags/FeatureFlags.types'; -import { FeatureFlagsService } from '../featureFlags/FeatureFlagsService'; +import { getDefaultFeatureFlagsService } from '../featureFlags/featureFlagsServiceInstance'; export const useFeatureFlags = () => { const [flags, setFlags] = useState>({}); @@ -12,9 +12,9 @@ export const useFeatureFlags = () => { const onChangeHandler = useCallback(() => updateFeatureFlags(), []); useEffect(() => { - FeatureFlagsService.setChangeHandler(onChangeHandler); + getDefaultFeatureFlagsService().setChangeHandler(onChangeHandler); return () => { - FeatureFlagsService.removeChangeHandler(onChangeHandler); + getDefaultFeatureFlagsService().removeChangeHandler(onChangeHandler); }; }, [onChangeHandler]); @@ -26,7 +26,7 @@ export const useFeatureFlags = () => { const features: FeatureFlags = {}; keys.forEach( key => - (features[key] = FeatureFlagsService.evaluateFlag( + (features[key] = getDefaultFeatureFlagsService().evaluateFlag( FeatureFlagsKeys[key], )), ); diff --git a/src/shared/lib/services/Logger.test.ts b/src/shared/lib/services/Logger.test.ts index 7ec11c820..2d140aab9 100644 --- a/src/shared/lib/services/Logger.test.ts +++ b/src/shared/lib/services/Logger.test.ts @@ -1,15 +1,13 @@ -import { FileSystem } from 'react-native-file-access'; +import { FileStat, FileSystem } from 'react-native-file-access'; import { FileLogger } from 'react-native-file-logger'; import { getDefaultFileService } from '@app/shared/api/services/fileServiceInstance'; +import { IFileService } from '@app/shared/api/services/IFileService'; import { Logger } from './Logger'; - -jest.mock('@shared/api', () => ({ - FileService: { - checkIfLogsExist: jest.fn(), - }, -})); +import { getDefaultLogger } from './loggerInstance'; +import { ILogger, NamePath } from '../types/logger'; +import { IMutex } from '../utils/common'; jest.mock('react-native-file-access', () => ({ FileSystem: { @@ -20,10 +18,6 @@ jest.mock('react-native-file-access', () => ({ }, })); -jest.mock('@shared/lib', () => ({ - isAppOnline: jest.fn(), -})); - jest.mock('react-native-file-logger', () => ({ FileLogger: { getLogFilePaths: jest.fn(), @@ -39,28 +33,45 @@ jest.mock('react-native-file-logger', () => ({ }, })); +type TestLogger = ILogger & { + mutex: IMutex; + withTime: Logger['withTime']; + checkIfFilesExist: Logger['checkIfFilesExist']; + getLogFiles: Logger['getLogFiles']; + onBeforeSendLogs: Logger['onBeforeSendLogs']; + sendInternal: Logger['sendInternal']; + isAppOnline: Logger['isAppOnline']; +}; + describe('Logger: regular tests', () => { + let logger: TestLogger; + let fileService: IFileService; + + beforeEach(() => { + logger = getDefaultLogger() as never as TestLogger; + fileService = getDefaultFileService(); + }); + it('should add a timestamp to the message', async () => { const input = 'Some input string'; - // @ts-expect-error - const result = getDefaultLogger().withTime(input); + const result = logger.withTime(input); expect(result).toHaveLength(input.length + 10); expect(result).toContain(`: ${input}`); }); it('should return empty array for no files', async () => { - const files: string[] = []; + const files: NamePath[] = []; - require('@shared/api').FileService.checkIfLogsExist.mockResolvedValue({ + jest.spyOn(fileService, 'checkIfLogsExist').mockResolvedValue({ data: { result: [], }, - }); + } as never); + + const result = await logger.checkIfFilesExist(files); - // @ts-expect-error - const result = await getDefaultLogger().checkIfFilesExist(files); expect(result).toEqual([]); }); @@ -74,14 +85,13 @@ describe('Logger: regular tests', () => { { fileId: 'file2.log', uploaded: false, fileSize: 512 }, ]; - require('@shared/api').FileService.checkIfLogsExist.mockResolvedValue({ + jest.spyOn(fileService, 'checkIfLogsExist').mockResolvedValue({ data: { result: response, }, - }); + } as never); - // @ts-expect-error - const result = await getDefaultLogger().checkIfFilesExist(files); + const result = await logger.checkIfFilesExist(files); expect(result).toEqual([ { @@ -100,23 +110,24 @@ describe('Logger: regular tests', () => { }); it('should return file information for existing log files', async () => { - (FileLogger.getLogFilePaths as jest.Mock).mockReturnValue([ - '/path/to/file1.log', - '/path/to/file2.log', - ]); - - (FileSystem.stat as jest.Mock).mockImplementation(async (path: string) => { - if (path === '/path/to/file1.log') { - return { filename: 'file1.log', size: 1024 }; - } else if (path === '/path/to/file2.log') { - return { filename: 'file2.log', size: 512 }; - } - - throw new Error('File not found'); - }); - - // @ts-expect-error - const result = await getDefaultLogger().getLogFiles(); + jest + .spyOn(FileLogger, 'getLogFilePaths') + .mockResolvedValue(['/path/to/file1.log', '/path/to/file2.log']); + + jest.spyOn(FileSystem, 'stat').mockImplementation( + (path: string) => + new Promise((resolve, reject) => { + if (path === '/path/to/file1.log') { + resolve({ filename: 'file1.log', size: 1024 } as FileStat); + } else if (path === '/path/to/file2.log') { + resolve({ filename: 'file2.log', size: 512 } as FileStat); + } else { + reject(new Error('File not found')); + } + }), + ); + + const result = await logger.getLogFiles(); expect(result).toEqual([ { @@ -134,26 +145,23 @@ describe('Logger: regular tests', () => { }); describe('Logger: test sending files', () => { - it('Should do 5 waiting mutex attempts (or 7 check if mutex is busy) when sending logs', async () => { - const logger = new Logger(getDefaultFileService()); + let logger: TestLogger; - //@ts-expect-error - logger.onBeforeSendLogs = jest.fn(); - //@ts-expect-error - logger.sendInternal = jest.fn(); - //@ts-expect-error - logger.isAppOnline = jest.fn().mockResolvedValue(true); + beforeEach(() => { + logger = getDefaultLogger() as never as TestLogger; + }); - const isBusyMock = jest.fn(() => true); - //@ts-expect-error - logger.mutex.isBusy = isBusyMock; + it('Should do 5 waiting mutex attempts (or 7 check if mutex is busy) when sending logs', async () => { + jest.spyOn(logger, 'onBeforeSendLogs').mockResolvedValue(undefined); + jest.spyOn(logger, 'sendInternal').mockResolvedValue(true); + jest.spyOn(logger, 'isAppOnline').mockResolvedValue(true); - //@ts-expect-error + jest.spyOn(logger.mutex, 'isBusy').mockReturnValue(true); logger.mutex.setBusy(); const result = await logger.send(); expect(result).toEqual(false); - expect(isBusyMock).toBeCalledTimes(7); + expect(logger.mutex.isBusy).toHaveBeenCalledTimes(7); }); }); diff --git a/src/shared/lib/services/Logger.ts b/src/shared/lib/services/Logger.ts index a7b159fc0..5df9fec1f 100644 --- a/src/shared/lib/services/Logger.ts +++ b/src/shared/lib/services/Logger.ts @@ -7,7 +7,13 @@ import { IFileService } from '@app/shared/api/services/IFileService'; import { IS_ANDROID, IS_IOS } from '../constants'; import { getNotificationSettingsData } from '../permissions/notificationPermissions'; -import { ILogger } from '../types/logger'; +import { + DeviceInfoLogObject, + FileExists, + ILogger, + NamePath, + NamePathSize, +} from '../types/logger'; import { IMutex, Mutex, @@ -17,28 +23,6 @@ import { } from '../utils/common'; import { isAppOnline } from '../utils/networkHelpers'; -type NamePath = { - fileName: string; - filePath: string; -}; - -type NamePathSize = { - size: number; -} & NamePath; - -type FileExists = { - exists: boolean; -} & NamePathSize; - -type DeviceInfoLogObject = { - brand: string; - readableVersion: string; - buildNumber: string; - firstInstallTime: number; - freeDiskStorage: number; - lastUpdateTime: number; -}; - export class Logger implements ILogger { private mutex: IMutex; diff --git a/src/shared/lib/types/logger.ts b/src/shared/lib/types/logger.ts index f80dfb980..c2e565b61 100644 --- a/src/shared/lib/types/logger.ts +++ b/src/shared/lib/types/logger.ts @@ -1,3 +1,25 @@ +export type NamePath = { + fileName: string; + filePath: string; +}; + +export type NamePathSize = { + size: number; +} & NamePath; + +export type FileExists = { + exists: boolean; +} & NamePathSize; + +export type DeviceInfoLogObject = { + brand: string; + readableVersion: string; + buildNumber: string; + firstInstallTime: number; + freeDiskStorage: number; + lastUpdateTime: number; +}; + export interface ILogger { log: (message: string) => void; info: (message: string) => void; diff --git a/src/shared/lib/utils/survey/survey.test.ts b/src/shared/lib/utils/survey/survey.test.ts index b0c3e8397..a27d877a5 100644 --- a/src/shared/lib/utils/survey/survey.test.ts +++ b/src/shared/lib/utils/survey/survey.test.ts @@ -35,7 +35,7 @@ describe('Test getEntityProgress', () => { 'mock-applet-id-1', 'mock-entity-id-1', 'mock-event-id-1', - 'mock-target-subject-id-1', + null, [], ); @@ -47,7 +47,7 @@ describe('Test getEntityProgress', () => { 'mock-applet-id-1', 'mock-entity-id-1', 'mock-event-id-1', - 'mock-target-subject-id-1', + null, [{ appletId: 'mock-applet-id-1' } as never as EntityProgression], ); @@ -59,7 +59,7 @@ describe('Test getEntityProgress', () => { 'mock-applet-id-1', 'mock-entity-id-1', 'mock-event-id-1', - 'mock-target-subject-id-1', + null, [ { appletId: 'mock-applet-id-1', @@ -76,7 +76,7 @@ describe('Test getEntityProgress', () => { 'mock-applet-id-1', 'mock-entity-id-1', 'mock-event-id-1', - 'mock-target-subject-id-1', + null, [ { appletId: 'mock-applet-id-1', @@ -94,14 +94,14 @@ describe('Test getEntityProgress', () => { 'mock-applet-id-1', 'mock-entity-id-1', 'mock-event-id-1', - 'mock-target-subject-id-1', + null, [ { status: 'in-progress', appletId: 'mock-applet-id-1', entityId: 'mock-entity-id-1', eventId: 'mock-event-id-1', - targetSubjectId: 'mock-target-subject-id-1', + targetSubjectId: null, } as EntityProgression, ], ); @@ -177,7 +177,7 @@ describe('Test isReadyForAutocompletion', () => { entityId: 'mock-entity-id-1', eventId: 'mock-event-id-1', entityType: 'regular', - targetSubjectId: 'mock-target-subject-id-1', + targetSubjectId: null, }, [], ); @@ -192,7 +192,7 @@ describe('Test isReadyForAutocompletion', () => { entityId: 'mock-entity-id-1', eventId: 'mock-event-id-1', entityType: 'regular', - targetSubjectId: 'mock-target-subject-id-1', + targetSubjectId: null, }, [ { @@ -200,7 +200,7 @@ describe('Test isReadyForAutocompletion', () => { appletId: 'mock-applet-id-1', entityId: 'mock-entity-id-1', eventId: 'mock-event-id-1', - targetSubjectId: 'mock-target-subject-id-1', + targetSubjectId: null, availableUntilTimestamp: null, } as EntityProgressionInProgress, ], @@ -216,7 +216,7 @@ describe('Test isReadyForAutocompletion', () => { entityId: 'mock-entity-id-1', eventId: 'mock-event-id-1', entityType: 'regular', - targetSubjectId: 'mock-target-subject-id-1', + targetSubjectId: null, }, [ { @@ -224,7 +224,7 @@ describe('Test isReadyForAutocompletion', () => { appletId: 'mock-applet-id-1', entityId: 'mock-entity-id-1', eventId: 'mock-event-id-1', - targetSubjectId: 'mock-target-subject-id-1', + targetSubjectId: null, availableUntilTimestamp: new Date(Date.now() + 10000).getTime(), } as EntityProgressionInProgress, ], @@ -240,7 +240,7 @@ describe('Test isReadyForAutocompletion', () => { entityId: 'mock-entity-id-1', eventId: 'mock-event-id-1', entityType: 'regular', - targetSubjectId: 'mock-target-subject-id-1', + targetSubjectId: null, }, [ { @@ -248,7 +248,7 @@ describe('Test isReadyForAutocompletion', () => { appletId: 'mock-applet-id-1', entityId: 'mock-entity-id-1', eventId: 'mock-event-id-1', - targetSubjectId: 'mock-target-subject-id-1', + targetSubjectId: null, availableUntilTimestamp: new Date(Date.now() - 10000).getTime(), } as EntityProgressionInProgress, ], diff --git a/src/shared/lib/utils/tests/file.test.js b/src/shared/lib/utils/tests/file.test.ts similarity index 100% rename from src/shared/lib/utils/tests/file.test.js rename to src/shared/lib/utils/tests/file.test.ts diff --git a/src/shared/ui/survey/AudioStimulusItem.tsx b/src/shared/ui/survey/AudioStimulusItem.tsx index 7d1b8c97e..dac0c660f 100644 --- a/src/shared/ui/survey/AudioStimulusItem.tsx +++ b/src/shared/ui/survey/AudioStimulusItem.tsx @@ -67,9 +67,9 @@ export const AudioStimulusItem: FC = ({ const canPlay = replayIsAllowed || playbackCount === 0; if (canPause) { - pause(); + pause().catch(console.error); } else if (canPlay) { - play(uri, () => onFinish(true)); + play(uri, () => onFinish(true)).catch(console.error); } }; diff --git a/src/shared/ui/survey/tests/AudioPlayer.test.tsx b/src/shared/ui/survey/tests/AudioPlayer.test.tsx index 4c7780ee9..bc880df71 100644 --- a/src/shared/ui/survey/tests/AudioPlayer.test.tsx +++ b/src/shared/ui/survey/tests/AudioPlayer.test.tsx @@ -1,25 +1,10 @@ import renderer from 'react-test-renderer'; import { TamaguiProvider } from '@app/app/ui/AppProvider/TamaguiProvider'; +import * as useAudioPlayerHooks from '@app/shared/lib/hooks/useAudioPlayer'; import { AudioPlayer } from '../AudioPlayer'; -jest.mock('@react-navigation/native', () => ({ - useFocusEffect: jest.fn(), -})); - -jest.mock('@app/shared/lib/hooks/useAudioPlayer', () => - jest.fn().mockImplementation(() => ({ - play: jest.fn(), - pause: jest.fn(), - togglePlay: jest.fn(), - destroy: jest.fn(), - isPlaying: false, - playbackCount: 1, - isLoading: false, - })), -); - describe('Test AudioPlayer', () => { afterEach(() => { jest.clearAllMocks(); @@ -40,15 +25,15 @@ describe('Test AudioPlayer', () => { }); it('Should render pause button', () => { - // jest.spyOn(hooks, 'useAudioPlayer').mockReturnValue({ - // play: jest.fn(), - // pause: jest.fn(), - // togglePlay: jest.fn(), - // destroy: jest.fn(), - // isPlaying: true, - // playbackCount: 0, - // isLoading: false, - // }); + jest.spyOn(useAudioPlayerHooks, 'useAudioPlayer').mockReturnValue({ + play: jest.fn(), + pause: jest.fn(), + togglePlay: jest.fn(), + destroy: jest.fn(), + isPlaying: true, + playbackCount: 0, + isLoading: false, + }); const audioPlayer = renderer.create( diff --git a/src/shared/ui/survey/tests/AudioStimulusItem.test.tsx b/src/shared/ui/survey/tests/AudioStimulusItem.test.tsx index 458349fcb..399123788 100644 --- a/src/shared/ui/survey/tests/AudioStimulusItem.test.tsx +++ b/src/shared/ui/survey/tests/AudioStimulusItem.test.tsx @@ -1,27 +1,12 @@ import renderer from 'react-test-renderer'; import { TamaguiProvider } from '@app/app/ui/AppProvider/TamaguiProvider'; +import * as useAudioPlayerHooks from '@app/shared/lib/hooks/useAudioPlayer'; import { ActivityIndicator } from '../../ActivityIndicator'; import { AudioStimulusItem } from '../AudioStimulusItem'; -jest.mock('@app/shared/lib/hooks/useAudioPlayer', () => - jest.fn().mockImplementation(() => ({ - play: jest.fn(), - pause: jest.fn(), - togglePlay: jest.fn(), - destroy: jest.fn(), - isPlaying: false, - playbackCount: 1, - isLoading: false, - })), -); - describe('Test AudioStimulusItem', () => { - afterEach(() => { - jest.clearAllMocks(); - }); - it('Should render play button', () => { const audioPlayer = renderer.create( @@ -44,15 +29,15 @@ describe('Test AudioStimulusItem', () => { }); it('Should render pause button', () => { - // jest.spyOn(hooks, 'useAudioPlayer').mockReturnValue({ - // play: jest.fn(), - // pause: jest.fn(), - // togglePlay: jest.fn(), - // destroy: jest.fn(), - // isPlaying: true, - // playbackCount: 0, - // isLoading: false, - // }); + jest.spyOn(useAudioPlayerHooks, 'useAudioPlayer').mockReturnValue({ + play: jest.fn(), + pause: jest.fn(), + togglePlay: jest.fn(), + destroy: jest.fn(), + isPlaying: true, + playbackCount: 0, + isLoading: false, + }); const audioPlayer = renderer.create( @@ -75,15 +60,15 @@ describe('Test AudioStimulusItem', () => { }); it('Should render activity indicator while loading', () => { - // jest.spyOn(hooks, 'useAudioPlayer').mockReturnValue({ - // play: jest.fn(), - // pause: jest.fn(), - // togglePlay: jest.fn(), - // destroy: jest.fn(), - // isPlaying: false, - // playbackCount: 0, - // isLoading: true, - // }); + jest.spyOn(useAudioPlayerHooks, 'useAudioPlayer').mockReturnValue({ + play: jest.fn(), + pause: jest.fn(), + togglePlay: jest.fn(), + destroy: jest.fn(), + isPlaying: false, + playbackCount: 0, + isLoading: true, + }); const audioPlayer = renderer.create( @@ -110,15 +95,15 @@ describe('Test AudioStimulusItem', () => { }); it('Should render correct button text if replay is not allowed', () => { - // jest.spyOn(hooks, 'useAudioPlayer').mockReturnValue({ - // play: jest.fn(), - // pause: jest.fn(), - // togglePlay: jest.fn(), - // destroy: jest.fn(), - // isPlaying: true, - // playbackCount: 0, - // isLoading: false, - // }); + jest.spyOn(useAudioPlayerHooks, 'useAudioPlayer').mockReturnValue({ + play: jest.fn(), + pause: jest.fn(), + togglePlay: jest.fn(), + destroy: jest.fn(), + isPlaying: true, + playbackCount: 0, + isLoading: false, + }); const audioPlayer = renderer.create( diff --git a/src/widgets/activity-group/model/factories/ActivityGroupsBuilder.test.ts b/src/widgets/activity-group/model/factories/ActivityGroupsBuilder.test.ts index b56cfc20c..9744e7233 100644 --- a/src/widgets/activity-group/model/factories/ActivityGroupsBuilder.test.ts +++ b/src/widgets/activity-group/model/factories/ActivityGroupsBuilder.test.ts @@ -14,7 +14,7 @@ import { ActivityPipelineType } from '@app/abstract/lib/types/activityPipeline'; import { EntityProgression, EntityProgressionCompleted, - EntityProgressionInProgress, + EntityProgressionInProgressActivityFlow, } from '@app/abstract/lib/types/entityProgress'; import { AvailabilityType, @@ -39,38 +39,26 @@ import { GroupsBuildContext, } from '../../lib/types/activityGroupsBuilder'; -jest.mock( - '@app/shared/lib/constants', - () => - ({ - ...jest.requireActual('@app/shared/lib/constants'), - STORE_ENCRYPTION_KEY: '12345', - }) as unknown, -); - -const getProgression = ( - startAt: Date, - endAt: Date | null, -): EntityProgression => { - const progression: EntityProgressionInProgress = { +const getProgressions = (startAt: Date, endAt: Date | null) => { + const progression: EntityProgression = { status: 'in-progress', appletId: 'test-applet-id-1', entityType: 'activity', entityId: 'test-entity-id-1', eventId: 'test-event-id-1', - targetSubjectId: 'test-target-subject-id-1', + targetSubjectId: null, startedAtTimestamp: startAt.getTime(), availableUntilTimestamp: null, }; - if (endAt) { - const completedProgression = - progression as never as EntityProgressionCompleted; - completedProgression.status = 'completed'; - completedProgression.endedAtTimestamp = endAt.getTime(); - } + (progression as never as EntityProgressionCompleted).endedAtTimestamp = + endAt?.getTime() || null; + + return [progression]; +}; - return progression; +const getEmptyProgressions = (): EntityProgression[] => { + return []; }; const getActivity = (): Entity => { @@ -240,9 +228,11 @@ describe('ActivityGroupsBuilder', () => { it('Should return group item when event is always-available and startAt is set in progress record', () => { const startAt = new Date(2023, 8, 1); + const entityProgressions = getProgressions(startAt, null); + const input: GroupsBuildContext = { allAppletActivities: [], - entityProgressions: [], + entityProgressions, appletId: 'test-applet-id-1', }; @@ -252,7 +242,7 @@ describe('ActivityGroupsBuilder', () => { scheduledAt: startAt, }); - const result = builder.buildInProgress(input.appletId, [eventEntity]); + const result = builder.buildInProgress('test-applet-id-1', [eventEntity]); const expectedItem: ActivityListItem = getExpectedInProgressItem(); expectedItem.availableTo = undefined; @@ -270,9 +260,11 @@ describe('ActivityGroupsBuilder', () => { const startAt = new Date(2023, 8, 1, 0, 0, 0); const endAt = new Date(2023, 8, 1, 15, 30, 0); + const entityProgressions = getProgressions(startAt, endAt); + const input: GroupsBuildContext = { allAppletActivities: [], - entityProgressions: [], + entityProgressions, appletId: 'test-applet-id-1', }; @@ -282,7 +274,7 @@ describe('ActivityGroupsBuilder', () => { scheduledAt: startAt, }); - const result = builder.buildInProgress(input.appletId, [eventEntity]); + const result = builder.buildInProgress('test-applet-id-1', [eventEntity]); const expectedResult: ActivityListGroup = { name: 'additional:in_progress', @@ -296,9 +288,11 @@ describe('ActivityGroupsBuilder', () => { it('Should not return group item when event is always-available and no any progress record', () => { const startAt = new Date(2023, 8, 1, 0, 0, 0); + const entityProgressions = getEmptyProgressions(); + const input: GroupsBuildContext = { allAppletActivities: [], - entityProgressions: [], + entityProgressions, appletId: 'test-applet-id-1', }; @@ -308,7 +302,7 @@ describe('ActivityGroupsBuilder', () => { scheduledAt: startAt, }); - const result = builder.buildInProgress(input.appletId, [eventEntity]); + const result = builder.buildInProgress('test-applet-id-1', [eventEntity]); const expectedResult: ActivityListGroup = { name: 'additional:in_progress', @@ -322,9 +316,11 @@ describe('ActivityGroupsBuilder', () => { it('Should return group-item when event is scheduled and getNow is out of start-end dates', () => { const date = new Date(2023, 8, 1); + const entityProgressions = getProgressions(date, null); + const input: GroupsBuildContext = { allAppletActivities: [], - entityProgressions: [], + entityProgressions, appletId: 'test-applet-id-1', }; @@ -338,7 +334,7 @@ describe('ActivityGroupsBuilder', () => { mockGetNow(builder, addDays(date, 10)); - const result = builder.buildInProgress(input.appletId, [eventEntity]); + const result = builder.buildInProgress('test-applet-id-1', [eventEntity]); const expectedItem: ActivityListItem = getExpectedInProgressItem(); @@ -355,9 +351,11 @@ describe('ActivityGroupsBuilder', () => { const date = new Date(2023, 8, 1, 15, 30, 0); const day = startOfDay(date); + const entityProgressions = getProgressions(date, null); + const input: GroupsBuildContext = { allAppletActivities: [], - entityProgressions: [], + entityProgressions, appletId: 'test-applet-id-1', }; @@ -376,7 +374,7 @@ describe('ActivityGroupsBuilder', () => { mockGetNow(builder, mockedNowDate); - const result = builder.buildInProgress(input.appletId, [eventEntity]); + const result = builder.buildInProgress('test-applet-id-1', [eventEntity]); const expectedItem: ActivityListItem = getExpectedInProgressItem(); expectedItem.isTimerSet = true; @@ -396,9 +394,11 @@ describe('ActivityGroupsBuilder', () => { const date = new Date(2023, 8, 1, 15, 30, 0); const day = startOfDay(date); + const entityProgressions = getProgressions(date, null); + const input: GroupsBuildContext = { allAppletActivities: [], - entityProgressions: [], + entityProgressions, appletId: 'test-applet-id-1', }; @@ -417,7 +417,7 @@ describe('ActivityGroupsBuilder', () => { mockGetNow(builder, mockedNowDate); - const result = builder.buildInProgress(input.appletId, [eventEntity]); + const result = builder.buildInProgress('test-applet-id-1', [eventEntity]); const expectedItem: ActivityListItem = getExpectedInProgressItem(); expectedItem.isTimerSet = true; @@ -437,9 +437,11 @@ describe('ActivityGroupsBuilder', () => { const date = new Date(2023, 8, 1, 15, 30, 0); const day = startOfDay(date); + const entityProgressions = getProgressions(date, null); + const input: GroupsBuildContext = { allAppletActivities: [], - entityProgressions: [], + entityProgressions, appletId: 'test-applet-id-1', }; @@ -458,7 +460,7 @@ describe('ActivityGroupsBuilder', () => { mockGetNow(builder, mockedNowDate); - const result = builder.buildInProgress(input.appletId, [eventEntity]); + const result = builder.buildInProgress('test-applet-id-1', [eventEntity]); const expectedItem: ActivityListItem = getExpectedInProgressItem(); expectedItem.isTimerSet = true; @@ -480,9 +482,11 @@ describe('ActivityGroupsBuilder', () => { const startAt = new Date(2023, 8, 1, 15, 0, 0); const endAt = addHours(startAt, 1); + const entityProgressions = getProgressions(startAt, endAt); + const input: GroupsBuildContext = { allAppletActivities: [], - entityProgressions: [], + entityProgressions, appletId: 'test-applet-id-1', }; @@ -498,7 +502,7 @@ describe('ActivityGroupsBuilder', () => { mockGetNow(builder, now); - const result = builder.buildAvailable(input.appletId, [eventEntity]); + const result = builder.buildAvailable('test-applet-id-1', [eventEntity]); const expectedItem: ActivityListItem = getExpectedAvailableItem(); expectedItem.availableTo = MIDNIGHT_DATE; @@ -516,9 +520,11 @@ describe('ActivityGroupsBuilder', () => { const startAt = new Date(2023, 8, 1, 15, 0, 0); const endAt = addHours(startAt, 1); + const entityProgressions = getEmptyProgressions(); + const input: GroupsBuildContext = { allAppletActivities: [], - entityProgressions: [], + entityProgressions, appletId: 'test-applet-id-1', }; @@ -534,7 +540,7 @@ describe('ActivityGroupsBuilder', () => { mockGetNow(builder, now); - const result = builder.buildAvailable(input.appletId, [eventEntity]); + const result = builder.buildAvailable('test-applet-id-1', [eventEntity]); const expectedItem: ActivityListItem = getExpectedAvailableItem(); expectedItem.availableTo = MIDNIGHT_DATE; @@ -552,9 +558,11 @@ describe('ActivityGroupsBuilder', () => { const startAt = new Date(2023, 8, 1, 15, 0, 0); const endAt = addHours(startAt, 1); + const entityProgressions = getProgressions(startAt, endAt); + const input: GroupsBuildContext = { allAppletActivities: [], - entityProgressions: [], + entityProgressions, appletId: 'test-applet-id-1', }; @@ -570,7 +578,7 @@ describe('ActivityGroupsBuilder', () => { mockGetNow(builder, now); - const result = builder.buildAvailable(input.appletId, [eventEntity]); + const result = builder.buildAvailable('test-applet-id-1', [eventEntity]); const expectedResult: ActivityListGroup = { name: 'additional:available', @@ -591,9 +599,11 @@ describe('ActivityGroupsBuilder', () => { it(`Should return group-item for scheduled event when periodicity is ${periodicity} and allowAccessBeforeFromTime is false and current time is is allowed time window`, () => { const scheduledAt = new Date(2023, 8, 1, 15, 0, 0); + const entityProgressions = getEmptyProgressions(); + const input: GroupsBuildContext = { allAppletActivities: [], - entityProgressions: [], + entityProgressions, appletId: 'test-applet-id-1', }; @@ -614,7 +624,9 @@ describe('ActivityGroupsBuilder', () => { mockGetNow(builder, new Date(now)); - const result = builder.buildAvailable(input.appletId, [eventEntity]); + const result = builder.buildAvailable('test-applet-id-1', [ + eventEntity, + ]); const expectedItem = getExpectedAvailableItem(); expectedItem.availableTo = new Date(startOfDay(scheduledAt)); @@ -634,9 +646,11 @@ describe('ActivityGroupsBuilder', () => { it('Should return empty for scheduled event when periodicity is Daily and allowAccessBeforeFromTime is false and current time is is allowed time window and start/end dates are in the future in 2-3 months', () => { const scheduledAt = new Date(2023, 8, 1, 15, 0, 0); + const entityProgressions = getEmptyProgressions(); + const input: GroupsBuildContext = { allAppletActivities: [], - entityProgressions: [], + entityProgressions, appletId: 'test-applet-id-1', }; @@ -659,7 +673,7 @@ describe('ActivityGroupsBuilder', () => { mockGetNow(builder, new Date(now)); - let result = builder.buildAvailable(input.appletId, [eventEntity]); + let result = builder.buildAvailable('test-applet-id-1', [eventEntity]); const expectedItem = getExpectedAvailableItem(); expectedItem.availableTo = new Date(startOfDay(scheduledAt)); @@ -672,7 +686,7 @@ describe('ActivityGroupsBuilder', () => { activities: [], }; - result = builder.buildAvailable(input.appletId, [eventEntity]); + result = builder.buildAvailable('test-applet-id-1', [eventEntity]); expect(result).toEqual(expectedEmptyResult); }); @@ -680,9 +694,11 @@ describe('ActivityGroupsBuilder', () => { it('Should return empty for scheduled event when periodicity is Daily and allowAccessBeforeFromTime is false and current time is is allowed time window and start/end dates are in the past 2-3 months', () => { const scheduledAt = new Date(2023, 8, 1, 15, 0, 0); + const entityProgressions = getEmptyProgressions(); + const input: GroupsBuildContext = { allAppletActivities: [], - entityProgressions: [], + entityProgressions, appletId: 'test-applet-id-1', }; @@ -705,7 +721,7 @@ describe('ActivityGroupsBuilder', () => { mockGetNow(builder, new Date(now)); - const result = builder.buildAvailable(input.appletId, [eventEntity]); + const result = builder.buildAvailable('test-applet-id-1', [eventEntity]); const expectedItem = getExpectedAvailableItem(); expectedItem.availableTo = new Date(startOfDay(scheduledAt)); @@ -724,14 +740,14 @@ describe('ActivityGroupsBuilder', () => { it('Should return item for scheduled event when periodicity is Daily and allowAccessBeforeFromTime is false and current time is is allowed time window and start/end dates cover now (-/+ 2 months)', () => { const scheduledAt = new Date(2023, 8, 1, 15, 0, 0); - const progression = getProgression( + const entityProgressions = getProgressions( subDays(scheduledAt, 1), addMinutes(subDays(scheduledAt, 1), 5), ); const input = { allAppletActivities: [], - entityProgressions: [progression], + entityProgressions, appletId: 'test-applet-id-1', }; @@ -754,7 +770,7 @@ describe('ActivityGroupsBuilder', () => { mockGetNow(builder, new Date(now)); - const result = builder.buildAvailable(input.appletId, [eventEntity]); + const result = builder.buildAvailable('test-applet-id-1', [eventEntity]); const expectedItem = getExpectedAvailableItem(); expectedItem.availableTo = new Date(startOfDay(scheduledAt)); @@ -773,9 +789,11 @@ describe('ActivityGroupsBuilder', () => { it('Should not return group-item for scheduled event and periodicity is Weekly and allowAccessBeforeFromTime is false when scheduledToday is false', () => { const scheduledAt = new Date(2023, 8, 1, 15, 0, 0); + const entityProgressions = getEmptyProgressions(); + const input: GroupsBuildContext = { allAppletActivities: [], - entityProgressions: [], + entityProgressions, appletId: 'test-applet-id-1', }; @@ -796,7 +814,7 @@ describe('ActivityGroupsBuilder', () => { mockGetNow(builder, new Date(now)); - const result = builder.buildAvailable(input.appletId, [eventEntity]); + const result = builder.buildAvailable('test-applet-id-1', [eventEntity]); const expectedResult: ActivityListGroup = { name: 'additional:available', @@ -810,9 +828,11 @@ describe('ActivityGroupsBuilder', () => { it('Should not return group-item for scheduled event and periodicity is Weekly and allowAccessBeforeFromTime is false when now time is less than timeFrom', () => { const scheduledAt = new Date(2023, 8, 1, 15, 0, 0); + const entityProgressions = getEmptyProgressions(); + const input: GroupsBuildContext = { allAppletActivities: [], - entityProgressions: [], + entityProgressions, appletId: 'test-applet-id-1', }; @@ -839,16 +859,18 @@ describe('ActivityGroupsBuilder', () => { mockGetNow(builder, new Date(now)); - const result = builder.buildAvailable(input.appletId, [eventEntity]); + const result = builder.buildAvailable('test-applet-id-1', [eventEntity]); expect(result).toEqual(expectedResult); }); it('Should not return group-item for scheduled event and periodicity is Weekly and allowAccessBeforeFromTime is false when now time is more than timeTo', () => { const scheduledAt = new Date(2023, 8, 1, 15, 0, 0); + const entityProgressions = getEmptyProgressions(); + const input: GroupsBuildContext = { allAppletActivities: [], - entityProgressions: [], + entityProgressions, appletId: 'test-applet-id-1', }; @@ -878,16 +900,18 @@ describe('ActivityGroupsBuilder', () => { mockGetNow(builder, new Date(now)); - const result = builder.buildAvailable(input.appletId, [eventEntity]); + const result = builder.buildAvailable('test-applet-id-1', [eventEntity]); expect(result).toEqual(expectedResult); }); it('Should not return group-item for scheduled event and periodicity is Weekly and allowAccessBeforeFromTime is false when completed today is true', () => { const scheduledAt = new Date(2023, 8, 1, 15, 0, 0); + let entityProgressions = getEmptyProgressions(); + let input: GroupsBuildContext = { allAppletActivities: [], - entityProgressions: [], + entityProgressions, appletId: 'test-applet-id-1', }; @@ -910,9 +934,14 @@ describe('ActivityGroupsBuilder', () => { activities: [], }; + entityProgressions = getProgressions( + new Date(scheduledAt), + addMinutes(scheduledAt, 5), + ); + input = { allAppletActivities: [], - entityProgressions: [], + entityProgressions, appletId: 'test-applet-id-1', }; @@ -922,16 +951,18 @@ describe('ActivityGroupsBuilder', () => { mockGetNow(builder, new Date(now)); - const result = builder.buildAvailable(input.appletId, [eventEntity]); + const result = builder.buildAvailable('test-applet-id-1', [eventEntity]); expect(result).toEqual(expectedResult); }); it('5Should not return group-item for scheduled event and periodicity is Weekly and allowAccessBeforeFromTime is false when started yesterday, but not completed yet', () => { const scheduledAt = new Date(2023, 8, 1, 15, 0, 0); + let entityProgressions = getEmptyProgressions(); + let input: GroupsBuildContext = { allAppletActivities: [], - entityProgressions: [], + entityProgressions, appletId: 'test-applet-id-1', }; @@ -954,9 +985,11 @@ describe('ActivityGroupsBuilder', () => { activities: [], }; + entityProgressions = getProgressions(subDays(scheduledAt, 1), null); + input = { allAppletActivities: [], - entityProgressions: [], + entityProgressions, appletId: 'test-applet-id-1', }; @@ -966,7 +999,7 @@ describe('ActivityGroupsBuilder', () => { mockGetNow(builder, new Date(now)); - const result = builder.buildAvailable(input.appletId, [eventEntity]); + const result = builder.buildAvailable('test-applet-id-1', [eventEntity]); expect(result).toEqual(expectedResult); }); @@ -980,9 +1013,11 @@ describe('ActivityGroupsBuilder', () => { it(`Should return group-item for scheduled event when periodicity is ${periodicity} and allowAccessBeforeFromTime is true and current time is less than startTime`, () => { const scheduledAt = new Date(2023, 8, 1, 15, 0, 0); + const entityProgressions = getEmptyProgressions(); + const input: GroupsBuildContext = { allAppletActivities: [], - entityProgressions: [], + entityProgressions, appletId: 'test-applet-id-1', }; @@ -1003,7 +1038,9 @@ describe('ActivityGroupsBuilder', () => { mockGetNow(builder, new Date(now)); - const result = builder.buildAvailable(input.appletId, [eventEntity]); + const result = builder.buildAvailable('test-applet-id-1', [ + eventEntity, + ]); const expectedItem = getExpectedAvailableItem(); expectedItem.availableTo = new Date(startOfDay(scheduledAt)); @@ -1023,9 +1060,11 @@ describe('ActivityGroupsBuilder', () => { it('Should return group-item for scheduled event when periodicity is Daily and allowAccessBeforeFromTime is true and current time is less than startTime and start/end dates are in the future in 2/3 months', () => { const scheduledAt = new Date(2023, 8, 1, 15, 0, 0); + const entityProgressions = getEmptyProgressions(); + const input: GroupsBuildContext = { allAppletActivities: [], - entityProgressions: [], + entityProgressions, appletId: 'test-applet-id-1', }; @@ -1048,7 +1087,7 @@ describe('ActivityGroupsBuilder', () => { eventEntity.event.availability.startDate = addMonths(now, 2); eventEntity.event.availability.endDate = addMonths(now, 3); - let result = builder.buildAvailable(input.appletId, [eventEntity]); + let result = builder.buildAvailable('test-applet-id-1', [eventEntity]); const expectedEmptyResult: ActivityListGroup = { name: 'additional:available', @@ -1056,16 +1095,18 @@ describe('ActivityGroupsBuilder', () => { activities: [], }; - result = builder.buildAvailable(input.appletId, [eventEntity]); + result = builder.buildAvailable('test-applet-id-1', [eventEntity]); expect(result).toEqual(expectedEmptyResult); }); it('Should return group-item for scheduled event when periodicity is Daily and allowAccessBeforeFromTime is true and current time is less than startTime when start/end dates are in the past 3/2 months', () => { const scheduledAt = new Date(2023, 8, 1, 15, 0, 0); + const entityProgressions = getEmptyProgressions(); + const input: GroupsBuildContext = { allAppletActivities: [], - entityProgressions: [], + entityProgressions, appletId: 'test-applet-id-1', }; @@ -1088,7 +1129,7 @@ describe('ActivityGroupsBuilder', () => { eventEntity.event.availability.startDate = subMonths(now, 3); eventEntity.event.availability.endDate = subMonths(now, 2); - let result = builder.buildAvailable(input.appletId, [eventEntity]); + let result = builder.buildAvailable('test-applet-id-1', [eventEntity]); const expectedItem = getExpectedAvailableItem(); expectedItem.availableTo = new Date(startOfDay(scheduledAt)); @@ -1101,21 +1142,21 @@ describe('ActivityGroupsBuilder', () => { activities: [], }; - result = builder.buildAvailable(input.appletId, [eventEntity]); + result = builder.buildAvailable('test-applet-id-1', [eventEntity]); expect(result).toEqual(expectedEmptyResult); }); it('Should return group-item for scheduled event when periodicity is Daily and allowAccessBeforeFromTime is true and current time is less than startTime and progress record exist and completed yesterday', () => { const scheduledAt = new Date(2023, 8, 1, 15, 0, 0); - const progression = getProgression( + const entityProgressions = getProgressions( subDays(scheduledAt, 1), addMinutes(subDays(scheduledAt, 1), 5), ); const input = { allAppletActivities: [], - entityProgressions: [progression], + entityProgressions, appletId: 'test-applet-id-1', }; @@ -1138,7 +1179,7 @@ describe('ActivityGroupsBuilder', () => { eventEntity.event.availability.startDate = subMonths(now, 2); eventEntity.event.availability.endDate = addMonths(now, 2); - let result = builder.buildAvailable(input.appletId, [eventEntity]); + let result = builder.buildAvailable('test-applet-id-1', [eventEntity]); const expectedItem = getExpectedAvailableItem(); expectedItem.availableTo = new Date(startOfDay(scheduledAt)); @@ -1151,7 +1192,7 @@ describe('ActivityGroupsBuilder', () => { activities: [expectedItem], }; - result = builder.buildAvailable(input.appletId, [eventEntity]); + result = builder.buildAvailable('test-applet-id-1', [eventEntity]); expect(result).toEqual(expectedResult); }); @@ -1159,9 +1200,11 @@ describe('ActivityGroupsBuilder', () => { it('Should not return group-item for scheduled event when periodicity is Weekly and allowAccessBeforeFromTime is true and scheduledToday is false', () => { const scheduledAt = new Date(2023, 8, 1, 15, 0, 0); + const entityProgressions = getEmptyProgressions(); + const input: GroupsBuildContext = { allAppletActivities: [], - entityProgressions: [], + entityProgressions, appletId: 'test-applet-id-1', }; @@ -1182,7 +1225,7 @@ describe('ActivityGroupsBuilder', () => { mockGetNow(builder, new Date(now)); - const result = builder.buildAvailable(input.appletId, [eventEntity]); + const result = builder.buildAvailable('test-applet-id-1', [eventEntity]); const expectedResult: ActivityListGroup = { name: 'additional:available', @@ -1196,14 +1239,14 @@ describe('ActivityGroupsBuilder', () => { it('Should not return group-item for scheduled event when periodicity is Weekly and allowAccessBeforeFromTime is true and completed today is true', () => { const scheduledAt = new Date(2023, 8, 1, 15, 0, 0); - const progression = getProgression( + const entityProgressions = getProgressions( new Date(scheduledAt), addMinutes(scheduledAt, 5), ); const input = { allAppletActivities: [], - entityProgressions: [progression], + entityProgressions, appletId: 'test-applet-id-1', }; @@ -1224,7 +1267,7 @@ describe('ActivityGroupsBuilder', () => { eventEntity.event.availability.timeFrom = { hours: 15, minutes: 0 }; eventEntity.event.availability.timeTo = { hours: 16, minutes: 30 }; - const result = builder.buildAvailable(input.appletId, [eventEntity]); + const result = builder.buildAvailable('test-applet-id-1', [eventEntity]); const expectedResult: ActivityListGroup = { name: 'additional:available', @@ -1238,11 +1281,11 @@ describe('ActivityGroupsBuilder', () => { it('Should not return group-item for scheduled event when periodicity is Weekly and allowAccessBeforeFromTime is true and started yesterday, but not completed yet', () => { const scheduledAt = new Date(2023, 8, 1, 15, 0, 0); - const progression = getProgression(subDays(scheduledAt, 1), null); + const entityProgressions = getProgressions(subDays(scheduledAt, 1), null); const input = { allAppletActivities: [], - entityProgressions: [progression], + entityProgressions, appletId: 'test-applet-id-1', }; @@ -1269,7 +1312,7 @@ describe('ActivityGroupsBuilder', () => { activities: [], }; - const result = builder.buildAvailable(input.appletId, [eventEntity]); + const result = builder.buildAvailable('test-applet-id-1', [eventEntity]); expect(result).toEqual(expectedResult); }); @@ -1286,9 +1329,11 @@ describe('ActivityGroupsBuilder', () => { it(`Should return group item when event is scheduled of ${periodicity} periodicity and now is less than scheduledAt and accessBeforeTimeFrom is false and not completed today`, () => { const scheduledAt = new Date(2023, 8, 1, 15, 30, 0); + const entityProgressions = getEmptyProgressions(); + const input: GroupsBuildContext = { allAppletActivities: [], - entityProgressions: [], + entityProgressions, appletId: 'test-applet-id-1', }; @@ -1309,7 +1354,9 @@ describe('ActivityGroupsBuilder', () => { mockGetNow(builder, new Date(now)); - const result = builder.buildScheduled(input.appletId, [eventEntity]); + const result = builder.buildScheduled('test-applet-id-1', [ + eventEntity, + ]); const expectedItem: ActivityListItem = getExpectedScheduledItem(); expectedItem.availableFrom = startOfDay(scheduledAt); @@ -1331,9 +1378,11 @@ describe('ActivityGroupsBuilder', () => { it('Should return group item when event is scheduled of Daily periodicity and now is less than scheduledAt and accessBeforeTimeFrom is false and not completed today and start/end dates in the future in 2/3 months', () => { const scheduledAt = new Date(2023, 8, 1, 15, 30, 0); + const entityProgressions = getEmptyProgressions(); + const input: GroupsBuildContext = { allAppletActivities: [], - entityProgressions: [], + entityProgressions, appletId: 'test-applet-id-1', }; @@ -1356,7 +1405,7 @@ describe('ActivityGroupsBuilder', () => { eventEntity.event.availability.startDate = addMonths(now, 2); eventEntity.event.availability.endDate = addMonths(now, 3); - let result = builder.buildScheduled(input.appletId, [eventEntity]); + let result = builder.buildScheduled('test-applet-id-1', [eventEntity]); const expectedItem: ActivityListItem = getExpectedScheduledItem(); expectedItem.availableFrom = startOfDay(scheduledAt); @@ -1371,7 +1420,7 @@ describe('ActivityGroupsBuilder', () => { activities: [], }; - result = builder.buildScheduled(input.appletId, [eventEntity]); + result = builder.buildScheduled('test-applet-id-1', [eventEntity]); expect(result).toEqual(expectedEmptyResult); }); @@ -1379,9 +1428,11 @@ describe('ActivityGroupsBuilder', () => { it('Should return group item when event is scheduled of Daily periodicity and now is less than scheduledAt and accessBeforeTimeFrom is false and not completed today and start/end dates are in the past: 3/2 months ago', () => { const scheduledAt = new Date(2023, 8, 1, 15, 30, 0); + const entityProgressions = getEmptyProgressions(); + const input: GroupsBuildContext = { allAppletActivities: [], - entityProgressions: [], + entityProgressions, appletId: 'test-applet-id-1', }; @@ -1404,7 +1455,7 @@ describe('ActivityGroupsBuilder', () => { eventEntity.event.availability.startDate = subMonths(now, 3); eventEntity.event.availability.endDate = subMonths(now, 2); - let result = builder.buildScheduled(input.appletId, [eventEntity]); + let result = builder.buildScheduled('test-applet-id-1', [eventEntity]); const expectedItem: ActivityListItem = getExpectedScheduledItem(); expectedItem.availableFrom = startOfDay(scheduledAt); @@ -1419,7 +1470,7 @@ describe('ActivityGroupsBuilder', () => { activities: [], }; - result = builder.buildScheduled(input.appletId, [eventEntity]); + result = builder.buildScheduled('test-applet-id-1', [eventEntity]); expect(result).toEqual(expectedEmptyResult); }); @@ -1427,9 +1478,11 @@ describe('ActivityGroupsBuilder', () => { it('Should return group item when event is scheduled of Daily type and now is less than scheduledAt and accessBeforeTimeFrom is false and not completed today and progress record exist and completed yesterday', () => { const scheduledAt = new Date(2023, 8, 1, 15, 30, 0); + let entityProgressions = getEmptyProgressions(); + let input: GroupsBuildContext = { allAppletActivities: [], - entityProgressions: [], + entityProgressions, appletId: 'test-applet-id-1', }; @@ -1450,7 +1503,7 @@ describe('ActivityGroupsBuilder', () => { eventEntity.event.availability.timeFrom = { hours: 15, minutes: 0 }; eventEntity.event.availability.timeTo = { hours: 16, minutes: 30 }; - let result = builder.buildScheduled(input.appletId, [eventEntity]); + let result = builder.buildScheduled('test-applet-id-1', [eventEntity]); const expectedItem: ActivityListItem = getExpectedScheduledItem(); expectedItem.availableFrom = startOfDay(scheduledAt); @@ -1465,9 +1518,14 @@ describe('ActivityGroupsBuilder', () => { activities: [expectedItem], }; + entityProgressions = getProgressions( + subDays(scheduledAt, 1), + addMinutes(subDays(scheduledAt, 1), 5), + ); + input = { allAppletActivities: [], - entityProgressions: [], + entityProgressions, appletId: 'test-applet-id-1', }; @@ -1478,16 +1536,18 @@ describe('ActivityGroupsBuilder', () => { eventEntity.event.availability.startDate = subMonths(now, 2); eventEntity.event.availability.endDate = addMonths(now, 2); - result = builder.buildScheduled(input.appletId, [eventEntity]); + result = builder.buildScheduled('test-applet-id-1', [eventEntity]); expect(result).toEqual(expectedResult); }); it('Should not return group item when event is scheduled and now is less than scheduledAt and no progress record and accessBeforeTimeFrom is true', () => { const scheduledAt = new Date(2023, 8, 1, 15, 30, 0); + const entityProgressions = getEmptyProgressions(); + const input: GroupsBuildContext = { allAppletActivities: [], - entityProgressions: [], + entityProgressions, appletId: 'test-applet-id-1', }; @@ -1508,7 +1568,7 @@ describe('ActivityGroupsBuilder', () => { eventEntity.event.availability.timeFrom = { hours: 15, minutes: 0 }; eventEntity.event.availability.timeTo = { hours: 16, minutes: 30 }; - const result = builder.buildScheduled(input.appletId, [eventEntity]); + const result = builder.buildScheduled('test-applet-id-1', [eventEntity]); const expectedResult: ActivityListGroup = { name: 'additional:scheduled', @@ -1522,14 +1582,14 @@ describe('ActivityGroupsBuilder', () => { it('2-Should not return group item when event is scheduled and now is less than scheduledAt and completed today and accessBeforeTimeFrom is true', () => { const scheduledAt = new Date(2023, 8, 1, 15, 30, 0); - const progression = getProgression( + const entityProgressions = getProgressions( subHours(scheduledAt, 1), subMinutes(scheduledAt, 30), ); const input = { allAppletActivities: [], - entityProgressions: [progression], + entityProgressions, appletId: 'test-applet-id-1', }; @@ -1556,7 +1616,7 @@ describe('ActivityGroupsBuilder', () => { activities: [], }; - const result = builder.buildScheduled(input.appletId, [eventEntity]); + const result = builder.buildScheduled('test-applet-id-1', [eventEntity]); expect(result).toEqual(expectedResult); }); @@ -1566,6 +1626,27 @@ describe('ActivityGroupsBuilder', () => { it("Should return group item with populated activity flow fields when when flow's progress record is set to the 1st and then to the 2nd activity", () => { const scheduledAt = new Date(2023, 8, 1, 15, 30, 0); + const entityProgression: EntityProgressionInProgressActivityFlow = { + status: 'in-progress', + appletId: 'test-applet-id-1', + entityType: 'activityFlow', + entityId: 'test-flow-id-1', + eventId: 'test-event-id-1', + targetSubjectId: null, + startedAtTimestamp: addMinutes(scheduledAt, 5).getTime(), + availableUntilTimestamp: null, + currentActivityId: 'test-id-1', + currentActivityStartAt: addMinutes(scheduledAt, 5).getTime(), + executionGroupKey: 'group-key-1', + pipelineActivityOrder: 0, + totalActivitiesInPipeline: 2, + currentActivityName: 'test-activity-name-1', + currentActivityDescription: 'test-description-1', + currentActivityImage: null, + }; + + const entityProgressions = [entityProgression]; + const input: GroupsBuildContext = { allAppletActivities: [ { @@ -1587,7 +1668,7 @@ describe('ActivityGroupsBuilder', () => { order: 1, }, ], - entityProgressions: [], + entityProgressions, appletId: 'test-applet-id-1', }; @@ -1623,12 +1704,12 @@ describe('ActivityGroupsBuilder', () => { assignment: null, }; - let result = builder.buildInProgress(input.appletId, [eventEntity]); + let result = builder.buildInProgress('test-applet-id-1', [eventEntity]); let expectedResult: ActivityListGroup = { activities: [ { - appletId: input.appletId, + appletId: 'test-applet-id-1', activityId: 'test-id-1', flowId: 'test-flow-id-1', eventId: 'test-event-id-1', @@ -1658,14 +1739,18 @@ describe('ActivityGroupsBuilder', () => { //switch to 2nd activity + entityProgression.currentActivityId = 'test-id-2'; + entityProgression.currentActivityName = 'test-activity-name-2'; + entityProgression.currentActivityDescription = 'test-description-2'; + entityProgression.pipelineActivityOrder = 1; activityFlow.hideBadge = true; - result = builder.buildInProgress(input.appletId, [eventEntity]); + result = builder.buildInProgress('test-applet-id-1', [eventEntity]); expectedResult = { activities: [ { - appletId: input.appletId, + appletId: 'test-applet-id-1', activityId: 'test-id-2', flowId: 'test-flow-id-1', eventId: 'test-event-id-1', diff --git a/src/widgets/activity-group/model/factories/ActivityGroupsBuilder.ts b/src/widgets/activity-group/model/factories/ActivityGroupsBuilder.ts index 047a1955c..4cb6ec378 100644 --- a/src/widgets/activity-group/model/factories/ActivityGroupsBuilder.ts +++ b/src/widgets/activity-group/model/factories/ActivityGroupsBuilder.ts @@ -44,7 +44,10 @@ export class ActivityGroupsBuilder implements IActivityGroupsBuilder { inputParams.appletId, inputParams.entityProgressions, ); - this.availableEvaluator = new AvailableGroupEvaluator(inputParams.appletId); + this.availableEvaluator = new AvailableGroupEvaluator( + inputParams.appletId, + inputParams.entityProgressions, + ); this.utility = new GroupUtility( inputParams.appletId, inputParams.entityProgressions, diff --git a/src/widgets/activity-group/model/factories/AvailableGroupEvaluator.test.ts b/src/widgets/activity-group/model/factories/AvailableGroupEvaluator.test.ts index 59f121b89..22c032ad5 100644 --- a/src/widgets/activity-group/model/factories/AvailableGroupEvaluator.test.ts +++ b/src/widgets/activity-group/model/factories/AvailableGroupEvaluator.test.ts @@ -124,7 +124,10 @@ describe('AvailableGroupEvaluator cross-day tests when access before start time endDate: addDays(startAt, 2), }); - const evaluator = new AvailableGroupEvaluator(input.appletId); + const evaluator = new AvailableGroupEvaluator( + input.appletId, + input.entityProgressions, + ); let now = buildDateTime(startAt, TimeFrom); now = subMinutes(now, 1); @@ -176,7 +179,10 @@ describe('AvailableGroupEvaluator cross-day tests when access before start time endDate: addDays(startAt, 2), }); - const evaluator = new AvailableGroupEvaluator(input.appletId); + const evaluator = new AvailableGroupEvaluator( + input.appletId, + input.entityProgressions, + ); const now = buildDateTime(startAt, TimeFrom); mockGetNow(evaluator, now); @@ -227,7 +233,10 @@ describe('AvailableGroupEvaluator cross-day tests when access before start time endDate: addDays(startAt, 2), }); - const evaluator = new AvailableGroupEvaluator(input.appletId); + const evaluator = new AvailableGroupEvaluator( + input.appletId, + input.entityProgressions, + ); let now = buildDateTime(startAt, TimeFrom); now = addMinutes(now, 10); @@ -283,7 +292,10 @@ describe('AvailableGroupEvaluator cross-day tests when access before start time endDate: addDays(startAt, 2), }); - const evaluator = new AvailableGroupEvaluator(input.appletId); + const evaluator = new AvailableGroupEvaluator( + input.appletId, + input.entityProgressions, + ); mockGetNow(evaluator, now); @@ -337,7 +349,10 @@ describe('AvailableGroupEvaluator cross-day tests when access before start time endDate: addDays(startAt, 2), }); - const evaluator = new AvailableGroupEvaluator(input.appletId); + const evaluator = new AvailableGroupEvaluator( + input.appletId, + input.entityProgressions, + ); mockGetNow(evaluator, now); @@ -387,7 +402,10 @@ describe('AvailableGroupEvaluator cross-day tests when access before start time endDate: addDays(startAt, 2), }); - const evaluator = new AvailableGroupEvaluator(input.appletId); + const evaluator = new AvailableGroupEvaluator( + input.appletId, + input.entityProgressions, + ); let now = buildDateTime(addDays(startAt, 1), TimeTo); now = subMinutes(now, 1); @@ -437,7 +455,10 @@ describe('AvailableGroupEvaluator cross-day tests when access before start time endDate: addDays(startAt, 2), }); - const evaluator = new AvailableGroupEvaluator(input.appletId); + const evaluator = new AvailableGroupEvaluator( + input.appletId, + input.entityProgressions, + ); mockGetNow(evaluator, now); @@ -485,7 +506,10 @@ describe('AvailableGroupEvaluator cross-day tests when access before start time endDate: addDays(startAt, 2), }); - const evaluator = new AvailableGroupEvaluator(input.appletId); + const evaluator = new AvailableGroupEvaluator( + input.appletId, + input.entityProgressions, + ); mockGetNow(evaluator, now); @@ -529,7 +553,10 @@ describe('AvailableGroupEvaluator cross-day tests when access before start time endDate: addDays(startAt, 2), }); - const evaluator = new AvailableGroupEvaluator(input.appletId); + const evaluator = new AvailableGroupEvaluator( + input.appletId, + input.entityProgressions, + ); const now = buildDateTime(addDays(startAt, 1), TimeTo); mockGetNow(evaluator, now); @@ -580,7 +607,10 @@ describe('AvailableGroupEvaluator cross-day tests when access before start time endDate: addDays(startAt, 2), }); - const evaluator = new AvailableGroupEvaluator(input.appletId); + const evaluator = new AvailableGroupEvaluator( + input.appletId, + input.entityProgressions, + ); let now = buildDateTime(startAt, TimeTo); now = addMinutes(now, 10); @@ -637,7 +667,10 @@ describe('AvailableGroupEvaluator cross-day tests when access before start time endDate: addDays(startAt, 2), }); - const evaluator = new AvailableGroupEvaluator(input.appletId); + const evaluator = new AvailableGroupEvaluator( + input.appletId, + input.entityProgressions, + ); it('Test Weekdays when now is later', () => { let now = buildDateTime(startAt, TimeTo); @@ -675,7 +708,10 @@ describe('AvailableGroupEvaluator cross-day tests when access before start time endDate: addDays(startAt, 2), }); - const evaluator = new AvailableGroupEvaluator(input.appletId); + const evaluator = new AvailableGroupEvaluator( + input.appletId, + input.entityProgressions, + ); let now = buildDateTime(startAt, TimeTo); now = subMinutes(now, 1); @@ -731,7 +767,10 @@ describe('AvailableGroupEvaluator cross-day tests when access before start time endDate: addDays(startAt, 2), }); - const evaluator = new AvailableGroupEvaluator(input.appletId); + const evaluator = new AvailableGroupEvaluator( + input.appletId, + input.entityProgressions, + ); let now = buildDateTime(startAt, TimeTo); now = addMinutes(now, 10); @@ -787,7 +826,10 @@ describe('AvailableGroupEvaluator cross-day tests when access before start time endDate: addDays(startAt, 2), }); - const evaluator = new AvailableGroupEvaluator(input.appletId); + const evaluator = new AvailableGroupEvaluator( + input.appletId, + input.entityProgressions, + ); let now = buildDateTime(addDays(startAt, 1), TimeTo); now = addMinutes(now, 10); @@ -842,7 +884,10 @@ describe('AvailableGroupEvaluator cross-day tests when access before start time }); eventEntity.event.availability.allowAccessBeforeFromTime = true; - const evaluator = new AvailableGroupEvaluator(input.appletId); + const evaluator = new AvailableGroupEvaluator( + input.appletId, + input.entityProgressions, + ); const now = buildDateTime(startAt, { hours: 0, minutes: 0 }); mockGetNow(evaluator, now); @@ -894,7 +939,10 @@ describe('AvailableGroupEvaluator cross-day tests when access before start time }); eventEntity.event.availability.allowAccessBeforeFromTime = true; - const evaluator = new AvailableGroupEvaluator(input.appletId); + const evaluator = new AvailableGroupEvaluator( + input.appletId, + input.entityProgressions, + ); let now = buildDateTime(startAt, TimeFrom); now = subMinutes(now, 1); @@ -947,7 +995,10 @@ describe('AvailableGroupEvaluator cross-day tests when access before start time }); eventEntity.event.availability.allowAccessBeforeFromTime = true; - const evaluator = new AvailableGroupEvaluator(input.appletId); + const evaluator = new AvailableGroupEvaluator( + input.appletId, + input.entityProgressions, + ); const now = buildDateTime(startAt, TimeFrom); mockGetNow(evaluator, now); @@ -999,7 +1050,10 @@ describe('AvailableGroupEvaluator cross-day tests when access before start time }); eventEntity.event.availability.allowAccessBeforeFromTime = true; - const evaluator = new AvailableGroupEvaluator(input.appletId); + const evaluator = new AvailableGroupEvaluator( + input.appletId, + input.entityProgressions, + ); let now = buildDateTime(startAt, TimeFrom); now = addMinutes(now, 10); @@ -1056,7 +1110,10 @@ describe('AvailableGroupEvaluator cross-day tests when access before start time }); eventEntity.event.availability.allowAccessBeforeFromTime = true; - const evaluator = new AvailableGroupEvaluator(input.appletId); + const evaluator = new AvailableGroupEvaluator( + input.appletId, + input.entityProgressions, + ); mockGetNow(evaluator, now); @@ -1111,7 +1168,10 @@ describe('AvailableGroupEvaluator cross-day tests when access before start time }); eventEntity.event.availability.allowAccessBeforeFromTime = true; - const evaluator = new AvailableGroupEvaluator(input.appletId); + const evaluator = new AvailableGroupEvaluator( + input.appletId, + input.entityProgressions, + ); mockGetNow(evaluator, now); @@ -1162,7 +1222,10 @@ describe('AvailableGroupEvaluator cross-day tests when access before start time }); eventEntity.event.availability.allowAccessBeforeFromTime = true; - const evaluator = new AvailableGroupEvaluator(input.appletId); + const evaluator = new AvailableGroupEvaluator( + input.appletId, + input.entityProgressions, + ); let now = buildDateTime(addDays(startAt, 1), TimeTo); now = subMinutes(now, 1); @@ -1213,7 +1276,10 @@ describe('AvailableGroupEvaluator cross-day tests when access before start time }); eventEntity.event.availability.allowAccessBeforeFromTime = true; - const evaluator = new AvailableGroupEvaluator(input.appletId); + const evaluator = new AvailableGroupEvaluator( + input.appletId, + input.entityProgressions, + ); mockGetNow(evaluator, now); @@ -1261,7 +1327,10 @@ describe('AvailableGroupEvaluator cross-day tests when access before start time }); eventEntity.event.availability.allowAccessBeforeFromTime = true; - const evaluator = new AvailableGroupEvaluator(input.appletId); + const evaluator = new AvailableGroupEvaluator( + input.appletId, + input.entityProgressions, + ); mockGetNow(evaluator, now); @@ -1310,7 +1379,10 @@ describe('AvailableGroupEvaluator cross-day tests when access before start time }); eventEntity.event.availability.allowAccessBeforeFromTime = true; - const evaluator = new AvailableGroupEvaluator(input.appletId); + const evaluator = new AvailableGroupEvaluator( + input.appletId, + input.entityProgressions, + ); mockGetNow(evaluator, now); @@ -1355,7 +1427,10 @@ describe('AvailableGroupEvaluator cross-day tests when access before start time }); eventEntity.event.availability.allowAccessBeforeFromTime = true; - const evaluator = new AvailableGroupEvaluator(input.appletId); + const evaluator = new AvailableGroupEvaluator( + input.appletId, + input.entityProgressions, + ); const now = buildDateTime(addDays(startAt, 1), TimeTo); mockGetNow(evaluator, now); @@ -1401,7 +1476,10 @@ describe('AvailableGroupEvaluator cross-day tests when access before start time }); eventEntity.event.availability.allowAccessBeforeFromTime = true; - const evaluator = new AvailableGroupEvaluator(input.appletId); + const evaluator = new AvailableGroupEvaluator( + input.appletId, + input.entityProgressions, + ); let now = buildDateTime(startAt, TimeTo); now = addMinutes(now, 10); @@ -1458,7 +1536,10 @@ describe('AvailableGroupEvaluator cross-day tests when access before start time }); eventEntity.event.availability.allowAccessBeforeFromTime = true; - const evaluator = new AvailableGroupEvaluator(input.appletId); + const evaluator = new AvailableGroupEvaluator( + input.appletId, + input.entityProgressions, + ); let now = buildDateTime(startAt, TimeTo); now = subMinutes(now, 1); @@ -1515,7 +1596,10 @@ describe('AvailableGroupEvaluator cross-day tests when access before start time }); eventEntity.event.availability.allowAccessBeforeFromTime = true; - const evaluator = new AvailableGroupEvaluator(input.appletId); + const evaluator = new AvailableGroupEvaluator( + input.appletId, + input.entityProgressions, + ); let now = buildDateTime(startAt, TimeTo); now = addMinutes(now, 10); @@ -1572,7 +1656,10 @@ describe('AvailableGroupEvaluator cross-day tests when access before start time }); eventEntity.event.availability.allowAccessBeforeFromTime = true; - const evaluator = new AvailableGroupEvaluator(input.appletId); + const evaluator = new AvailableGroupEvaluator( + input.appletId, + input.entityProgressions, + ); let now = buildDateTime(addDays(startAt, 1), TimeTo); now = addMinutes(now, 10); diff --git a/src/widgets/activity-group/model/factories/AvailableGroupEvaluator.ts b/src/widgets/activity-group/model/factories/AvailableGroupEvaluator.ts index 1a040beec..5f27ebff8 100644 --- a/src/widgets/activity-group/model/factories/AvailableGroupEvaluator.ts +++ b/src/widgets/activity-group/model/factories/AvailableGroupEvaluator.ts @@ -1,5 +1,8 @@ import { IEvaluator } from '@app/abstract/lib/interfaces/evaluator'; -import { EntityProgressionCompleted } from '@app/abstract/lib/types/entityProgress'; +import { + EntityProgression, + EntityProgressionCompleted, +} from '@app/abstract/lib/types/entityProgress'; import { AvailabilityType } from '@app/abstract/lib/types/event'; import { ScheduleEvent } from '@app/entities/event/lib/types/event'; import { @@ -15,8 +18,8 @@ export class AvailableGroupEvaluator { private utility: GroupUtility; - constructor(appletId: string) { - this.utility = new GroupUtility(appletId, []); + constructor(appletId: string, entityProgressions: EntityProgression[]) { + this.utility = new GroupUtility(appletId, entityProgressions); } private isEventValidForAlwaysAvailable( diff --git a/src/widgets/activity-group/model/factories/GroupUtility.ts b/src/widgets/activity-group/model/factories/GroupUtility.ts index ab8e90cbc..dfb059cc8 100644 --- a/src/widgets/activity-group/model/factories/GroupUtility.ts +++ b/src/widgets/activity-group/model/factories/GroupUtility.ts @@ -133,7 +133,11 @@ export class GroupUtility { if (!record) { return false; } - return record.status === 'in-progress'; + return ( + record.status === 'in-progress' && + !!record.startedAtTimestamp && + !(record as unknown as EntityProgressionCompleted).endedAtTimestamp + ); } public isInInterval( diff --git a/src/widgets/activity-group/model/factories/ListItemsFactory.ts b/src/widgets/activity-group/model/factories/ListItemsFactory.ts index 88c96c8d2..254f2f4f2 100644 --- a/src/widgets/activity-group/model/factories/ListItemsFactory.ts +++ b/src/widgets/activity-group/model/factories/ListItemsFactory.ts @@ -40,6 +40,15 @@ export class ListItemsFactory { item: ActivityListItem, activityEvent: EventEntity, ) { + console.log( + '!!! populateActivityFlowFields item', + JSON.stringify(item, null, 2), + ); + console.log( + '!!! populateActivityFlowFields activityEvent', + JSON.stringify(activityEvent, null, 2), + ); + const activityFlow = activityEvent.entity as ActivityFlow; const { event, assignment } = activityEvent; diff --git a/src/widgets/survey/lib/tests/useFlowStorageRecord.test.ts b/src/widgets/survey/lib/tests/useFlowStorageRecord.test.ts index 1483cfb90..52629accb 100644 --- a/src/widgets/survey/lib/tests/useFlowStorageRecord.test.ts +++ b/src/widgets/survey/lib/tests/useFlowStorageRecord.test.ts @@ -1,5 +1,8 @@ /* eslint-disable @typescript-eslint/no-unsafe-return */ import { renderHook, screen } from '@testing-library/react-native'; +import { MMKV } from 'react-native-mmkv'; + +import { getDefaultStorageInstanceManager } from '@app/shared/lib/storages/storageInstanceManagerInstance'; import { FlowState, @@ -7,37 +10,41 @@ import { useFlowStorageRecord, } from '../useFlowStorageRecord'; -const deleteMock = jest.fn(); - -jest.mock('@shared/lib/storages', () => ({ - ...jest.requireActual('@shared/lib/storages'), - createStorage: jest.fn().mockReturnValue({ - getString: (id: string) => JSON.stringify({ flowName: `test-name-${id}` }), - delete: (key: string) => deleteMock(key) as void, - addOnValueChangedListener: jest.fn(), +jest.mock('react-native-mmkv', () => ({ + ...jest.requireActual('react-native-mmkv'), + useMMKVObject: jest.fn().mockImplementation((key: string) => { + return [ + { name: key }, + (item: FlowState) => upsertFlowStorageRecordMock(item), + ]; }), })); +const deleteMock = jest.fn(); + const upsertFlowStorageRecordMock = jest.fn(); +const storageMock = { + getString: (id: string) => JSON.stringify({ flowName: `test-name-${id}` }), + delete: (key: string) => deleteMock(key) as void, + addOnValueChangedListener: jest.fn(), +} as never as MMKV; + const initialProps = { appletId: 'mock-applet-id-1', eventId: 'mock-event-id-1', flowId: 'mock-flow-id-1', - targetSubjectId: 'mock-target-subject-id-1', + targetSubjectId: null as string | null, }; -jest.mock('react-native-mmkv', () => ({ - ...jest.requireActual('react-native-mmkv'), - useMMKVObject: jest.fn().mockImplementation((key: string) => { - return [ - { name: key }, - (item: FlowState) => upsertFlowStorageRecordMock(item), - ]; - }), -})); - describe('Test useFlowStorageRecord', () => { + beforeEach(() => { + const storageManager = getDefaultStorageInstanceManager(); + jest + .spyOn(storageManager, 'getFlowProgressStorage') + .mockReturnValue(storageMock); + }); + afterEach(() => { screen.unmount(); }); @@ -49,7 +56,7 @@ describe('Test useFlowStorageRecord', () => { ); expect(result.current.flowStorageRecord).toEqual({ - name: 'mock-flow-id-1-mock-applet-id-1-mock-event-id-1', + name: 'mock-flow-id-1-mock-applet-id-1-mock-event-id-1-NULL', }); }); @@ -60,13 +67,13 @@ describe('Test useFlowStorageRecord', () => { initialProps: { appletId: 'mock-applet-id-3', eventId: 'mock-event-id-3', - targetSubjectId: 'mock-target-subject-id-3', + targetSubjectId: null, }, }, ); expect(result.current.flowStorageRecord).toEqual({ - name: 'default_one_step_flow-mock-applet-id-3-mock-event-id-3', + name: 'default_one_step_flow-mock-applet-id-3-mock-event-id-3-NULL', }); }); @@ -80,11 +87,11 @@ describe('Test useFlowStorageRecord', () => { appletId: 'mock-applet-id-2', eventId: 'mock-event-id-2', flowId: 'mock-flow-id-2', - targetSubjectId: 'mock-target-subject-id-2', + targetSubjectId: null, }); expect(result.current.flowStorageRecord).toEqual({ - name: 'mock-flow-id-2-mock-applet-id-2-mock-event-id-2', + name: 'mock-flow-id-2-mock-applet-id-2-mock-event-id-2-NULL', }); }); @@ -112,7 +119,7 @@ describe('Test useFlowStorageRecord', () => { result.current.clearFlowStorageRecord(); expect(deleteMock).toHaveBeenCalledWith( - 'mock-flow-id-1-mock-applet-id-1-mock-event-id-1', + 'mock-flow-id-1-mock-applet-id-1-mock-event-id-1-NULL', ); }); @@ -125,7 +132,7 @@ describe('Test useFlowStorageRecord', () => { const record = result.current.getCurrentFlowStorageRecord(); expect(record).toEqual({ - flowName: 'test-name-mock-flow-id-1-mock-applet-id-1-mock-event-id-1', + flowName: 'test-name-mock-flow-id-1-mock-applet-id-1-mock-event-id-1-NULL', }); }); }); diff --git a/src/widgets/survey/model/hooks/useAutoCompletion.ts b/src/widgets/survey/model/hooks/useAutoCompletion.ts index a7d394584..6e21f97b9 100644 --- a/src/widgets/survey/model/hooks/useAutoCompletion.ts +++ b/src/widgets/survey/model/hooks/useAutoCompletion.ts @@ -14,8 +14,11 @@ import { selectAppletsEntityProgressions, selectIncompletedEntities, } from '@app/entities/applet/model/selectors'; +import { getDefaultAlertsExtractor } from '@app/features/pass-survey/model/alertsExtractorInstance'; +import { getDefaultScoresExtractor } from '@app/features/pass-survey/model/scoresExtractorInstance'; import { LogTrigger } from '@app/shared/api/services/INotificationService'; import { useAppDispatch, useAppSelector } from '@app/shared/lib/hooks/redux'; +import { ReduxPersistor } from '@app/shared/lib/redux-state/store'; import { Emitter } from '@app/shared/lib/services/Emitter'; import { getDefaultLogger } from '@app/shared/lib/services/loggerInstance'; import { getMutexDefaultInstanceManager } from '@app/shared/lib/utils/mutexDefaultInstanceManagerInstance'; @@ -54,9 +57,13 @@ export const useAutoCompletion = (): Result => { const createConstructService = useCallback(() => { return new ConstructCompletionsService( null, + getDefaultLogger(), queryClient, getDefaultQueueProcessingService(), + getDefaultAlertsExtractor(), + getDefaultScoresExtractor(), dispatch, + ReduxPersistor, entityProgressions, ); }, [dispatch, queryClient, entityProgressions]); diff --git a/src/widgets/survey/model/services/ConstructCompletionsService.ts b/src/widgets/survey/model/services/ConstructCompletionsService.ts index 36e6dc9df..0c20c9c61 100644 --- a/src/widgets/survey/model/services/ConstructCompletionsService.ts +++ b/src/widgets/survey/model/services/ConstructCompletionsService.ts @@ -1,5 +1,6 @@ import { QueryClient } from '@tanstack/react-query'; import { addMilliseconds, subSeconds } from 'date-fns'; +import { Persistor } from 'redux-persist'; import { EntityProgression, @@ -14,17 +15,16 @@ import { ScoreRecord, } from '@app/features/pass-survey/lib/types/summary'; import { InitializeHiddenItem } from '@app/features/pass-survey/model/ActivityRecordInitializer'; -import { getDefaultAlertsExtractor } from '@app/features/pass-survey/model/alertsExtractorInstance'; -import { getDefaultScoresExtractor } from '@app/features/pass-survey/model/scoresExtractorInstance'; +import { IAlertsExtractor } from '@app/features/pass-survey/model/IAlertsExtractor'; +import { IScoresExtractor } from '@app/features/pass-survey/model/IScoresExtractor'; import { AppletEncryptionDTO } from '@app/shared/api/services/IAppletService'; import { QueryDataUtils } from '@app/shared/api/services/QueryDataUtils'; +import { getDefaultAnalyticsService } from '@app/shared/lib/analytics/analyticsServiceInstance'; import { - AnalyticsService, MixEvents, MixProperties, -} from '@app/shared/lib/analytics/AnalyticsService'; -import { ReduxPersistor } from '@app/shared/lib/redux-state/store'; -import { getDefaultLogger } from '@app/shared/lib/services/loggerInstance'; +} from '@app/shared/lib/analytics/IAnalyticsService'; +import { ILogger } from '@app/shared/lib/types/logger'; import { wait } from '@app/shared/lib/utils/common'; import { getNow, getTimezoneOffset } from '@app/shared/lib/utils/dateTime'; import { @@ -89,24 +89,36 @@ export type ConstructInput = ( const DistinguishInterimAndFinishLag = 1; // For correct sort on BE, Admin, TODO export class ConstructCompletionsService { + private logger: ILogger; private saveActivitySummary: SaveActivitySummary | null; private queryDataUtils: QueryDataUtils; private entityProgressions: EntityProgression[]; private pushToQueueService: IPushToQueue; + private alertsExtractor: IAlertsExtractor; + private scoresExtractor: IScoresExtractor; private dispatch: AppDispatch; + private persistor: Persistor; constructor( saveActivitySummary: SaveActivitySummary | null, + logger: ILogger, queryClient: QueryClient, pushToQueueService: IPushToQueue, + alertsExtractor: IAlertsExtractor, + scoresExtractor: IScoresExtractor, dispatch: AppDispatch, + persistor: Persistor, entityProgressions: EntityProgression[], ) { this.saveActivitySummary = saveActivitySummary; + this.logger = logger; this.queryDataUtils = new QueryDataUtils(queryClient); this.pushToQueueService = pushToQueueService; + this.alertsExtractor = alertsExtractor; + this.scoresExtractor = scoresExtractor; this.dispatch = dispatch; this.entityProgressions = entityProgressions; + this.persistor = persistor; } private getLogDates( @@ -133,12 +145,12 @@ export class ConstructCompletionsService { ) { const logDates = this.getLogDates(evaluatedEndAt, availableTo); - getDefaultLogger().log( + this.logger.log( `[ConstructCompletionsService.logFinish]: Activity: "${activityName}|${activityId}", applet: "${appletName}|${appletId}", submitId: ${submitId}, ${logDates}`, ); if (flowId) { - getDefaultLogger().log( + this.logger.log( `[ConstructCompletionsService.logFinish]: Flow "${flowId}", applet "${appletName}|${appletId}", submitId: ${submitId}, ${logDates}`, ); } @@ -159,7 +171,7 @@ export class ConstructCompletionsService { ) { const logDates = this.getLogDates(evaluatedEndAt, availableTo); - getDefaultLogger().log( + this.logger.log( `[ConstructCompletionsService.logIntermediate]: Activity: "${activityName}|${activityId}", flow: "${flowName}|${flowId}", applet: "${appletName}|${appletId}", submitId: ${submitId}, ${logDates}`, ); } @@ -213,7 +225,7 @@ export class ConstructCompletionsService { if (!appletEncryption) { const error = '[ConstructCompletionsService] Encryption params is undefined'; - getDefaultLogger().warn(error); + this.logger.warn(error); throw new Error(error); } } @@ -222,7 +234,7 @@ export class ConstructCompletionsService { activityStorageRecord: ActivityState | null | undefined, ): boolean { if (!activityStorageRecord) { - getDefaultLogger().warn( + this.logger.warn( '[ConstructCompletionsService] activityStorageRecord does not exist', ); return false; @@ -238,14 +250,13 @@ export class ConstructCompletionsService { return; } - const summaryAlerts: AnswerAlerts = - getDefaultAlertsExtractor().extractForSummary( - activityStorageRecord.items, - activityStorageRecord.answers, - activityName, - ); + const summaryAlerts: AnswerAlerts = this.alertsExtractor.extractForSummary( + activityStorageRecord.items, + activityStorageRecord.answers, + activityName, + ); - const scores: ScoreRecord[] = getDefaultScoresExtractor().extract( + const scores: ScoreRecord[] = this.scoresExtractor.extract( activityStorageRecord.items, activityStorageRecord.answers, activityStorageRecord.scoreSettings, @@ -266,7 +277,7 @@ export class ConstructCompletionsService { private async constructForIntermediate( input: ConstructForIntermediateInput, ): Promise { - getDefaultLogger().log( + this.logger.log( '[ConstructCompletionsService.constructForIntermediate] input:\n' + JSON.stringify(input, null, 2), ); @@ -380,12 +391,12 @@ export class ConstructCompletionsService { order, ); - AnalyticsService.track(MixEvents.AssessmentCompleted, { + getDefaultAnalyticsService().track(MixEvents.AssessmentCompleted, { [MixProperties.AppletId]: appletId, [MixProperties.SubmitId]: submitId, }); - getDefaultLogger().log( + this.logger.log( `[ConstructCompletionsService.constructForIntermediate] Done`, ); } @@ -393,7 +404,7 @@ export class ConstructCompletionsService { private async constructForFinish( input: ConstructForFinishInput, ): Promise { - getDefaultLogger().log( + this.logger.log( '[ConstructCompletionsService.constructForFinish] input:\n' + JSON.stringify(input, null, 2), ); @@ -451,7 +462,7 @@ export class ConstructCompletionsService { }), ); - await ReduxPersistor.flush(); + await this.persistor.flush(); const { items, answers: recordAnswers, actions } = activityStorageRecord; @@ -531,14 +542,12 @@ export class ConstructCompletionsService { order, ); - AnalyticsService.track(MixEvents.AssessmentCompleted, { + getDefaultAnalyticsService().track(MixEvents.AssessmentCompleted, { [MixProperties.AppletId]: appletId, [MixProperties.SubmitId]: submitId, }); - getDefaultLogger().log( - `[ConstructCompletionsService.constructForFinish] Done`, - ); + this.logger.log(`[ConstructCompletionsService.constructForFinish] Done`); } public async construct(input: ConstructInput): Promise { @@ -554,7 +563,7 @@ export class ConstructCompletionsService { await this.constructForFinish(input); } } catch (error) { - getDefaultLogger().warn( + this.logger.warn( `[ConstructCompletionsService.construct] Error occurred: \n${error}`, ); } diff --git a/src/widgets/survey/model/services/tests/CollectCompletionsService.collectForEntity.test.ts b/src/widgets/survey/model/services/tests/CollectCompletionsService.collectForEntity.test.ts index b8e93dd4e..8add18ff3 100644 --- a/src/widgets/survey/model/services/tests/CollectCompletionsService.collectForEntity.test.ts +++ b/src/widgets/survey/model/services/tests/CollectCompletionsService.collectForEntity.test.ts @@ -139,6 +139,7 @@ describe('Test CollectCompletionsService: collectForEntity', () => { completionType: 'finish', eventId: 'mock-event-id-1', flowId: undefined, + targetSubjectId: null, order: 0, }, ]); @@ -180,6 +181,7 @@ describe('Test CollectCompletionsService: collectForEntity', () => { completionType: 'intermediate', eventId: 'mock-event-id-1', flowId: 'mock-entity-id-1', + targetSubjectId: null, order: 0, }, { @@ -189,6 +191,7 @@ describe('Test CollectCompletionsService: collectForEntity', () => { completionType: 'finish', eventId: 'mock-event-id-1', flowId: 'mock-entity-id-1', + targetSubjectId: null, order: 1, }, ]); @@ -230,6 +233,7 @@ describe('Test CollectCompletionsService: collectForEntity', () => { completionType: 'finish', eventId: 'mock-event-id-1', flowId: 'mock-entity-id-1', + targetSubjectId: null, order: 1, }, ]); diff --git a/src/widgets/survey/model/services/tests/ConstructCompletionsService.test.ts b/src/widgets/survey/model/services/tests/ConstructCompletionsService.test.ts index f9b8a1281..bb52aad33 100644 --- a/src/widgets/survey/model/services/tests/ConstructCompletionsService.test.ts +++ b/src/widgets/survey/model/services/tests/ConstructCompletionsService.test.ts @@ -3,12 +3,17 @@ import { addHours, addMilliseconds, subHours, subSeconds } from 'date-fns'; import { Answers } from '@app/features/pass-survey/lib/hooks/useActivityStorageRecord'; import { PipelineItem } from '@app/features/pass-survey/lib/types/payload'; +import { IAlertsExtractor } from '@app/features/pass-survey/model/IAlertsExtractor'; +import { IScoresExtractor } from '@app/features/pass-survey/model/IScoresExtractor'; import { getSliderItem } from '@app/features/pass-survey/model/tests/testHelpers'; -import { AppletEncryptionDTO } from '@app/shared/api/services/IAppletService'; +import { ReduxPersistor } from '@app/shared/lib/redux-state/store'; +import { ILogger } from '@app/shared/lib/types/logger'; import { createGetActivityRecordMock, expectedUserActions, + getActivityProgressionsMock, + getFlowProgressionsMock, getInputsForFinish, getInputsForIntermediate, mockConstructionServiceExternals, @@ -19,28 +24,32 @@ import { ConstructCompletionsService, } from '../ConstructCompletionsService'; -jest.mock('@app/shared/lib/services/Logger', () => ({ +const mockLogger = { log: jest.fn(), info: jest.fn(), warn: jest.fn(), -})); +} as never as ILogger; -jest.mock('@app/features/pass-survey/model/ScoresExtractor', () => ({ +const mockScoresExtractor = { extract: jest .fn() .mockReturnValue([ { name: 'mock-score-name-1', value: 125, flagged: true }, ]), -})); +} as never as IScoresExtractor; -jest.mock('@app/features/pass-survey/model/AlertsExtractor', () => ({ +const mockAlertsExtractor = { extractForSummary: jest.fn().mockReturnValue([ { activityItemId: 'mock-activity-item-1', message: 'mock-message-1', }, ]), -})); +} as never as IAlertsExtractor; + +const mockPersistor = { + flush: jest.fn().mockResolvedValue(undefined), +} as never as typeof ReduxPersistor; const mockNowDate = new Date(2023, 3, 8, 15, 27); @@ -80,12 +89,18 @@ describe('Test ConstructCompletionsService.constructForIntermediate', () => { const pushToQueueMock = { push: jest.fn() }; + const entityProgressions = getFlowProgressionsMock(); + const service = new ConstructCompletionsService( saveSummaryMock, + mockLogger, {} as any, pushToQueueMock, + mockAlertsExtractor, + mockScoresExtractor, jest.fn(), - [], + mockPersistor, + entityProgressions, ); //@ts-expect-error @@ -137,6 +152,7 @@ describe('Test ConstructCompletionsService.constructForIntermediate', () => { eventId: 'mock-event-id-1', executionGroupKey: 'mock-flow-group-key-1', flowId: 'mock-flow-id-1', + targetSubjectId: null, isFlowCompleted: false, itemIds: ['mock-slider-id-1', 'mock-slider-id-2'], activityName: 'mock-activity-name-1', @@ -167,12 +183,18 @@ describe('Test ConstructCompletionsService.constructForIntermediate', () => { const pushToQueueMock = { push: jest.fn() }; + const entityProgressions = getFlowProgressionsMock(); + const service = new ConstructCompletionsService( saveSummaryMock, + mockLogger, {} as any, pushToQueueMock, + mockAlertsExtractor, + mockScoresExtractor, jest.fn(), - [], + mockPersistor, + entityProgressions, ); //@ts-expect-error @@ -237,12 +259,18 @@ describe('Test ConstructCompletionsService.constructForFinish', () => { const pushToQueueMock = { push: jest.fn() }; + const entityProgressions = getFlowProgressionsMock(); + const service = new ConstructCompletionsService( saveSummaryMock, + mockLogger, {} as any, pushToQueueMock, + mockAlertsExtractor, + mockScoresExtractor, jest.fn(), - [], + mockPersistor, + entityProgressions, ); //@ts-expect-error @@ -296,6 +324,7 @@ describe('Test ConstructCompletionsService.constructForFinish', () => { eventId: 'mock-event-id-1', executionGroupKey: 'mock-flow-group-key-1', flowId: 'mock-flow-id-1', + targetSubjectId: null, isFlowCompleted: true, itemIds: ['mock-slider-id-1', 'mock-slider-id-2'], activityName: 'mock-activity-name-1', @@ -346,12 +375,18 @@ describe('Test ConstructCompletionsService.constructForFinish', () => { const pushToQueueMock = { push: jest.fn() }; + const entityProgressions = getActivityProgressionsMock(); + const service = new ConstructCompletionsService( saveSummaryMock, + mockLogger, {} as any, pushToQueueMock, + mockAlertsExtractor, + mockScoresExtractor, jest.fn(), - [], + mockPersistor, + entityProgressions, ); //@ts-expect-error @@ -407,6 +442,7 @@ describe('Test ConstructCompletionsService.constructForFinish', () => { eventId: 'mock-event-id-1', executionGroupKey: 'mock-group-key-1', flowId: null, + targetSubjectId: null, isFlowCompleted: false, itemIds: ['mock-slider-id-1', 'mock-slider-id-2'], activityName: 'mock-activity-name-1', @@ -429,16 +465,22 @@ describe('Test ConstructCompletionsService: edge cases', () => { }); it('"getAppletProperties" should throw error when no applet dto in the cache', () => { + const entityProgressions = getFlowProgressionsMock(); + const pushToQueueMock = { push: jest.fn() }; const { saveSummaryMock } = mockConstructionServiceExternals(mockNowDate); const service = new ConstructCompletionsService( saveSummaryMock, + mockLogger, {} as any, pushToQueueMock, + mockAlertsExtractor, + mockScoresExtractor, jest.fn(), - [], + mockPersistor, + entityProgressions, ); //@ts-expect-error @@ -471,6 +513,8 @@ describe('Test ConstructCompletionsService: edge cases', () => { : String(appletEncryption); it(`"validateEncryption" should throw error when appletEncryption is ${appletEncryptionAsText}`, () => { + const entityProgressions = getFlowProgressionsMock(); + const { saveSummaryMock } = mockConstructionServiceExternals(mockNowDate); const pushMock = jest.fn(); @@ -479,10 +523,14 @@ describe('Test ConstructCompletionsService: edge cases', () => { const service = new ConstructCompletionsService( saveSummaryMock, + mockLogger, {} as QueryClient, pushToQueueMock, + mockAlertsExtractor, + mockScoresExtractor, jest.fn(), - [], + mockPersistor, + entityProgressions, ); expect(() => @@ -513,6 +561,8 @@ describe('Test ConstructCompletionsService: edge cases', () => { : String(activityStorageRecord); it(`isRecordExist should return ${expectedResult} when activityStorageRecord is ${activityStorageRecordAsText}`, () => { + const entityProgressions = getFlowProgressionsMock(); + const { saveSummaryMock } = mockConstructionServiceExternals(mockNowDate); const pushMock = jest.fn(); @@ -521,10 +571,14 @@ describe('Test ConstructCompletionsService: edge cases', () => { const service = new ConstructCompletionsService( saveSummaryMock, + mockLogger, {} as QueryClient, pushToQueueMock, + mockAlertsExtractor, + mockScoresExtractor, jest.fn(), - [], + mockPersistor, + entityProgressions, ); //@ts-expect-error @@ -622,6 +676,8 @@ describe('Test ConstructCompletionsService: evaluateEndAt', () => { expectedResultLog, }) => { it(`Should return '${expectedResultLog}' when completionType is ${completionType} and availableTo is '${logAvailableTo}' and isAutocompletion is ${isAutocompletion}`, () => { + const entityProgressions = getFlowProgressionsMock(); + const pushToQueueMock = { push: jest.fn() }; const { saveSummaryMock } = @@ -629,10 +685,14 @@ describe('Test ConstructCompletionsService: evaluateEndAt', () => { const service = new ConstructCompletionsService( saveSummaryMock, + mockLogger, {} as QueryClient, pushToQueueMock, + mockAlertsExtractor, + mockScoresExtractor, jest.fn(), - [], + mockPersistor, + entityProgressions, ); // @ts-expect-error diff --git a/src/widgets/survey/model/services/tests/testHelpers.ts b/src/widgets/survey/model/services/tests/testHelpers.ts index 5a97395cc..9bc77c90d 100644 --- a/src/widgets/survey/model/services/tests/testHelpers.ts +++ b/src/widgets/survey/model/services/tests/testHelpers.ts @@ -1,5 +1,9 @@ import { EntityPath, EntityType } from '@app/abstract/lib/types/entity'; -import { EntityProgressionInProgressActivityFlow } from '@app/abstract/lib/types/entityProgress'; +import { + EntityProgression, + EntityProgressionCompleted, + EntityProgressionInProgressActivityFlow, +} from '@app/abstract/lib/types/entityProgress'; import { IncompleteEntity } from '@app/entities/applet/model/selectors'; import { ActivityState, @@ -230,6 +234,48 @@ export const getUserActionsMock = (answersMock: Answers): UserAction[] => { return userActionsMock; }; +export const getFlowProgressionsMock = (): EntityProgression[] => { + const progression: EntityProgression = { + status: 'in-progress', + appletId: 'mock-applet-id-1', + entityType: 'activityFlow', + entityId: 'mock-flow-id-1', + eventId: 'mock-event-id-1', + targetSubjectId: null, + startedAtTimestamp: 12367800000, + availableUntilTimestamp: null, + currentActivityDescription: 'mock-activity-description-1', + currentActivityId: 'mock-activity-id-1', + currentActivityImage: null, + currentActivityName: 'mock-activity-name-1', + currentActivityStartAt: 12389100000, + executionGroupKey: 'mock-flow-group-key-1', + pipelineActivityOrder: 0, + totalActivitiesInPipeline: 2, + }; + + (progression as never as EntityProgressionCompleted).endedAtTimestamp = null; + + return [progression]; +}; + +export const getActivityProgressionsMock = (): EntityProgression[] => { + const progression: EntityProgression = { + status: 'in-progress', + appletId: 'mock-applet-id-1', + entityType: 'activity', + entityId: 'mock-activity-id-1', + eventId: 'mock-event-id-1', + targetSubjectId: null, + startedAtTimestamp: 12367800000, + availableUntilTimestamp: null, + }; + + (progression as never as EntityProgressionCompleted).endedAtTimestamp = null; + + return [progression]; +}; + export const getActivityRecordMockResult = ( answersMock: Answers, userActionsMock: UserAction[], @@ -354,7 +400,7 @@ export const getInputsForIntermediate = (): ConstructInput => { appletId: 'mock-applet-id-1', eventId: 'mock-event-id-1', flowId: 'mock-flow-id-1', - targetSubjectId: 'mock-target-subject-id-1', + targetSubjectId: null, order: 0, isAutocompletion: false, }; @@ -368,7 +414,7 @@ export const getInputsForFinish = (entityType: EntityType): ConstructInput => { appletId: 'mock-applet-id-1', eventId: 'mock-event-id-1', flowId: entityType === 'flow' ? 'mock-flow-id-1' : undefined, - targetSubjectId: 'mock-target-subject-id-1', + targetSubjectId: null, order: 0, isAutocompletion: false, }; diff --git a/src/widgets/survey/model/tests/mappers.test.ts b/src/widgets/survey/model/tests/mappers.test.ts index a7dc9c27d..302804f9a 100644 --- a/src/widgets/survey/model/tests/mappers.test.ts +++ b/src/widgets/survey/model/tests/mappers.test.ts @@ -1,4 +1,6 @@ +import { PipelineItem } from '@app/features/pass-survey/lib/types/payload'; import { getDefaultLogger } from '@app/shared/lib/services/loggerInstance'; +import { ILogger } from '@app/shared/lib/types/logger'; import { textInput, @@ -76,21 +78,15 @@ import { mapUserActionsToDto, } from '../mappers'; -jest.mock('@app/shared/lib', () => { - const mockedLib = jest.requireActual('@app/shared/lib'); - - return { - ...mockedLib, - Logger: { - log: jest.fn(), - warn: jest.fn(), - error: jest.fn(), - info: jest.fn(), - }, - }; -}); - describe('Survey widget mapAnswersToDto tests', () => { + let logger: ILogger; + + beforeEach(() => { + logger = getDefaultLogger(); + + jest.spyOn(logger, 'warn'); + }); + it('Should return mapped result for different items pipeline', async () => { const pipeline = [ textInput, @@ -516,14 +512,13 @@ describe('Survey widget mapAnswersToDto tests', () => { }); it('Should throw error for mapAnswersToAlerts with invalid arguments', async () => { - const pipeline = null; + const pipeline = null as never as PipelineItem[]; const answers = { 0: stackedSliderAnswer, }; - //@ts-ignore expect(() => mapAnswersToAlerts(pipeline, answers)).toThrow(); - expect(getDefaultLogger().warn).toBeCalled(); + expect(logger.warn).toBeCalled(); }); it('Should return mapped result for userActions', async () => { diff --git a/src/widgets/survey/model/tests/pipelineBuilder.multiple.test.ts b/src/widgets/survey/model/tests/pipelineBuilder.multiple.test.ts index d17bc874d..9b9bc3cd6 100644 --- a/src/widgets/survey/model/tests/pipelineBuilder.multiple.test.ts +++ b/src/widgets/survey/model/tests/pipelineBuilder.multiple.test.ts @@ -19,7 +19,7 @@ describe('Test pipelineBuilder for multiple activities', () => { const result = buildActivityFlowPipeline({ appletId: 'mock-applet-id-1', eventId: 'mock-event-id-1', - targetSubjectId: 'mock-target-subject-id-1', + targetSubjectId: null, hasSummary: () => false, startFrom: 0, flowId: 'mock-flow-id-1', @@ -84,7 +84,7 @@ describe('Test pipelineBuilder for multiple activities', () => { const result = buildActivityFlowPipeline({ appletId: 'mock-applet-id-1', eventId: 'mock-event-id-1', - targetSubjectId: 'mock-target-subject-id-1', + targetSubjectId: null, hasSummary: () => true, startFrom: 0, flowId: 'mock-flow-id-1', @@ -160,7 +160,7 @@ describe('Test pipelineBuilder for multiple activities', () => { const result = buildActivityFlowPipeline({ appletId: 'mock-applet-id-1', eventId: 'mock-event-id-1', - targetSubjectId: 'mock-target-subject-id-1', + targetSubjectId: null, hasSummary: () => false, startFrom: 3, flowId: 'mock-flow-id-1', @@ -188,7 +188,7 @@ describe('Test pipelineBuilder for multiple activities', () => { const result = buildActivityFlowPipeline({ appletId: 'mock-applet-id-1', eventId: 'mock-event-id-1', - targetSubjectId: 'mock-target-subject-id-1', + targetSubjectId: null, hasSummary: () => true, startFrom: 3, flowId: 'mock-flow-id-1', @@ -227,7 +227,7 @@ describe('Test pipelineBuilder for multiple activities', () => { const result = buildActivityFlowPipeline({ appletId: 'mock-applet-id-1', eventId: 'mock-event-id-1', - targetSubjectId: 'mock-target-subject-id-1', + targetSubjectId: null, hasSummary: () => false, startFrom: 0, flowId: 'mock-flow-id-1', diff --git a/src/widgets/survey/model/tests/pipelineBuilder.single.test.ts b/src/widgets/survey/model/tests/pipelineBuilder.single.test.ts index 7f09582e3..07feefd12 100644 --- a/src/widgets/survey/model/tests/pipelineBuilder.single.test.ts +++ b/src/widgets/survey/model/tests/pipelineBuilder.single.test.ts @@ -19,7 +19,7 @@ describe('Test pipelineBuilder for single activity', () => { const result = buildSingleActivityPipeline({ appletId: 'mock-applet-id-1', eventId: 'mock-event-id-1', - targetSubjectId: 'mock-target-subject-id-1', + targetSubjectId: null, hasSummary: () => false, activity: { ...mockForActivity1 }, }); @@ -56,7 +56,7 @@ describe('Test pipelineBuilder for single activity', () => { const result = buildSingleActivityPipeline({ appletId: 'mock-applet-id-2', eventId: 'mock-event-id-2', - targetSubjectId: 'mock-target-subject-id-1', + targetSubjectId: null, hasSummary: () => true, activity: { ...mockForActivity2 }, }); diff --git a/src/widgets/survey/ui/Finish.tsx b/src/widgets/survey/ui/Finish.tsx index e8ddec8da..9dc3f37c3 100644 --- a/src/widgets/survey/ui/Finish.tsx +++ b/src/widgets/survey/ui/Finish.tsx @@ -7,8 +7,11 @@ import { useQueueProcessing } from '@app/entities/activity/lib/hooks/useQueuePro import { useRetryUpload } from '@app/entities/activity/lib/hooks/useRetryUpload'; import { getDefaultQueueProcessingService } from '@app/entities/activity/lib/services/queueProcessingServiceInstance'; import { selectAppletsEntityProgressions } from '@app/entities/applet/model/selectors'; +import { getDefaultAlertsExtractor } from '@app/features/pass-survey/model/alertsExtractorInstance'; +import { getDefaultScoresExtractor } from '@app/features/pass-survey/model/scoresExtractorInstance'; import { useAppDispatch, useAppSelector } from '@app/shared/lib/hooks/redux'; import { getDefaultUploadObservable } from '@app/shared/lib/observables/uploadObservableInstance'; +import { ReduxPersistor } from '@app/shared/lib/redux-state/store'; import { getDefaultLogger } from '@app/shared/lib/services/loggerInstance'; import { ImageBackground } from '@app/shared/ui/ImageBackground'; @@ -115,9 +118,13 @@ export function FinishItem({ async function completeActivity() { const constructCompletionService = new ConstructCompletionsService( null, + getDefaultLogger(), queryClient, getDefaultQueueProcessingService(), + getDefaultAlertsExtractor(), + getDefaultScoresExtractor(), dispatch, + ReduxPersistor, entityProgressions, ); diff --git a/src/widgets/survey/ui/FlowElementSwitch.tsx b/src/widgets/survey/ui/FlowElementSwitch.tsx index 74aa963ca..2b7db0b9e 100644 --- a/src/widgets/survey/ui/FlowElementSwitch.tsx +++ b/src/widgets/survey/ui/FlowElementSwitch.tsx @@ -5,11 +5,11 @@ import { useNavigation } from '@react-navigation/native'; import { ScheduleEvent } from '@app/entities/event/lib/types/event'; import { ActivityIdentityContext } from '@app/features/pass-survey/lib/contexts/ActivityIdentityContext'; import { ActivityStepper } from '@app/features/pass-survey/ui/ActivityStepper'; +import { getDefaultAnalyticsService } from '@app/shared/lib/analytics/analyticsServiceInstance'; import { - AnalyticsService, MixEvents, MixProperties, -} from '@app/shared/lib/analytics/AnalyticsService'; +} from '@app/shared/lib/analytics/IAnalyticsService'; import { colors } from '@app/shared/lib/constants/colors'; import { BackButton } from '@app/shared/ui/BackButton'; import { Box } from '@app/shared/ui/base'; @@ -54,7 +54,7 @@ export function FlowElementSwitch({ const closeAssessment = (reason: 'regular' | 'click-on-return') => { if (reason === 'click-on-return') { - AnalyticsService.track(MixEvents.ReturnToActivitiesPressed, { + getDefaultAnalyticsService().track(MixEvents.ReturnToActivitiesPressed, { [MixProperties.AppletId]: context.appletId, }); } diff --git a/src/widgets/survey/ui/Intermediate.tsx b/src/widgets/survey/ui/Intermediate.tsx index b239c382b..52a509485 100644 --- a/src/widgets/survey/ui/Intermediate.tsx +++ b/src/widgets/survey/ui/Intermediate.tsx @@ -8,10 +8,13 @@ import { useRetryUpload } from '@app/entities/activity/lib/hooks/useRetryUpload' import { getDefaultQueueProcessingService } from '@app/entities/activity/lib/services/queueProcessingServiceInstance'; import { selectAppletsEntityProgressions } from '@app/entities/applet/model/selectors'; import { appletActions } from '@app/entities/applet/model/slice'; +import { getDefaultAlertsExtractor } from '@app/features/pass-survey/model/alertsExtractorInstance'; +import { getDefaultScoresExtractor } from '@app/features/pass-survey/model/scoresExtractorInstance'; import { QueryDataUtils } from '@app/shared/api/services/QueryDataUtils'; import { useAppDispatch, useAppSelector } from '@app/shared/lib/hooks/redux'; import { getDefaultInterimSubmitMutex } from '@app/shared/lib/mutexes/interimSubmitMutexInstance'; import { getDefaultUploadObservable } from '@app/shared/lib/observables/uploadObservableInstance'; +import { ReduxPersistor } from '@app/shared/lib/redux-state/store'; import { getDefaultLogger } from '@app/shared/lib/services/loggerInstance'; import { SubScreenContainer } from './completion/containers'; @@ -169,9 +172,13 @@ export function Intermediate({ const constructCompletionService = new ConstructCompletionsService( saveActivitySummary, + getDefaultLogger(), queryClient, getDefaultQueueProcessingService(), + getDefaultAlertsExtractor(), + getDefaultScoresExtractor(), dispatch, + ReduxPersistor, entityProgressions, ); diff --git a/src/widgets/survey/ui/UploadRetryBanner.tsx b/src/widgets/survey/ui/UploadRetryBanner.tsx index 7531e76ad..42de1549a 100644 --- a/src/widgets/survey/ui/UploadRetryBanner.tsx +++ b/src/widgets/survey/ui/UploadRetryBanner.tsx @@ -4,10 +4,8 @@ import { AccessibilityProps } from 'react-native'; import { useTranslation } from 'react-i18next'; import { AutocompletionEventOptions } from '@app/abstract/lib/types/autocompletion'; -import { - AnalyticsService, - MixEvents, -} from '@app/shared/lib/analytics/AnalyticsService'; +import { getDefaultAnalyticsService } from '@app/shared/lib/analytics/analyticsServiceInstance'; +import { MixEvents } from '@app/shared/lib/analytics/IAnalyticsService'; import { useUploadObservable } from '@app/shared/lib/hooks/useUploadObservable'; import { Emitter } from '@app/shared/lib/services/Emitter'; import { Box } from '@app/shared/ui/base'; @@ -25,7 +23,7 @@ export const UploadRetryBanner: FC = () => { const { t } = useTranslation(); const onRetry = () => { - AnalyticsService.track(MixEvents.RetryButtonPressed); + getDefaultAnalyticsService().track(MixEvents.RetryButtonPressed); Emitter.emit('autocomplete', { checksToExclude: [],