diff --git a/buildConfig/data/staging/form/form-config_webview_version_get.json b/buildConfig/data/staging/form/form-config_webview_version_get.json new file mode 100644 index 0000000000..7d27f91257 --- /dev/null +++ b/buildConfig/data/staging/form/form-config_webview_version_get.json @@ -0,0 +1,32 @@ +{ + "id": "api.form.read", + "params": { + "resmsgid": "cc557f06-5fcf-4feb-919d-627944163857", + "msgid": "f0dcdf53-4949-431c-b804-2dbf21886153", + "status": "successful" + }, + "responseCode": "OK", + "result": { + "form": { + "type": "config", + "subtype": "webview_version", + "action": "get", + "component": "*", + "framework": "*", + "data": { + "templateName": "webview_version", + "action": "get", + "fields": [ + { + "version": "54" + } + ] + }, + "created_on": "2020-01-07T10:51:43.203Z", + "last_modified_on": null, + "rootOrgId": "*" + } + }, + "ts": "2020-01-07T10:52:04.924Z", + "ver": "1.0" +} \ No newline at end of file diff --git a/build_config b/build_config index d17a61c312..5708c0d3ae 100644 --- a/build_config +++ b/build_config @@ -10,6 +10,7 @@ cordova-plugin=cordova-plugin-file-transfer cordova-plugin=cordova-plugin-inappbrowser cordova-plugin=cordova-plugin-network-information cordova-plugin=cordova-plugin-statusbar +cordova-plugin=cordova-plugin-webview-checker cordova-plugin=https://github.com/adriano-di-giovanni/cordova-plugin-shared-preferences.git cordova-plugin=https://github.com/katzer/cordova-plugin-local-notifications.git cordova-plugin=https://github.com/swayangjit/cordova-plugin-fcm-with-dependecy-updated.git#release-2.6.0 @@ -17,7 +18,7 @@ cordova-plugin=cordova-plugin-advanced-http cordova-plugin=cordova-plugin-android-permissions cordova-plugin=cordova-plugin-media cordova-plugin=cordova.plugins.diagnostic -cordova-plugin=https://github.com/swayangjit/cordova-plugin-buildconfig-reader.git +cordova-plugin=https://github.com/swayangjit/cordova-plugin-buildconfig-reader.git#release-2.6.5 cordova-plugin=https://github.com/swayangjit/cordova-plugin-android-downloadmanager.git cordova-plugin=https://github.com/swayangjit/sb-cordova-plugin-db.git cordova-plugin=cordova-android-support-gradle-release --variable ANDROID_SUPPORT_VERSION=28.0.0 @@ -25,4 +26,4 @@ cordova-plugin=https://github.com/Sunbird-Ed/sb-cordova-plugin-customtabs.git -- sunbird-cordova-plugin=https://github.com/project-sunbird/cordova-plugin-openrap.git sunbird-cordova-plugin=https://github.com/project-sunbird/cordova-plugin-qr-scanner.git sunbird-cordova-plugin=https://github.com/project-sunbird/cordova-plugin-sunbirdsplash.git#release-2.6.5 -sunbird-cordova-plugin=https://github.com/project-sunbird/cordova-plugin-file-support.git +sunbird-cordova-plugin=https://github.com/project-sunbird/cordova-plugin-file-support.git#release-2.6.5 diff --git a/config.xml b/config.xml index 85ce7ab29e..f8afe9a3ae 100644 --- a/config.xml +++ b/config.xml @@ -140,5 +140,6 @@ + diff --git a/package.json b/package.json index bc161a820c..70f97aa30d 100644 --- a/package.json +++ b/package.json @@ -93,7 +93,7 @@ "rxjs": "^6.5.3", "sb-cordova-plugin-customtabs": "git+https://github.com/Sunbird-Ed/sb-cordova-plugin-customtabs.git", "sb-cordova-plugin-db": "git+https://github.com/swayangjit/sb-cordova-plugin-db.git", - "sunbird-sdk": "git+https://github.com/Sunbird-Ed/sunbird-mobile-sdk.git#v2.7.5", + "sunbird-sdk": "git+https://github.com/Sunbird-Ed/sunbird-mobile-sdk.git#v2.7.7", "tslib": "^1.10.0", "x2js": "^3.3.0", "zone.js": "~0.8.29" @@ -180,7 +180,8 @@ "cordova-android-support-gradle-release": { "ANDROID_SUPPORT_VERSION": "28.0.0" }, - "cordova-plugin-openrap": {} + "cordova-plugin-openrap": {}, + "cordova-plugin-webview-checker": {} }, "platforms": [ "android" @@ -231,4 +232,4 @@ }, "transformIgnorePatterns": [] } -} +} \ No newline at end of file diff --git a/src/app/app.component.html b/src/app/app.component.html index 04cae011d2..2ab9fcd3bb 100644 --- a/src/app/app.component.html +++ b/src/app/app.component.html @@ -39,4 +39,26 @@ + + + \ No newline at end of file diff --git a/src/app/app.component.ts b/src/app/app.component.ts index 7d91032116..27c816ae18 100644 --- a/src/app/app.component.ts +++ b/src/app/app.component.ts @@ -132,9 +132,43 @@ export class AppComponent implements OnInit, AfterViewInit { this.appRatingService.checkInitialDate(); this.getUtmParameter(); this.checkForCodeUpdates(); + this.checkAndroidWebViewVersion(); }); } + checkAndroidWebViewVersion() { + var that = this; + plugins['webViewChecker'].getCurrentWebViewPackageInfo() + .then(function(packageInfo) { + that.formAndFrameworkUtilService.getWebviewConfig().then(function(webviewVersion) { + if (parseInt(packageInfo.versionName.split('.')[0], 10) <= webviewVersion) { + document.getElementById('update-webview-container').style.display = 'block'; + this.telemetryGeneratorService.generateImpressionTelemetry( + ImpressionType.VIEW, '', + PageId.UPDATE_WEBVIEW_POPUP, + Environment.HOME); + } + }).catch(function(err) { + if (parseInt(packageInfo.versionName.split('.')[0], 10) <= 54) { + document.getElementById('update-webview-container').style.display = 'block'; + } + }); + }) + .catch(function(error) { }); + } + + openPlaystore() { + plugins['webViewChecker'].openGooglePlayPage() + .then(function() { }) + .catch(function(error) { }); + + this.telemetryGeneratorService.generateInteractTelemetry( + InteractType.TOUCH, + InteractSubtype.UPDATE_WEBVIEW_CLICKED, + Environment.HOME, + PageId.UPDATE_WEBVIEW_POPUP); + } + getSystemConfig() { const getSystemSettingsRequest: GetSystemSettingsRequest = { id: SystemSettingsIds.COURSE_FRAMEWORK_ID @@ -739,10 +773,7 @@ export class AppComponent implements OnInit, AfterViewInit { && !(await this.commonUtilService.isIpLocationAvailable())) { this.deviceRegisterService.getDeviceProfile().toPromise().then(async (response) => { if (response.userDeclaredLocation) { - const locationMap = new Map(); - locationMap['state'] = response.userDeclaredLocation.state; - locationMap['district'] = response.userDeclaredLocation.district; - await this.preferences.putString(PreferenceKey.DEVICE_LOCATION, JSON.stringify(locationMap)).toPromise(); + await this.preferences.putString(PreferenceKey.DEVICE_LOCATION, JSON.stringify(response.userDeclaredLocation)).toPromise(); } else if (response.ipLocation) { const ipLocationMap = new Map(); if (response.ipLocation.state) { diff --git a/src/app/components/popups/account-recovery-id/account-recovery-id-popup.component.spec.ts b/src/app/components/popups/account-recovery-id/account-recovery-id-popup.component.spec.ts new file mode 100644 index 0000000000..5f614847a3 --- /dev/null +++ b/src/app/components/popups/account-recovery-id/account-recovery-id-popup.component.spec.ts @@ -0,0 +1,211 @@ +import { AccountRecoveryInfoComponent } from './account-recovery-id-popup.component'; +import { CommonUtilService, TelemetryGeneratorService, AppGlobalService } from '../../../../services'; +import { PopoverController, Platform, MenuController } from '@ionic/angular'; +import { ImpressionType, Environment, PageId } from '@app/services/telemetry-constants'; +import { of, throwError } from 'rxjs'; +import { ProfileService } from 'sunbird-sdk'; + +describe('AccountRecoveryInfoComponent', () => { + let accountRecoveryInfoComponent: AccountRecoveryInfoComponent; + + const serverProfileResponse = { response: 'SUCCESS' } as any; + const mockProfileService: Partial = { + updateServerProfile: jest.fn(() => of(serverProfileResponse)), + }; + + const mockTelemetryGeneratorService: Partial = { + generateImpressionTelemetry: jest.fn(), + generateInteractTelemetry: jest.fn() + }; + + const profile = { uid: '0123456789' } as any; + const mockAppGlobalService: Partial = { + getCurrentUser: jest.fn(() => profile) + }; + + const mockPlatform: Partial = { + }; + + const mockCommonUtilService: Partial = { + showToast: jest.fn(() => { }) + }; + const dismissFn = jest.fn(() => Promise.resolve()); + const presentFn = jest.fn(() => Promise.resolve()); + mockCommonUtilService.getLoader = jest.fn(() => ({ + present: presentFn, + dismiss: dismissFn, + })); + + const mockPopoverCtrl: Partial = { + dismiss: jest.fn() + }; + + const mockMenuController: Partial = { + enable: jest.fn() + }; + + beforeAll(() => { + accountRecoveryInfoComponent = new AccountRecoveryInfoComponent( + mockProfileService as ProfileService, + mockTelemetryGeneratorService as TelemetryGeneratorService, + mockAppGlobalService as AppGlobalService, + mockCommonUtilService as CommonUtilService, + mockPopoverCtrl as PopoverController, + mockPlatform as Platform, + mockMenuController as MenuController, + ); + }); + + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('should be create a instance of EditContactDetailsPopupComponent', () => { + expect(accountRecoveryInfoComponent).toBeTruthy(); + }); + + it('should generate IMPRESSION telemtry on ngOnInit', () => { + // arrange + accountRecoveryInfoComponent.recoveryPhone = 'phone'; + // act + accountRecoveryInfoComponent.ngOnInit(); + // assert + expect(accountRecoveryInfoComponent.profile).toEqual({ uid: '0123456789' }); + expect(mockTelemetryGeneratorService.generateImpressionTelemetry).toHaveBeenCalledWith( + ImpressionType.VIEW, '', + PageId.RECOVERY_ACCOUNT_ID_POPUP, + Environment.USER + ); + expect(mockMenuController.enable).toHaveBeenCalledWith(false); + }); + + it('should handle the back button in ionViewWillEnter ', () => { + // arrange + const subscribeWithPriorityData = jest.fn((_, fn) => fn()); + mockPlatform.backButton = { + subscribeWithPriority: subscribeWithPriorityData, + + } as any; + + accountRecoveryInfoComponent.unregisterBackButton = { + unsubscribe: jest.fn(), + } as any; + + // act + accountRecoveryInfoComponent.ionViewWillEnter(); + // assert + expect(mockPopoverCtrl.dismiss).toHaveBeenCalled(); + }); + + it('should enable MenuDrawer and unsubscribe back function', () => { + // arrange + accountRecoveryInfoComponent.unregisterBackButton = { + unsubscribe: jest.fn(), + + } as any; + // act + accountRecoveryInfoComponent.ionViewWillLeave(); + // assert + expect(mockMenuController.enable).toHaveBeenCalledWith(true); + expect(accountRecoveryInfoComponent.unregisterBackButton.unsubscribe).toHaveBeenCalled(); + }); + + it('should update the server profile successfully', (done) => { + // arrange + accountRecoveryInfoComponent.recoveryPhone = ''; + mockCommonUtilService.networkInfo = { isNetworkAvailable: true }; + accountRecoveryInfoComponent.recoveryEmailForm = { value: { email: 'abc@email.com' } } as any; + // act + accountRecoveryInfoComponent.submitRecoveryId(accountRecoveryInfoComponent.RecoveryType.EMAIL); + // assert + setTimeout(() => { + expect(mockPopoverCtrl.dismiss).toHaveBeenCalledWith({ isEdited: true }); + expect(mockTelemetryGeneratorService.generateInteractTelemetry).toHaveBeenCalled(); + done(); + }, 1); + }); + + it('should handle the error scenario incase of API failure while updating email', (done) => { + // arrange + accountRecoveryInfoComponent.recoveryPhone = ''; + mockCommonUtilService.networkInfo = { isNetworkAvailable: true }; + accountRecoveryInfoComponent.recoveryEmailForm = { value: { email: 'abc@email.com' } } as any; + mockProfileService.updateServerProfile = jest.fn(() => throwError( + { response: { body: { params: { err: 'RECOVERY_PARAM_MATCH_EXCEPTION' } } } })); + // act + accountRecoveryInfoComponent.submitRecoveryId(accountRecoveryInfoComponent.RecoveryType.EMAIL); + // assert + setTimeout(() => { + expect(accountRecoveryInfoComponent.sameEmailErr).toBeTruthy(); + done(); + }, 1); + }); + + + it('should handle the error scenario incase of API failure while updating phone', (done) => { + // arrange + accountRecoveryInfoComponent.recoveryPhone = 'phone'; + mockCommonUtilService.networkInfo = { isNetworkAvailable: true }; + accountRecoveryInfoComponent.recoveryEmailForm = { value: { phone: '0123456789' } } as any; + mockProfileService.updateServerProfile = jest.fn(() => throwError( + { response: { body: { params: { err: 'RECOVERY_PARAM_MATCH_EXCEPTION' } } } })); + // act + accountRecoveryInfoComponent.submitRecoveryId(accountRecoveryInfoComponent.RecoveryType.PHONE); + // assert + setTimeout(() => { + expect(accountRecoveryInfoComponent.samePhoneErr).toBeTruthy(); + done(); + }, 1); + }); + + it('should show TOAST in case of unexpected error scenario incase of API failure while updating phone', (done) => { + // arrange + accountRecoveryInfoComponent.recoveryPhone = 'phone'; + mockCommonUtilService.networkInfo = { isNetworkAvailable: true }; + accountRecoveryInfoComponent.recoveryEmailForm = { value: { phone: '0123456789' } } as any; + mockProfileService.updateServerProfile = jest.fn(() => throwError( + { response: { body: { params: { err: 'ANY_OTHER_ERROR' } } } })); + // act + accountRecoveryInfoComponent.submitRecoveryId(accountRecoveryInfoComponent.RecoveryType.PHONE); + // assert + setTimeout(() => { + expect(mockCommonUtilService.showToast).toHaveBeenCalledWith('SOMETHING_WENT_WRONG'); + done(); + }, 1); + }); + + it('should show NETWORK ERROR popup in case of no network connection', () => { + // arrange + mockCommonUtilService.networkInfo = { isNetworkAvailable: false }; + // act + accountRecoveryInfoComponent.submitRecoveryId(accountRecoveryInfoComponent.RecoveryType.PHONE); + // assert + expect(mockCommonUtilService.showToast).toHaveBeenCalledWith('INTERNET_CONNECTIVITY_NEEDED'); + }); + + it('should dismiss the popup when cancel is clicked', () => { + // arrange + // act + accountRecoveryInfoComponent.cancel(); + // assert + expect(mockPopoverCtrl.dismiss).toHaveBeenCalledWith({ isEdited: false }); + }); + + it('should reset the error properties', () => { + // arrange + accountRecoveryInfoComponent.sameEmailErr = true; + // act + accountRecoveryInfoComponent.removeSameRecoveryIdErr(accountRecoveryInfoComponent.RecoveryType.EMAIL); + // assert + expect(accountRecoveryInfoComponent.sameEmailErr).toBeFalsy(); + + // arrange + accountRecoveryInfoComponent.samePhoneErr = true; + // act + accountRecoveryInfoComponent.removeSameRecoveryIdErr(accountRecoveryInfoComponent.RecoveryType.PHONE); + // assert + expect(accountRecoveryInfoComponent.samePhoneErr).toBeFalsy(); + }); + + +}); diff --git a/src/app/components/popups/account-recovery-id/account-recovery-id-popup.component.ts b/src/app/components/popups/account-recovery-id/account-recovery-id-popup.component.ts index 55d96dc591..6b0d3fc8a1 100644 --- a/src/app/components/popups/account-recovery-id/account-recovery-id-popup.component.ts +++ b/src/app/components/popups/account-recovery-id/account-recovery-id-popup.component.ts @@ -120,9 +120,7 @@ export class AccountRecoveryInfoComponent implements OnInit { this.telemetryGeneratorService.generateImpressionTelemetry( ImpressionType.VIEW, '', PageId.RECOVERY_ACCOUNT_ID_POPUP, - Environment.USER, '', '', '', - undefined, - undefined + Environment.USER ); } diff --git a/src/app/components/popups/sb-generic-popover/sb-generic-popover.component.spec.ts b/src/app/components/popups/sb-generic-popover/sb-generic-popover.component.spec.ts new file mode 100644 index 0000000000..5fe54695e0 --- /dev/null +++ b/src/app/components/popups/sb-generic-popover/sb-generic-popover.component.spec.ts @@ -0,0 +1,85 @@ +import { Events, PopoverController, Platform } from '@ionic/angular'; +import { SbGenericPopoverComponent } from '../sb-generic-popover/sb-generic-popover.component'; + +describe('SbGenericPopoverComponent', () => { + let sbGenericPopoverComponent: SbGenericPopoverComponent; + + const mockEventsResponse = { selectedContents: ['do_1234'] }; + const mockEvents: Partial = { + subscribe: jest.fn(() => mockEventsResponse), + unsubscribe: jest.fn() + }; + + const mockPopOverController: Partial = { + dismiss: jest.fn() + }; + + const mockPlatform: Partial = { + }; + + beforeAll(() => { + sbGenericPopoverComponent = new SbGenericPopoverComponent( + mockPopOverController as PopoverController, + mockPlatform as Platform, + mockEvents as Events + ); + }); + + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('should create a instance of SbGenericPopoverComponent', () => { + expect(sbGenericPopoverComponent).toBeTruthy(); + }); + + it('should subscribe to back button and events subscription', () => { + // arrange + const subscribeWithPriorityData = jest.fn((_, fn) => fn()); + mockPlatform.backButton = { + subscribeWithPriority: subscribeWithPriorityData, + } as any; + + const unsubscribeFn = jest.fn(); + sbGenericPopoverComponent.backButtonFunc = { + unsubscribe: unsubscribeFn, + } as any; + + // act + sbGenericPopoverComponent.ngOnInit(); + // assert + expect(mockPopOverController.dismiss).toHaveBeenCalledWith({ isLeftButtonClicked: null }); + // expect(sbGenericPopoverComponent.selectedContents).toEqual(mockEventsResponse); + expect(unsubscribeFn).toHaveBeenCalled(); + }); + + it('should unsubscribe to back button and events on ngOnDestroy', () => { + // arrange + sbGenericPopoverComponent.backButtonFunc = { + unsubscribe: jest.fn(), + } as any; + // act + sbGenericPopoverComponent.ngOnDestroy(); + // assert + expect(mockEvents.unsubscribe).toHaveBeenCalledWith('selectedContents:changed'); + // expect(sbGenericPopoverComponent.selectedContents).toEqual(mockEventsResponse); + expect(sbGenericPopoverComponent.backButtonFunc.unsubscribe).toHaveBeenCalled(); + }); + + it('should dismiss the popup on closePopOver', () => { + // arrange + // act + sbGenericPopoverComponent.closePopover(); + // assert + expect(mockPopOverController.dismiss).toHaveBeenCalledWith({ isLeftButtonClicked: null }); + }); + + it('should dismiss the popup on deleteContent', () => { + // arrange + // act + sbGenericPopoverComponent.deleteContent(1); + // assert + expect(mockPopOverController.dismiss).toHaveBeenCalledWith({ isLeftButtonClicked: false }); + }); + +}); diff --git a/src/app/components/popups/sb-generic-popover/sb-generic-popover.component.ts b/src/app/components/popups/sb-generic-popover/sb-generic-popover.component.ts index e711eec829..cbf0c25b76 100644 --- a/src/app/components/popups/sb-generic-popover/sb-generic-popover.component.ts +++ b/src/app/components/popups/sb-generic-popover/sb-generic-popover.component.ts @@ -19,7 +19,10 @@ export class SbGenericPopoverComponent implements OnInit, OnDestroy { @Input() showHeader = true; backButtonFunc: Subscription; - constructor(public popoverCtrl: PopoverController, private platform: Platform, private events: Events) { } + constructor( + public popoverCtrl: PopoverController, + private platform: Platform, + private events: Events) { } ngOnInit() { this.backButtonFunc = this.platform.backButton.subscribeWithPriority(11, () => { diff --git a/src/app/components/popups/sb-insufficient-storage-popup/sb-insufficient-storage-popup.ts b/src/app/components/popups/sb-insufficient-storage-popup/sb-insufficient-storage-popup.ts index 25a0a202ae..309a346758 100644 --- a/src/app/components/popups/sb-insufficient-storage-popup/sb-insufficient-storage-popup.ts +++ b/src/app/components/popups/sb-insufficient-storage-popup/sb-insufficient-storage-popup.ts @@ -12,12 +12,12 @@ export class SbInsufficientStoragePopupComponent { sbPopoverMessage = ''; constructor(private navParams: NavParams, - private popoverCtrl: PopoverController, - private router: Router) { + private popoverCtrl: PopoverController, + private router: Router) { this.initParams(); } - private initParams() { + initParams() { this.sbPopoverHeading = this.navParams.get('sbPopoverHeading'); this.sbPopoverMessage = this.navParams.get('sbPopoverMessage'); } @@ -28,7 +28,6 @@ export class SbInsufficientStoragePopupComponent { navigateToStorageSettings() { this.popoverCtrl.dismiss(); - // this.app.getActiveNav().push(StorageSettingsPage); this.router.navigate([RouterLinks.STORAGE_SETTINGS]); } diff --git a/src/app/components/popups/sb-insufficient-storage-popup/sb-insufficient-storage.popup.spec.ts b/src/app/components/popups/sb-insufficient-storage-popup/sb-insufficient-storage.popup.spec.ts new file mode 100644 index 0000000000..e4cbdf3659 --- /dev/null +++ b/src/app/components/popups/sb-insufficient-storage-popup/sb-insufficient-storage.popup.spec.ts @@ -0,0 +1,72 @@ +import { SbInsufficientStoragePopupComponent } from './sb-insufficient-storage-popup'; +import { Events, PopoverController, Platform, NavParams } from '@ionic/angular'; +import { Router } from '@angular/router'; + +describe('SbInsufficientStoragePopupComponent', () => { + let sbInsufficientStoragePopupComponent: SbInsufficientStoragePopupComponent; + + const mockNavParams: Partial = { + get: jest.fn((arg) => { + let value; + switch (arg) { + case 'sbPopoverHeading': + value = 'sample_heading'; + break; + case 'sbPopoverMessage': + value = 'sample_message'; + break; + } + return value; + }) + }; + + const mockPopOverController: Partial = { + dismiss: jest.fn() + }; + + const mockRouter: Partial = { + navigate: jest.fn() + }; + + beforeAll(() => { + sbInsufficientStoragePopupComponent = new SbInsufficientStoragePopupComponent( + mockNavParams as NavParams, + mockPopOverController as PopoverController, + mockRouter as Router + ); + }); + + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('should create a instance of SbInsufficientStoragePopupComponent', () => { + expect(sbInsufficientStoragePopupComponent).toBeTruthy(); + }); + + it('should initialize the sbPopoverHeading and sbPopoverMessage', () => { + // arrange + // act + sbInsufficientStoragePopupComponent.initParams(); + // assert + expect(sbInsufficientStoragePopupComponent.sbPopoverMessage).toBeDefined(); + expect(sbInsufficientStoragePopupComponent.sbPopoverHeading).toBeDefined(); + }); + + it('should navigate to StorageSettings page', () => { + // arrange + // act + sbInsufficientStoragePopupComponent.navigateToStorageSettings(); + // assert + expect(mockRouter.navigate).toHaveBeenCalledWith(['storage-settings']); + }); + + it('should close popover closePopover', () => { + // arrange + // act + sbInsufficientStoragePopupComponent.closePopover(); + // assert + expect(mockPopOverController.dismiss).toHaveBeenCalled(); + }); + +}); diff --git a/src/app/components/popups/sb-no-network-popup/sb-no-network.popup.spec.ts b/src/app/components/popups/sb-no-network-popup/sb-no-network.popup.spec.ts new file mode 100644 index 0000000000..1112b44756 --- /dev/null +++ b/src/app/components/popups/sb-no-network-popup/sb-no-network.popup.spec.ts @@ -0,0 +1,34 @@ +import { SbNoNetworkPopupComponent } from './sb-no-network-popup.component'; +import { Events, PopoverController, Platform, NavParams } from '@ionic/angular'; +import { Router } from '@angular/router'; + +describe('SbNoNetworkPopupComponent', () => { + let sbNoNetworkPopupComponent: SbNoNetworkPopupComponent; + const mockPopOverController: Partial = { + dismiss: jest.fn() + }; + + beforeAll(() => { + sbNoNetworkPopupComponent = new SbNoNetworkPopupComponent( + mockPopOverController as PopoverController, + ); + }); + + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('should create a instance of SbNoNetworkPopupComponent', () => { + expect(sbNoNetworkPopupComponent).toBeTruthy(); + }); + + it('should close popover closePopover', () => { + // arrange + // act + sbNoNetworkPopupComponent.closePopover(); + // assert + expect(mockPopOverController.dismiss).toHaveBeenCalled(); + }); + +}); + diff --git a/src/app/components/popups/sb-popover/sb-popover.component.spec.ts b/src/app/components/popups/sb-popover/sb-popover.component.spec.ts new file mode 100644 index 0000000000..477d118338 --- /dev/null +++ b/src/app/components/popups/sb-popover/sb-popover.component.spec.ts @@ -0,0 +1,151 @@ +import { SbPopoverComponent } from './sb-popover.component'; +import { PopoverController, Platform, NavParams } from '@ionic/angular'; +import { NgZone } from '@angular/core'; +import { of } from 'rxjs'; + +describe('SbPopoverComponent', () => { + let sbPopoverComponent: SbPopoverComponent; + const mockNavParams: Partial = { + get: jest.fn((arg) => { + let value; + switch (arg) { + case 'actionsButtons': + value = [ + { + btntext: 'OKAY', + btnClass: 'popover-color', + btnDisabled$: of([]) + } + ]; + break; + case 'sbPopoverDynamicContent': + value = of([]); + break; + case 'sbPopoverDynamicMainTitle': + value = of([]); + break; + case 'btnDisabled': + value = of([]); + break; + case 'isChild': + value = true; + break; + case 'handler': + value = () => { + }; + break; + } + return value; + }) + }; + + const mockPlatform: Partial = { + }; + + const mockNgZone: Partial = { + run: jest.fn((fn) => fn()) + }; + + const mockPopOverController: Partial = { + dismiss: jest.fn() + }; + + beforeAll(() => { + sbPopoverComponent = new SbPopoverComponent( + mockNavParams as NavParams, + mockPlatform as Platform, + mockNgZone as NgZone, + mockPopOverController as PopoverController + ); + }); + + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('should create a instance of SbInsufficientStoragePopupComponent', () => { + expect(sbPopoverComponent).toBeTruthy(); + }); + + it('should poulate all properties provided in navparams', () => { + expect(sbPopoverComponent.actionsButtons).toBeDefined(); + expect(sbPopoverComponent.sbPopoverDynamicContent$).toBeDefined(); + expect(sbPopoverComponent.sbPopoverDynamicMainTitle$).toBeDefined(); + }); + + describe('closePopOver()', () => { + it('should close popover', () => { + // arrange + // act + sbPopoverComponent.closePopover(true); + // assert + expect(mockPopOverController.dismiss).toHaveBeenCalledWith({ closeDeletePopOver: true }); + }); + }); + + describe('deleteContent()', () => { + it('should close popover', () => { + // arrange + // act + sbPopoverComponent.deleteContent(true); + // assert + expect(mockPopOverController.dismiss).toHaveBeenCalledWith({ canDelete: true }); + }); + + it('should invoke handler method passed by navparams', () => { + // arrange + // act + sbPopoverComponent.deleteContent(true, 'clickedButtonText'); + // assert + expect(mockPopOverController.dismiss).toHaveBeenCalledWith({ canDelete: true }); + }); + }); + + describe('ionViewWillEnter()', () => { + it('should dismiss the popup when backButton is clicked', () => { + // arrange + mockPlatform.backButton = { + subscribeWithPriority: jest.fn((_, fn) => fn()), + } as any; + + const unsubscribeFn = jest.fn(); + sbPopoverComponent.backButtonFunc = { + unsubscribe: unsubscribeFn + } as any; + // act + sbPopoverComponent.ionViewWillEnter(); + // assert + expect(mockPopOverController.dismiss).toHaveBeenCalled(); + expect(unsubscribeFn).toHaveBeenCalled(); + }); + }); + + describe('ngOnDestroy()', () => { + it('should unsubscribe all subscription', () => { + // arrange + const unsubscribeFn = jest.fn(); + + sbPopoverComponent.sbPopoverDynamicMainTitleSubscription = { + unsubscribe: unsubscribeFn + } as any; + + sbPopoverComponent.sbPopoverDynamicButtonDisabledSubscription = { + unsubscribe: unsubscribeFn + } as any; + + sbPopoverComponent.sbPopoverDynamicContentSubscription = { + unsubscribe: unsubscribeFn + } as any; + + sbPopoverComponent.backButtonFunc = { + unsubscribe: unsubscribeFn + } as any; + + // act + sbPopoverComponent.ngOnDestroy(); + // assert + expect(unsubscribeFn).toHaveBeenCalledTimes(4); + }); + }); + +}); diff --git a/src/app/components/popups/sb-popover/sb-popover.component.ts b/src/app/components/popups/sb-popover/sb-popover.component.ts index 50356ffde2..9e85142249 100644 --- a/src/app/components/popups/sb-popover/sb-popover.component.ts +++ b/src/app/components/popups/sb-popover/sb-popover.component.ts @@ -30,12 +30,12 @@ export class SbPopoverComponent implements OnDestroy { public objRollup: Rollup; public commonUtilService: CommonUtilService; - private corRelationList: Array; - private sbPopoverDynamicMainTitle$?: Observable; - private sbPopoverDynamicMainTitleSubscription?: Subscription; - private sbPopoverDynamicContent$?: Observable; - private sbPopoverDynamicContentSubscription?: Subscription; - private sbPopoverDynamicButtonDisabledSubscription?: Subscription; + public corRelationList: Array; + public sbPopoverDynamicMainTitle$?: Observable; + public sbPopoverDynamicMainTitleSubscription?: Subscription; + public sbPopoverDynamicContent$?: Observable; + public sbPopoverDynamicContentSubscription?: Subscription; + public sbPopoverDynamicButtonDisabledSubscription?: Subscription; constructor( public navParams: NavParams, private platform: Platform, diff --git a/src/app/components/popups/teacher-id-verification-popup/teacher-id-verification-popup.component.ts b/src/app/components/popups/teacher-id-verification-popup/teacher-id-verification-popup.component.ts index 2dc7137cb7..3333698d6a 100644 --- a/src/app/components/popups/teacher-id-verification-popup/teacher-id-verification-popup.component.ts +++ b/src/app/components/popups/teacher-id-verification-popup/teacher-id-verification-popup.component.ts @@ -7,14 +7,12 @@ import { TelemetryGeneratorService } from '@app/services/telemetry-generator.ser import { featureIdMap } from '@app/feature-id-map'; import { Environment, - ImpressionSubtype, ImpressionType, InteractSubtype, InteractType, PageId, ID } from '@app/services/telemetry-constants'; -import { map } from 'rxjs/operators'; export enum TeacherIdPopupFlags { STATE_CONFIRMATION = 'stateConfirmation', @@ -46,11 +44,11 @@ export class TeacherIdVerificationComponent implements OnInit { tenantMessages: any; constructor( + @Inject('PROFILE_SERVICE') private profileService: ProfileService, private popOverCtrl: PopoverController, private navParams: NavParams, private telemetryGeneratorService: TelemetryGeneratorService, - private commonUtilService: CommonUtilService, - @Inject('PROFILE_SERVICE') private profileService: ProfileService) { + private commonUtilService: CommonUtilService) { if (this.navParams.data) { this.userFeed = this.navParams.data.userFeed; this.stateList = this.userFeed.data.prospectChannels; @@ -86,7 +84,6 @@ export class TeacherIdVerificationComponent implements OnInit { }; this.profileService.userMigrate(req).toPromise() .then(async (response) => { - console.log('UserMigrateResponse', response); this.count = 0; if ((response.responseCode).toLowerCase() === TeacherIdPopupFlags.OK) { this.teacherIdFlag = TeacherIdPopupFlags.VERIFIED_STATE_ID; @@ -99,17 +96,15 @@ export class TeacherIdVerificationComponent implements OnInit { } } - private initializeFormFields() { + initializeFormFields() { this.teacherIdFlag = TeacherIdPopupFlags.STATE_ID_INPUT; this.teacherIdForm = new FormGroup({ teacherId: new FormControl('', Validators.requiredTrue) }); - console.log('teacherId initialise', this.teacherIdForm.value.teacherId); } submitTeacherId() { this.count++; - // this.disableButton = true; this.telemetryGeneratorService.generateInteractTelemetry( InteractType.TOUCH, '', @@ -130,14 +125,12 @@ export class TeacherIdVerificationComponent implements OnInit { feedId: this.userFeed.id }; this.externalUserVerfication(req); - console.log('UserMigrateRequest', req, this.count); } } externalUserVerfication(req) { this.profileService.userMigrate(req).toPromise() .then(async (response) => { - console.log('UserMigrateResponse', response); this.count = 0; if ((response.responseCode).toLowerCase() === TeacherIdPopupFlags.INVALIDEXTERNALID) { this.showTeacherIdIncorrectErr = true; @@ -158,7 +151,6 @@ export class TeacherIdVerificationComponent implements OnInit { } }) .catch((error) => { - console.log('error', error); if (error instanceof HttpClientError) { if (error.response.responseCode === 400) { this.generateTelemetryForFailedVerification(); @@ -168,13 +160,13 @@ export class TeacherIdVerificationComponent implements OnInit { this.teacherIdFlag = TeacherIdPopupFlags.FAILED_STATE_ID; } else if (error.response.responseCode === 429) { this.closePopup(); - this.commonUtilService.showToast(this.commonUtilService.translateMessage('USER_IS_NOT_VERIFIED')); + this.commonUtilService.showToast('USER_IS_NOT_VERIFIED'); } else if (error.response.responseCode === 401) { this.closePopup(); - this.commonUtilService.showToast(this.commonUtilService.translateMessage('USER_IS_NOT_VERIFIED')); + this.commonUtilService.showToast('USER_IS_NOT_VERIFIED'); } else { this.closePopup(); - this.commonUtilService.showToast(this.commonUtilService.translateMessage('USER_IS_NOT_VERIFIED')); + this.commonUtilService.showToast('USER_IS_NOT_VERIFIED'); } } }); @@ -194,7 +186,7 @@ export class TeacherIdVerificationComponent implements OnInit { undefined, undefined, undefined, - featureIdMap.userVerification.EXTERNAL_USER_VERIFICATION, + featureIdMap.userVerification.EXTERNAL_USER_VERIFICATION ); } generateTelemetryForFailedVerification() { diff --git a/src/app/components/popups/teacher-id-verification-popup/teacher-id-verification.component.spec.ts b/src/app/components/popups/teacher-id-verification-popup/teacher-id-verification.component.spec.ts new file mode 100644 index 0000000000..63a8dd9872 --- /dev/null +++ b/src/app/components/popups/teacher-id-verification-popup/teacher-id-verification.component.spec.ts @@ -0,0 +1,362 @@ +import { TeacherIdVerificationComponent } from './teacher-id-verification-popup.component'; +import { PopoverController, NavParams } from '@ionic/angular'; +import { TelemetryGeneratorService, CommonUtilService } from '../../../../services'; +import { of, throwError } from 'rxjs'; +import { ProfileService, HttpClientError, Response } from 'sunbird-sdk'; +import { featureIdMap } from '@app/feature-id-map'; +import { + Environment, + ImpressionType, + InteractSubtype, + InteractType, + PageId, + ID +} from '@app/services/telemetry-constants'; + +describe('TeacherIdVerificationComponent', () => { + let teacherIdVerificationComponent: TeacherIdVerificationComponent; + + const userMigrationResponse = { responseCode: 'ok' } as any; + const mockProfileService: Partial = { + userMigrate: jest.fn(() => of(userMigrationResponse)) + }; + const mockPopOverController: Partial = { + dismiss: jest.fn() + }; + const mockNavParams: Partial = { + data: { + userFeed: { id: '0123456789', userId: '0123456789', data: { prospectChannels: ['tn'] } }, + tenantMessages: {} + } + }; + + const mockTelemetryGeneratorService: Partial = { + generateImpressionTelemetry: jest.fn(), + generateInteractTelemetry: jest.fn() + }; + + const mockCommonUtilService: Partial = { + showToast: jest.fn() + }; + + + beforeAll(() => { + teacherIdVerificationComponent = new TeacherIdVerificationComponent( + mockProfileService as ProfileService, + mockPopOverController as PopoverController, + mockNavParams as NavParams, + mockTelemetryGeneratorService as TelemetryGeneratorService, + mockCommonUtilService as CommonUtilService, + ); + }); + + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('should create a instance of TeacherIdVerificationComponent', () => { + expect(teacherIdVerificationComponent).toBeTruthy(); + }); + + it('should generate IMPRESSION telemetry on ngOnit', () => { + // arrange + // act + teacherIdVerificationComponent.ngOnInit(); + // assert + expect(mockTelemetryGeneratorService.generateImpressionTelemetry).toHaveBeenCalledWith(ImpressionType.VIEW, + '', + PageId.EXTERNAL_USER_VERIFICATION_POPUP, + Environment.HOME, '', '', '', undefined, featureIdMap.userVerification.EXTERNAL_USER_VERIFICATION); + }); + + it('should close popover and generate INTERACT telemetry', () => { + // arrange + // act + teacherIdVerificationComponent.cancelPopup('SAMPLE_MESSAGE'); + // assert + expect(mockPopOverController.dismiss).toHaveBeenCalled(); + const reason = new Map(); + reason['popup_close'] = 'SAMPLE_MESSAGE'; + expect(mockTelemetryGeneratorService.generateInteractTelemetry).toHaveBeenCalledWith( InteractType.TOUCH, + InteractSubtype.POPUP_DISMISSED, + Environment.HOME, + PageId.EXTERNAL_USER_VERIFICATION_POPUP, + undefined, + reason, + undefined, + featureIdMap.userVerification.EXTERNAL_USER_VERIFICATION); + }); + + it('should close popover and generate INTERACT telemetry', () => { + // arrange + // act + teacherIdVerificationComponent.selectState('SAMPLE_STATE'); + // assert + expect(teacherIdVerificationComponent.stateName).toEqual('SAMPLE_STATE'); + expect(teacherIdVerificationComponent.showStates).toBeFalsy(); + }); + + it('should close the popOver on closePopup', () => { + // arrange + // act + teacherIdVerificationComponent.closePopup(); + // assert + expect(mockPopOverController.dismiss).toHaveBeenCalled(); + expect(mockTelemetryGeneratorService.generateInteractTelemetry).toHaveBeenCalledWith(InteractType.TOUCH, + InteractSubtype.POPUP_DISMISSED, + Environment.HOME, + PageId.EXTERNAL_USER_VERIFICATION_POPUP, + undefined, + undefined, + undefined, + featureIdMap.userVerification.EXTERNAL_USER_VERIFICATION); + }); + + describe('teacherConfirmation()', () => { + it('should generate INTERACT telemetry if user confirms verification', () => { + // arrange + jest.spyOn(teacherIdVerificationComponent, 'initializeFormFields'); + // act + teacherIdVerificationComponent.teacherConfirmation(true); + // assert + expect(teacherIdVerificationComponent.initializeFormFields).toHaveBeenCalled(); + expect(mockTelemetryGeneratorService.generateInteractTelemetry).toHaveBeenCalledWith(InteractType.TOUCH, + '', + Environment.HOME, + PageId.EXTERNAL_USER_VERIFICATION_POPUP, + undefined, + undefined, + undefined, + featureIdMap.userVerification.EXTERNAL_USER_VERIFICATION, + ID.USER_VERIFICATION_CONFIRMED); + }); + + it('should generate INTERACT telemetry if user rejects verification', () => { + // arrange + // act + teacherIdVerificationComponent.teacherConfirmation(false); + // assert + expect(mockTelemetryGeneratorService.generateInteractTelemetry).toHaveBeenCalledWith(InteractType.TOUCH, + '', + Environment.HOME, + PageId.EXTERNAL_USER_VERIFICATION_POPUP, + undefined, + undefined, + undefined, + featureIdMap.userVerification.EXTERNAL_USER_VERIFICATION, + ID.USER_VERIFICATION_REJECTED); + }); + + it('should invoke userMigrate API with reject status if user rejects verification', (done) => { + // arrange + jest.spyOn(teacherIdVerificationComponent, 'closePopup'); + teacherIdVerificationComponent.stateName = ''; + // act + teacherIdVerificationComponent.teacherConfirmation(false); + // assert + expect(mockProfileService.userMigrate).toHaveBeenCalledWith({ + userId: '0123456789', + action: 'reject', + channel: 'tn', + feedId: '0123456789' + }); + + setTimeout(() => { + expect(teacherIdVerificationComponent.closePopup).toHaveBeenCalled(); + expect(teacherIdVerificationComponent.count).toEqual(0); + expect(teacherIdVerificationComponent.teacherIdFlag).toEqual('verifiedStateId'); + done(); + }, 0); + }); + + it('should close the popup in vase of API failure', (done) => { + // arrange + jest.spyOn(teacherIdVerificationComponent, 'closePopup'); + mockProfileService.userMigrate = jest.fn(() => throwError({ error: 'API_ERROR' })); + // act + teacherIdVerificationComponent.teacherConfirmation(false); + // assert + setTimeout(() => { + expect(teacherIdVerificationComponent.closePopup).toHaveBeenCalled(); + done(); + }, 0); + }); + }); + + describe('submitTeacherId()', () => { + it('should generate INTERACT telemetry if user confirms verification', () => { + // arrange + teacherIdVerificationComponent.teacherIdForm = { value: { teacherId: 'SAMPLE_TEACHERID' } } as any; + teacherIdVerificationComponent.stateName = ''; + jest.spyOn(teacherIdVerificationComponent, 'externalUserVerfication'); + // act + teacherIdVerificationComponent.submitTeacherId(); + // assert + expect(teacherIdVerificationComponent.count).toEqual(1); + expect(teacherIdVerificationComponent.externalUserVerfication).toHaveBeenCalledWith({ + userId: '0123456789', + userExtId: 'SAMPLE_TEACHERID', + channel: 'tn', + action: 'accept', + feedId: '0123456789' + }); + expect(mockTelemetryGeneratorService.generateInteractTelemetry).toHaveBeenCalledWith(InteractType.TOUCH, + '', + Environment.HOME, + PageId.EXTERNAL_USER_VERIFICATION_POPUP, + undefined, + undefined, + undefined, + featureIdMap.userVerification.EXTERNAL_USER_VERIFICATION, + ID.USER_VERIFICATION_SUBMITED); + }); + }); + + describe('externalUserVerfication()', () => { + it('should generate INTERACT telemetry for successsfull validation', (done) => { + // arrange + mockProfileService.userMigrate = jest.fn(() => of(userMigrationResponse)); + // act + teacherIdVerificationComponent.externalUserVerfication({ + userId: '0123456789', + userExtId: 'SAMPLE_TEACHERID', + channel: 'tn', + action: 'accept', + feedId: '0123456789' + }); + // assert + setTimeout(() => { + expect(teacherIdVerificationComponent.teacherIdFlag).toEqual('verifiedStateId'); + expect(mockTelemetryGeneratorService.generateInteractTelemetry).toHaveBeenCalledWith(InteractType.OTHER, + '', + Environment.USER, + PageId.EXTERNAL_USER_VERIFICATION_POPUP, + undefined, + undefined, + undefined, + featureIdMap.userVerification.EXTERNAL_USER_VERIFICATION, + ID.USER_VERIFICATION_SUCCESS); + done(); + }, 0); + }); + + it('should set the error flag in case of invalid id', (done) => { + // arrange + const userMigrationInvalidIdResponse = { responseCode: 'invaliduserexternalid' } as any; + mockProfileService.userMigrate = jest.fn(() => of(userMigrationInvalidIdResponse)); + // act + teacherIdVerificationComponent.externalUserVerfication({ + userId: '0123456789', + userExtId: 'SAMPLE_TEACHERID', + channel: 'tn', + action: 'accept', + feedId: '0123456789' + }); + // assert + setTimeout(() => { + expect(teacherIdVerificationComponent.teacherModelId).toEqual(''); + expect(teacherIdVerificationComponent.showTeacherIdIncorrectErr).toBeTruthy(); + done(); + }, 0); + }); + + it('should generate failed verification INTERACT telemetry and set teacherIdFlag for 400 error code', (done) => { + // arrange + const sunbirdResponse = new Response(); + sunbirdResponse.responseCode = 400; + sunbirdResponse.body = {}; + mockProfileService.userMigrate = jest.fn(() => throwError(new HttpClientError('', sunbirdResponse))); + // act + teacherIdVerificationComponent.externalUserVerfication({}); + // assert + setTimeout(() => { + expect(teacherIdVerificationComponent.teacherIdFlag).toEqual('failedStateId'); + expect(mockTelemetryGeneratorService.generateInteractTelemetry).toHaveBeenCalledWith(InteractType.OTHER, + '', + Environment.HOME, + PageId.EXTERNAL_USER_VERIFICATION_POPUP, + undefined, + undefined, + undefined, + featureIdMap.userVerification.EXTERNAL_USER_VERIFICATION, + ID.USER_VERIFICATION_FAILED); + done(); + }, 0); + }); + + it('should generate failed verification INTERACT telemetry and set teacherIdFlag for 404 error code', (done) => { + // arrange + const sunbirdResponse = new Response(); + sunbirdResponse.responseCode = 404; + sunbirdResponse.body = {}; + mockProfileService.userMigrate = jest.fn(() => throwError(new HttpClientError('', sunbirdResponse))); + // act + teacherIdVerificationComponent.externalUserVerfication({}); + // assert + setTimeout(() => { + expect(teacherIdVerificationComponent.teacherIdFlag).toEqual('failedStateId'); + expect(mockTelemetryGeneratorService.generateInteractTelemetry).toHaveBeenCalledWith(InteractType.OTHER, + '', + Environment.HOME, + PageId.EXTERNAL_USER_VERIFICATION_POPUP, + undefined, + undefined, + undefined, + featureIdMap.userVerification.EXTERNAL_USER_VERIFICATION, + ID.USER_VERIFICATION_FAILED); + done(); + }, 0); + }); + + it('should cloe popup and show USER_IS_NOT_VERIFIED toast for 429 error code', (done) => { + // arrange + const sunbirdResponse = new Response(); + sunbirdResponse.responseCode = 429; + sunbirdResponse.body = {}; + mockProfileService.userMigrate = jest.fn(() => throwError(new HttpClientError('', sunbirdResponse))); + jest.spyOn(teacherIdVerificationComponent, 'closePopup'); + // act + teacherIdVerificationComponent.externalUserVerfication({}); + // assert + setTimeout(() => { + expect(mockCommonUtilService.showToast).toHaveBeenCalledWith('USER_IS_NOT_VERIFIED'); + expect(teacherIdVerificationComponent.closePopup).toHaveBeenCalled(); + done(); + }, 0); + }); + + it('should cloe popup and show USER_IS_NOT_VERIFIED toast for 401 error code', (done) => { + // arrange + const sunbirdResponse = new Response(); + sunbirdResponse.responseCode = 401; + sunbirdResponse.body = {}; + mockProfileService.userMigrate = jest.fn(() => throwError(new HttpClientError('', sunbirdResponse))); + jest.spyOn(teacherIdVerificationComponent, 'closePopup'); + // act + teacherIdVerificationComponent.externalUserVerfication({}); + // assert + setTimeout(() => { + expect(mockCommonUtilService.showToast).toHaveBeenCalledWith('USER_IS_NOT_VERIFIED'); + expect(teacherIdVerificationComponent.closePopup).toHaveBeenCalled(); + done(); + }, 0); + }); + + it('should cloe popup and show USER_IS_NOT_VERIFIED toast for othe error code', (done) => { + // arrange + const sunbirdResponse = new Response(); + sunbirdResponse.responseCode = 500; + sunbirdResponse.body = {}; + mockProfileService.userMigrate = jest.fn(() => throwError(new HttpClientError('', sunbirdResponse))); + jest.spyOn(teacherIdVerificationComponent, 'closePopup'); + // act + teacherIdVerificationComponent.externalUserVerfication({}); + // assert + setTimeout(() => { + expect(mockCommonUtilService.showToast).toHaveBeenCalledWith('USER_IS_NOT_VERIFIED'); + expect(teacherIdVerificationComponent.closePopup).toHaveBeenCalled(); + done(); + }, 0); + }); + }); +}); diff --git a/src/app/components/popups/upgrade-popover/upgrade-popover.component.spec.ts b/src/app/components/popups/upgrade-popover/upgrade-popover.component.spec.ts new file mode 100644 index 0000000000..0fe6d0194a --- /dev/null +++ b/src/app/components/popups/upgrade-popover/upgrade-popover.component.spec.ts @@ -0,0 +1,75 @@ +import { UpgradePopoverComponent } from './upgrade-popover.component'; +import { PopoverController, Platform, NavParams } from '@ionic/angular'; +import { UtilityService } from '../../../../services/utility-service'; +import { NgZone } from '@angular/core'; +import { of } from 'rxjs'; + +describe('UpgradePopoverComponent', () => { + let upgradePopoverComponent: UpgradePopoverComponent; + const mockUtilityService: Partial = { + openPlayStore: jest.fn() + }; + + const mockPopOverController: Partial = { + dismiss: jest.fn() + }; + + const mockNavParams: Partial = { + get: jest.fn((arg) => { + let value; + switch (arg) { + case 'type': + value = { + type: 'force', + optional: 'forceful', + title: 'We recommend that you upgrade to the latest version of Sunbird.', + desc: '', + actionButtons: [ + { + action: 'yes', + label: 'Update Now', + link: 'https://play.google.com/store/apps/details?id=org.sunbird.app&hl=en' + } + ] + }; + break; + } + return value; + }) + }; + + beforeAll(() => { + upgradePopoverComponent = new UpgradePopoverComponent( + mockUtilityService as UtilityService, + mockPopOverController as PopoverController, + mockNavParams as NavParams + ); + }); + + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('should create a instance of UpgradePopoverComponent', () => { + expect(upgradePopoverComponent).toBeTruthy(); + }); + + it('should close popover', () => { + // arrange + // act + upgradePopoverComponent.cancel(); + // assert + expect(mockPopOverController.dismiss).toHaveBeenCalled(); + }); + + it('should invoke openPlayStore', () => { + // arrange + jest.spyOn(upgradePopoverComponent, 'cancel'); + // act + upgradePopoverComponent.upgrade('https://play.google.com/store/apps/details?id=org.sunbird.app'); + // assert + expect(mockUtilityService.openPlayStore).toHaveBeenCalledWith('org.sunbird.app'); + expect(upgradePopoverComponent.cancel).toHaveBeenCalled(); + }); + +}); diff --git a/src/app/components/popups/upgrade-popover/upgrade-popover.component.ts b/src/app/components/popups/upgrade-popover/upgrade-popover.component.ts index d3c0d5ed6c..c0a10a00f9 100644 --- a/src/app/components/popups/upgrade-popover/upgrade-popover.component.ts +++ b/src/app/components/popups/upgrade-popover/upgrade-popover.component.ts @@ -40,5 +40,4 @@ export class UpgradePopoverComponent { this.utilityService.openPlayStore(appId); this.cancel(); } - -} \ No newline at end of file +} diff --git a/src/app/components/popups/view-credits/view-credits.component.html b/src/app/components/popups/view-credits/view-credits.component.html index 826703b00a..7128fe5744 100644 --- a/src/app/components/popups/view-credits/view-credits.component.html +++ b/src/app/components/popups/view-credits/view-credits.component.html @@ -16,7 +16,7 @@ {{ 'CREATORS' | translate }} - {{mergeProperties('creators', 'creator')}} + {{mergeProperties(['creators', 'creator'])}}
@@ -24,7 +24,7 @@ {{ 'CONTRIBUTORS' | translate }} - {{mergeProperties('contributors', 'owner')}} + {{mergeProperties(['contributors', 'owner'])}}
diff --git a/src/app/components/popups/view-credits/view-credits.component.spec.ts b/src/app/components/popups/view-credits/view-credits.component.spec.ts new file mode 100644 index 0000000000..3545292e8c --- /dev/null +++ b/src/app/components/popups/view-credits/view-credits.component.spec.ts @@ -0,0 +1,114 @@ +import { ViewCreditsComponent } from './view-credits.component'; +import { PopoverController, Platform, NavParams } from '@ionic/angular'; +import { TelemetryGeneratorService } from '../../../../services/telemetry-generator.service'; +import { PageId, InteractSubtype, Environment, InteractType } from '@app/services/telemetry-constants'; + +describe('UpgradePopoverComponent', () => { + let viewCreditsComponent: ViewCreditsComponent; + + const mockNavParams: Partial = { + get: jest.fn((arg) => { + let value; + switch (arg) { + case 'content': + value = { + identifier: 'do_123', + pkgVersion: '1', + contentType: 'Resource', + creator: 'SAMPLE_CREATOR', + creators: 'SAMPLE_CREATORS' + }; + break; + case 'pageId': + value = PageId.CONTENT_DETAIL; + break; + case 'rollUp': + value = { l1: 'do_1', l2: 'do_2'}; + break; + } + return value; + }) + }; + + const mockPlatform: Partial = { + }; + + const subscribeWithPriorityData = jest.fn((_, fn) => fn()); + mockPlatform.backButton = { + subscribeWithPriority: subscribeWithPriorityData + } as any; + + const mockPopOverController: Partial = { + dismiss: jest.fn() + }; + + const mockTelemetryGeneratorService: Partial = { + generateInteractTelemetry: jest.fn() + }; + + + beforeAll(() => { + viewCreditsComponent = new ViewCreditsComponent( + mockNavParams as NavParams, + mockPlatform as Platform, + mockPopOverController as PopoverController, + mockTelemetryGeneratorService as TelemetryGeneratorService + ); + }); + + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('should create a instance of ViewCreditsComponent', () => { + expect(viewCreditsComponent).toBeTruthy(); + }); + + it('should dismiss the popup and unsubscribe back Function', () => { + // arrange + viewCreditsComponent.backButtonFunc = { + unsubscribe: jest.fn() + }; + // act + viewCreditsComponent.ngOnInit(); + // assert + expect(mockPopOverController.dismiss).toHaveBeenCalled(); + // expect(viewCreditsComponent.backButtonFunc.unsubscribe).toHaveBeenCalled(); + + }); + + it('should generate INTERACT telemetry on ionViewDidload', () => { + // arrange + // act + viewCreditsComponent.ionViewDidLoad(); + // assert + expect(mockTelemetryGeneratorService.generateInteractTelemetry).toHaveBeenCalledWith(InteractType.TOUCH, + InteractSubtype.CREDITS_CLICKED, + Environment.HOME, + PageId.CONTENT_DETAIL, + { id: 'do_123', type: 'Resource', version: '1'}, + undefined, + {l1: 'do_1', l2: 'do_2'}, + undefined); + }); + + it('should dissmiss popup on cancel', () => { + // arrange + viewCreditsComponent.backButtonFunc = { + unsubscribe: jest.fn() + }; + // act + viewCreditsComponent.cancel(); + // assert + expect(mockPopOverController.dismiss).toHaveBeenCalled(); + expect(viewCreditsComponent.backButtonFunc.unsubscribe).toHaveBeenCalled(); + }); + + it('should return merged properties ', () => { + // arrange + // act + // assert + expect(viewCreditsComponent.mergeProperties(['creator', 'creators'])).toEqual('SAMPLE_CREATOR, SAMPLE_CREATORS'); + }); + +}); diff --git a/src/app/components/popups/view-credits/view-credits.component.ts b/src/app/components/popups/view-credits/view-credits.component.ts index a05b08ca8d..568b008625 100644 --- a/src/app/components/popups/view-credits/view-credits.component.ts +++ b/src/app/components/popups/view-credits/view-credits.component.ts @@ -1,67 +1,50 @@ -import { Component, NgZone } from '@angular/core'; +import { Component, NgZone, OnInit } from '@angular/core'; import { NavParams, Platform, PopoverController } from '@ionic/angular'; import { TelemetryObject } from 'sunbird-sdk'; import { ProfileConstants } from '@app/app/app.constant'; import { AppGlobalService } from '@app/services/app-global-service.service'; import { TelemetryGeneratorService } from '@app/services/telemetry-generator.service'; -import { Environment, InteractType } from '@app/services/telemetry-constants'; +import { Environment, InteractType, InteractSubtype } from '@app/services/telemetry-constants'; +import { ContentUtil } from '@app/util/content-util'; @Component({ selector: 'app-view-credits', templateUrl: './view-credits.component.html', styleUrls: ['./view-credits.component.scss'], }) -export class ViewCreditsComponent { +export class ViewCreditsComponent implements OnInit { userId = ''; backButtonFunc = undefined; content: any; rollUp: any; correlation: any; - private pageId = ''; - private popupType: string; + pageId = ''; - /** - * Default function of class ViewCreditsComponent - * - * @param navParams - * @param viewCtrl - * @param platform - * @param ngZone - * @param telemetrygeneratorService - * @param appGlobalService - */ constructor( private navParams: NavParams, private platform: Platform, - private ngZone: NgZone, - private telemetrygeneratorService: TelemetryGeneratorService, - private appGlobalService: AppGlobalService, - private popOverCtrl: PopoverController - ) { - this.getUserId(); + private popOverCtrl: PopoverController, + private telemetrygeneratorService: TelemetryGeneratorService + ) {} + + ngOnInit(): void { this.backButtonFunc = this.platform.backButton.subscribeWithPriority(11, () => { this.popOverCtrl.dismiss(); this.backButtonFunc.unsubscribe(); }); - this.ngZone.run(() => { - this.popupType = this.navParams.get('popupType'); - }); } - /** - * Ionic life cycle hook - */ ionViewDidLoad(): void { this.content = this.navParams.get('content'); this.pageId = this.navParams.get('pageId'); this.rollUp = this.navParams.get('rollUp'); this.correlation = this.navParams.get('correlation'); - const telemetryObject = new TelemetryObject(this.content.identifier, this.content.contentType, this.content.pkgVersion); + const telemetryObject = ContentUtil.getTelemetryObject(this.content); this.telemetrygeneratorService.generateInteractTelemetry(InteractType.TOUCH, - 'credits-clicked', + InteractSubtype.CREDITS_CLICKED, Environment.HOME, this.pageId, telemetryObject, @@ -71,43 +54,13 @@ export class ViewCreditsComponent { ); } - /* SUDO - if firstprperty is there and secondprperty is not there, then return firstprperty value - else if firstprperty is not there and secondprperty is there, then return secondprperty value - else do the merger of firstprperty and secondprperty value and return merged value - */ - mergeProperties(firstProp, secondProp) { - if (this.content[firstProp] && !this.content[secondProp]) { - return this.content[firstProp]; - } else if (!this.content[firstProp] && this.content[secondProp]) { - return this.content[secondProp]; - } else { - let first: any; - let second: any; - first = this.content[firstProp].split(', '); - second = this.content[secondProp].split(', '); - first = second.concat(first); - first = Array.from(new Set(first)); - return first.join(', '); - } - } - - /** - * Get user id - */ - getUserId() { - if (this.appGlobalService.getSessionData()) { - this.userId = this.appGlobalService.getSessionData()[ - ProfileConstants.USER_TOKEN - ]; - } else { - this.userId = ''; - } + mergeProperties(mergeProp) { + return ContentUtil.mergeProperties(this.content, mergeProp); } cancel() { this.popOverCtrl.dismiss(); - if(this.backButtonFunc) { + if (this.backButtonFunc) { this.backButtonFunc.unsubscribe(); } } diff --git a/src/app/components/sign-in-card/sign-in-card.component.ts b/src/app/components/sign-in-card/sign-in-card.component.ts index e7bab23535..507156d805 100644 --- a/src/app/components/sign-in-card/sign-in-card.component.ts +++ b/src/app/components/sign-in-card/sign-in-card.component.ts @@ -126,6 +126,10 @@ export class SignInCardComponent implements OnInit { }) .then(async () => { await loader.dismiss(); + if (!this.appGlobalService.signinOnboardingLoader) { + this.appGlobalService.signinOnboardingLoader = await this.commonUtilService.getLoader(); + await this.appGlobalService.signinOnboardingLoader.present(); + } that.ngZone.run(() => { that.preferences.putString('SHOW_WELCOME_TOAST', 'true').toPromise().then(); diff --git a/src/app/district-mapping/district-mapping.page.spec.ts b/src/app/district-mapping/district-mapping.page.spec.ts index e1730beb50..122314241b 100644 --- a/src/app/district-mapping/district-mapping.page.spec.ts +++ b/src/app/district-mapping/district-mapping.page.spec.ts @@ -15,6 +15,7 @@ import { SharedPreferences } from '../../../../sunbird-mobile-sdk/src/util/share import { EMPTY, of, throwError } from 'rxjs'; import { LocationSearchResult } from '../../../../sunbird-mobile-sdk/src/profile/def/location-search-result'; import { ProfileService, Profile, ProfileType, ProfileSource, DeviceRegisterResponse } from 'sunbird-sdk'; +import { PreferenceKey } from '@app/app/app.constant'; describe('DistrictMappingPage', () => { let districtMappingPage: DistrictMappingPage; @@ -53,7 +54,8 @@ describe('DistrictMappingPage', () => { }; const mockAppGlobalService: Partial = { isUserLoggedIn: jest.fn(() => true), - getCurrentUser: jest.fn(() => profile) + getCurrentUser: jest.fn(() => profile), + closeSigninOnboardingLoader: jest.fn() }; const mockEvents: Partial = { publish: jest.fn() @@ -291,11 +293,41 @@ describe('DistrictMappingPage', () => { expect(mockCommonUtilService.showToast).toHaveBeenCalledWith('INTERNET_CONNECTIVITY_NEEDED'); }); + it('should invoke device register API and save it in the preference', (done) => { + // arrange + districtMappingPage.stateList = [{ type: 'state', name: 'Odisha', id: 'od_123' }]; + districtMappingPage.districtList = [{ type: 'district', name: 'Cuttack', id: 'ct_123' }]; + districtMappingPage.stateName = 'Odisha'; + districtMappingPage.districtName = 'Cuttack'; + mockCommonUtilService.networkInfo = { isNetworkAvailable: true }; + const req = { + userDeclaredLocation: { + state: 'Odisha', + stateId: 'od_123', + district: 'Cuttack', + districtId: 'ct_123', + declaredOffline: false + } + }; + + // act + districtMappingPage.saveDeviceLocation(); + // assert + setTimeout( () => { + expect(mockDeviceRegisterService.registerDevice).toHaveBeenCalledWith(req); + expect(mockPreferences.putString).toHaveBeenCalledWith(PreferenceKey.DEVICE_LOCATION, JSON.stringify(req.userDeclaredLocation)); + expect(mockCommonUtilService.getLoader().dismiss).toHaveBeenCalled(); + done(); + }, 1); + }); + it('should invoke updateServerProfile when submit clicked', (done) => { // arrange window.history.replaceState({ profile }, 'MOCK'); mockCommonUtilService.networkInfo = { isNetworkAvailable: true }; districtMappingPage.name = 'sample_name'; + mockCommonUtilService.isDeviceLocationAvailable = jest.fn(() => Promise.resolve(false)); + jest.spyOn(districtMappingPage, 'saveDeviceLocation').mockImplementation(); // act districtMappingPage.submit(); // assert @@ -381,11 +413,12 @@ describe('DistrictMappingPage', () => { // arrange window.history.replaceState({ source: 'guest-profile' }, 'MOCK'); mockAppGlobalService.isUserLoggedIn = jest.fn(() => false); + jest.spyOn(districtMappingPage, 'saveDeviceLocation').mockImplementation(); // act districtMappingPage.submit(); // assert setTimeout(() => { - expect(mockDeviceRegisterService.registerDevice).toHaveBeenCalled(); + expect(districtMappingPage.saveDeviceLocation).toHaveBeenCalled(); expect(mockLocation.back).toHaveBeenCalled(); expect(mockEvents.publish).toHaveBeenCalledWith('refresh:profile'); done(); @@ -398,11 +431,13 @@ describe('DistrictMappingPage', () => { // arrange window.history.replaceState({ source: 'profile-setting' }, 'MOCK'); mockAppGlobalService.isUserLoggedIn = jest.fn(() => false); + mockCommonUtilService.isDeviceLocationAvailable = jest.fn(() => Promise.resolve(false)); + jest.spyOn(districtMappingPage, 'saveDeviceLocation').mockImplementation(); // act districtMappingPage.submit(); // assert setTimeout(() => { - expect(mockDeviceRegisterService.registerDevice).toHaveBeenCalled(); + expect(districtMappingPage.saveDeviceLocation).toHaveBeenCalled(); expect(mockRouter.navigate).toHaveBeenCalledWith(['/tabs'], { state: { loginMode: 'guest' diff --git a/src/app/district-mapping/district-mapping.page.ts b/src/app/district-mapping/district-mapping.page.ts index e624f0f363..49890920be 100644 --- a/src/app/district-mapping/district-mapping.page.ts +++ b/src/app/district-mapping/district-mapping.page.ts @@ -1,7 +1,7 @@ import {Component, OnInit, Inject, ChangeDetectorRef, NgZone, ViewChild} from '@angular/core'; import { LocationSearchCriteria, ProfileService, - SharedPreferences, Profile, DeviceRegisterRequest, DeviceRegisterService, DeviceInfo + SharedPreferences, Profile, DeviceRegisterRequest, DeviceRegisterService, DeviceInfo, LocationSearchResult } from 'sunbird-sdk'; import { Location as loc, PreferenceKey, RouterLinks, LocationConfig } from '../../app/app.constant'; import { AppHeaderService, CommonUtilService, AppGlobalService, FormAndFrameworkUtilService } from '@app/services'; @@ -104,8 +104,8 @@ export class DistrictMappingPage { } name; - stateList = []; - districtList = []; + stateList: LocationSearchResult[] = []; + districtList: LocationSearchResult[] = []; stateCode; districtCode; backButtonFunc: Subscription; @@ -137,6 +137,7 @@ export class DistrictMappingPage { private ngZone: NgZone, private externalIdVerificationService: ExternalIdVerificationService ) { + this.appGlobalService.closeSigninOnboardingLoader(); this.isKeyboardShown$ = deviceInfo.isKeyboardShown().pipe( tap(() => this.changeDetectionRef.detectChanges()) ); @@ -435,15 +436,14 @@ export class DistrictMappingPage { const req: DeviceRegisterRequest = { userDeclaredLocation: { state: this.stateName, + stateId: this.stateList.find((s) => s.name === this.stateName).id, district: this.districtName, + districtId: this.districtList.find((d) => d.name === this.districtName).id, + declaredOffline: !this.commonUtilService.networkInfo.isNetworkAvailable } }; this.deviceRegisterService.registerDevice(req).toPromise(); - - const locationMap = new Map(); - locationMap['state'] = this.stateName ? this.stateName : this.availableLocationState; - locationMap['district'] = this.districtName ? this.districtName : this.availableLocationDistrict; - await this.preferences.putString(PreferenceKey.DEVICE_LOCATION, JSON.stringify(locationMap)).toPromise(); + this.preferences.putString(PreferenceKey.DEVICE_LOCATION, JSON.stringify(req.userDeclaredLocation)).toPromise(); await loader.dismiss(); } diff --git a/src/app/profile/categories-edit/categories-edit.page.ts b/src/app/profile/categories-edit/categories-edit.page.ts index 3a023a0eb4..fe5e3a174e 100644 --- a/src/app/profile/categories-edit/categories-edit.page.ts +++ b/src/app/profile/categories-edit/categories-edit.page.ts @@ -143,6 +143,7 @@ export class CategoriesEditPage { private tncUpdateHandlerService: TncUpdateHandlerService, ) { + this.appGlobalService.closeSigninOnboardingLoader(); this.profile = this.appGlobalService.getCurrentUser(); const extrasState = this.router.getCurrentNavigation().extras.state; if (extrasState && extrasState.showOnlyMandatoryFields) { diff --git a/src/app/tabs/tabs.page.html b/src/app/tabs/tabs.page.html index 6e19dc0bdd..6dfc76726a 100644 --- a/src/app/tabs/tabs.page.html +++ b/src/app/tabs/tabs.page.html @@ -1,5 +1,5 @@ - + diff --git a/src/app/tabs/tabs.page.scss b/src/app/tabs/tabs.page.scss index a9ea93c1d2..7d13e72d6f 100644 --- a/src/app/tabs/tabs.page.scss +++ b/src/app/tabs/tabs.page.scss @@ -4,7 +4,11 @@ ion-tabs { ion-tab-button { } - + .rm-before{ + &::before { + position: relative !important; + } + } ion-tab-bar { align-items: flex-end; contain: none; diff --git a/src/app/tabs/tabs.page.ts b/src/app/tabs/tabs.page.ts index bf36f2bde6..c6b136cd35 100644 --- a/src/app/tabs/tabs.page.ts +++ b/src/app/tabs/tabs.page.ts @@ -31,6 +31,7 @@ export class TabsPage implements OnInit, AfterViewInit { }; selectedLanguage: string; appLabel: any; + olderWebView = false; constructor( private container: ContainerService, @@ -49,7 +50,7 @@ export class TabsPage implements OnInit, AfterViewInit { async ngOnInit() { console.log('Inside tabsPage'); - + this.checkAndroidWebViewVersion(); const session = await this.appGlobalService.authService.getSession().toPromise(); if (!session) { console.log(`Success Platform Session`, session); @@ -111,6 +112,17 @@ export class TabsPage implements OnInit, AfterViewInit { } + checkAndroidWebViewVersion() { + var that = this; + plugins['webViewChecker'].getCurrentWebViewPackageInfo() + .then(function(packageInfo) { + if (parseInt(packageInfo.versionName.split('.')[0], 10) <= 68) { + that.olderWebView = true; + } + }) + .catch(function(error) { }); + } + ionViewWillEnter() { this.tabs = this.container.getAllTabs(); this.events.publish('update_header'); diff --git a/src/app/terms-and-conditions/terms-and-conditions.page.ts b/src/app/terms-and-conditions/terms-and-conditions.page.ts index 8a21737420..3d06df7b26 100644 --- a/src/app/terms-and-conditions/terms-and-conditions.page.ts +++ b/src/app/terms-and-conditions/terms-and-conditions.page.ts @@ -9,9 +9,8 @@ import { LogoutHandlerService } from '@app/services/handlers/logout-handler.serv import { TncUpdateHandlerService } from '@app/services/handlers/tnc-update-handler.service'; import { CommonUtilService } from '@app/services/common-util.service'; import { TelemetryGeneratorService } from '@app/services/telemetry-generator.service'; -import { AppHeaderService } from '@app/services/app-header.service'; import { ProfileConstants, RouterLinks } from '../app.constant'; -import { FormAndFrameworkUtilService } from '@app/services'; +import { FormAndFrameworkUtilService, AppGlobalService } from '@app/services'; import { Router, NavigationExtras } from '@angular/router'; import { SplashScreenService } from '@app/services/splash-screen.service'; import { ExternalIdVerificationService } from '@app/services/externalid-verification.service'; @@ -37,19 +36,19 @@ export class TermsAndConditionsPage implements OnInit { private sanitizer: DomSanitizer, private commonUtilService: CommonUtilService, private telemetryGeneratorService: TelemetryGeneratorService, - private headerService: AppHeaderService, private appVersion: AppVersion, private injector: Injector, private formAndFrameworkUtilService: FormAndFrameworkUtilService, private router: Router, private splashScreenService: SplashScreenService, - private externalIdVerificationService: ExternalIdVerificationService + private externalIdVerificationService: ExternalIdVerificationService, + private appGlobalService: AppGlobalService ) { } public async ngOnInit() { + this.appGlobalService.closeSigninOnboardingLoader(); this.appName = await this.appVersion.getAppName(); - this.headerService.hideHeader(); this.userProfileDetails = (await this.profileService.getActiveSessionProfile( { requiredFields: ProfileConstants.REQUIRED_FIELDS }).toPromise()).serverProfile; @@ -134,15 +133,21 @@ export class TermsAndConditionsPage implements OnInit { await loader.dismiss(); loader = undefined; } + if (!this.appGlobalService.signinOnboardingLoader) { + this.appGlobalService.signinOnboardingLoader = await this.commonUtilService.getLoader(); + await this.appGlobalService.signinOnboardingLoader.present(); + } this.disableSubmitButton = false; if (value['status']) { if (this.commonUtilService.isUserLocationAvalable(serverProfile) - && await tncUpdateHandlerService.isSSOUser(profile)) { + || await tncUpdateHandlerService.isSSOUser(profile)) { await tncUpdateHandlerService.dismissTncPage(); + this.appGlobalService.closeSigninOnboardingLoader(); this.router.navigate(['/', RouterLinks.TABS]); this.externalIdVerificationService.showExternalIdVerificationPopup(); this.splashScreenService.handleSunbirdSplashScreenActions(); } else { + // closeSigninOnboardingLoader() is called in District-Mapping page const navigationExtras: NavigationExtras = { state: { isShowBackButton: false @@ -151,6 +156,7 @@ export class TermsAndConditionsPage implements OnInit { this.router.navigate(['/', RouterLinks.DISTRICT_MAPPING] , navigationExtras); } } else { + // closeSigninOnboardingLoader() is called in CategoryEdit page await tncUpdateHandlerService.dismissTncPage(); this.router.navigate([`/${RouterLinks.PROFILE}/${RouterLinks.CATEGORIES_EDIT}`], { state: { diff --git a/src/assets/i18n/en.json b/src/assets/i18n/en.json index a4405e3ca0..2b0f35cdb6 100644 --- a/src/assets/i18n/en.json +++ b/src/assets/i18n/en.json @@ -649,5 +649,8 @@ "DOWNLOAD_FILE_INFO": "Download the file to enable the disabled options", "FILE_SAVED": "File saved in Downloads", "DOWNLOAD_PATH": "Internal storage/Download", - "DOWNLOAD_FILE_SIZE": "File Size {{%s}}" + "DOWNLOAD_FILE_SIZE": "File Size {{%s}}", + "UPDATE_REQUIRED": "Update Required", + "DOWNLOAD_NOW": "Download Now", + "WEBVIEW_UPDATE_TEXT": "For an effective experience, download the latest version of webview from Google Play store." } diff --git a/src/pipes/mime-type/mime-type.ts b/src/pipes/mime-type/mime-type.ts index 97b673742a..03eef6bdae 100644 --- a/src/pipes/mime-type/mime-type.ts +++ b/src/pipes/mime-type/mime-type.ts @@ -1,11 +1,9 @@ import { MimeType } from './../../app/app.constant'; -import { Content, ContentData } from 'sunbird-sdk'; +import { Content } from 'sunbird-sdk'; import { Pipe, PipeTransform } from '@angular/core'; -​ /* Contents are filtered based on given mimetype */ -​ @Pipe({ name: 'hasMimeType', }) @@ -29,14 +27,13 @@ export class MimeTypePipe implements PipeTransform { return this.getFilteredItems(item.children, mimeTypes); } } -​ -​ + getFilteredItems(contents: Content[] = [], mimeTypes: string[]): boolean { const t = this.flattenDeep(contents) .some((c) => !!mimeTypes.find(m => m === c.contentData.mimeType)); return t; } -​ + private flattenDeep(contents: Content[]): Content[] { return contents.reduce((acc, val) => { if (val.children) { diff --git a/src/services/app-global-service.service.spec.ts b/src/services/app-global-service.service.spec.ts index dd6286ab00..fe57cf9719 100644 --- a/src/services/app-global-service.service.spec.ts +++ b/src/services/app-global-service.service.spec.ts @@ -7,13 +7,17 @@ import { of } from 'rxjs'; import { PreferenceKey } from '../app/app.constant'; describe('AppGlobalService', () => { let appGlobalService: AppGlobalService; - const mockProfile: Partial = {}; + const profile = { syllabus: 'tn'} as any; + const mockProfile: Partial = { + getActiveSessionProfile: jest.fn(() => of(profile)) + }; const mockAuthService: Partial = { getSession: jest.fn(() => of()) }; const mockFrameworkService: Partial = {}; const mockEvent: Partial = { - subscribe: jest.fn() + subscribe: jest.fn(), + publish: jest.fn() }; const mockPopoverCtrl: Partial = {}; const mockTelemetryGeneratorService: Partial = {}; @@ -118,4 +122,30 @@ describe('AppGlobalService', () => { // assert expect(appGlobalService.limitedShareQuizContent).toBeNull(); }); + + it('should set the signin Onboarding loader', () => { + // arrange + appGlobalService.signinOnboardingLoader = { + dismiss: jest.fn((fn) => fn()) + }; + // act + // assert + expect(appGlobalService.signinOnboardingLoader).toBeDefined(); + }); + + it('should dismiss the signin Onboarding loader', (done) => { + // arrange + appGlobalService.signinOnboardingLoader = { + dismiss: jest.fn(() => Promise.resolve()) + }; + // act + appGlobalService.closeSigninOnboardingLoader(); + // assert + expect(appGlobalService.signinOnboardingLoader.dismiss).toHaveBeenCalled(); + + setTimeout(() => { + expect(appGlobalService.signinOnboardingLoader).toBeNull(); + done(); + }, 1); + }); }); diff --git a/src/services/app-global-service.service.ts b/src/services/app-global-service.service.ts index fce42cbffc..2bf273b35d 100644 --- a/src/services/app-global-service.service.ts +++ b/src/services/app-global-service.service.ts @@ -71,6 +71,7 @@ export class AppGlobalService implements OnDestroy { private _limitedShareQuizContent: any; private _isSignInOnboardingCompleted: any; private isJoinTraningOnboarding: any; + private _signinOnboardingLoader: any; constructor( @@ -677,10 +678,24 @@ export class AppGlobalService implements OnDestroy { this.isJoinTraningOnboarding = value; } + get signinOnboardingLoader() { + return this._signinOnboardingLoader; + } + set signinOnboardingLoader(value) { + this._signinOnboardingLoader = value; + } + // This method is used to reset if any quiz content data is previously saved before Joining a Training // So it wont affect in the exterId verification page resetSavedQuizContent() { this.limitedShareQuizContent = null; } + async closeSigninOnboardingLoader() { + if (this.signinOnboardingLoader) { + await this.signinOnboardingLoader.dismiss(); + this.signinOnboardingLoader = null; + } + } + } diff --git a/src/services/content/content-info.ts b/src/services/content/content-info.ts index 5b5ec88cbb..423220b123 100644 --- a/src/services/content/content-info.ts +++ b/src/services/content/content-info.ts @@ -5,5 +5,5 @@ export interface ContentInfo { rollUp: Rollup; correlationList: CorrelationData[]; hierachyInfo: HierarchyInfo[]; - course?:Course + course?: Course; } diff --git a/src/services/externalid-verification.service.ts b/src/services/externalid-verification.service.ts index 8317c09e96..e03ea247c5 100644 --- a/src/services/externalid-verification.service.ts +++ b/src/services/externalid-verification.service.ts @@ -28,6 +28,7 @@ export class ExternalIdVerificationService { } async showExternalIdVerificationPopup() { + this.appGlobalService.closeSigninOnboardingLoader(); if (await this.checkQuizContent()) { return; } diff --git a/src/services/formandframeworkutil.service.spec.ts b/src/services/formandframeworkutil.service.spec.ts new file mode 100644 index 0000000000..62e8dd6a6c --- /dev/null +++ b/src/services/formandframeworkutil.service.spec.ts @@ -0,0 +1,93 @@ +import { FormAndFrameworkUtilService } from './formandframeworkutil.service'; +import { ProfileService, SystemSettingsService, FrameworkUtilService, FormService, FrameworkService, SharedPreferences } from 'sunbird-sdk'; +import { AppGlobalService } from './app-global-service.service'; +import { AppVersion } from '@ionic-native/app-version/ngx'; +import { TranslateService } from '@ngx-translate/core'; +import { Events } from '@ionic/angular'; +import { of, throwError } from 'rxjs'; + + +describe('FormAndFrameworkUtilService', () => { + let formAndFrameworkUtilService: FormAndFrameworkUtilService; + + const mockProfileService: Partial = {}; + const mockSystemSettingsService: Partial = {}; + const mockFrameworkUtilService: Partial = {}; + const mockFormAndFrameworkUtilService: Partial = {}; + const mockFormService: Partial = {}; + const mockFrameworkService: Partial = {}; + const mockSharedPreferences: Partial = {}; + const mockAppGlobalService: Partial = {}; + const mockAppVersion: Partial = {}; + const mockTranslateService: Partial = {}; + const mockEvents: Partial = {}; + + beforeAll(() => { + formAndFrameworkUtilService = new FormAndFrameworkUtilService( + mockProfileService as ProfileService, + mockSystemSettingsService as SystemSettingsService, + mockFrameworkUtilService as FrameworkUtilService, + mockFormService as FormService, + mockFrameworkService as FrameworkService, + mockSharedPreferences as SharedPreferences, + mockAppGlobalService as AppGlobalService, + mockAppVersion as AppVersion, + mockTranslateService as TranslateService, + mockEvents as Events + ); + }); + + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('should create an instance of FormAndFrameworkUtilService', () => { + expect(formAndFrameworkUtilService).toBeTruthy(); + }); + + describe('getWebviewConfig()', () => { + + it('should return the webview version', () => { + // arrange + const formResponse = { + form: { + type: 'config', + subtype: 'webview_version', + action: 'get', + data: { + action: 'get', + fields: [ + { + version: '54' + } + ] + } + } + }; + mockFormService.getForm = jest.fn(() => of(formResponse)); + // act + // assert + expect(formAndFrameworkUtilService.getWebviewConfig()).resolves.toBe(54); + }); + }); + + it('should return the webview version if value is not set', () => { + // arrange + const formResponse = { + form: '' + }; + mockFormService.getForm = jest.fn(() => of(formResponse)); + // act + // assert + expect(formAndFrameworkUtilService.getWebviewConfig()).resolves.toBe(54); + }); + + it('should reject the error if API throws some error', () => { + // arrange + mockFormService.getForm = jest.fn(() => throwError({error: 'API_ERROR'})); + // act + // assert + expect(formAndFrameworkUtilService.getWebviewConfig()).rejects.toBe({error: 'API_ERROR'}); + }); + +}); diff --git a/src/services/formandframeworkutil.service.ts b/src/services/formandframeworkutil.service.ts index 568f8caa42..138c525d97 100644 --- a/src/services/formandframeworkutil.service.ts +++ b/src/services/formandframeworkutil.service.ts @@ -601,4 +601,26 @@ export class FormAndFrameworkUtilService { }); }); } + + // get the required webview version + getWebviewConfig() { + return new Promise((resolve, reject) => { + const req: FormRequest = { + type: 'config', + subType: 'webview_version', + action: 'get', + }; + // form api call + this.formService.getForm(req).toPromise() + .then((res: any) => { + if (res.form && res.form.data && res.form.data.fields[0].version) { + resolve(parseInt(res.form.data.fields[0].version, 10)); + } else { + resolve(54); + } + }).catch((error: any) => { + reject(error); + }); + }); + } } diff --git a/src/services/handlers/tnc-update-handler.service.ts b/src/services/handlers/tnc-update-handler.service.ts index 5c8b4f3d66..5e62f6ef82 100644 --- a/src/services/handlers/tnc-update-handler.service.ts +++ b/src/services/handlers/tnc-update-handler.service.ts @@ -49,6 +49,8 @@ export class TncUpdateHandlerService { return; } this.presentTncPage({ profile }); + }).catch(e => { + this.appGlobalService.closeSigninOnboardingLoader(); }); } @@ -130,6 +132,7 @@ export class TncUpdateHandlerService { } }) .catch(() => { + this.appGlobalService.closeSigninOnboardingLoader(); this.externalIdVerificationService.showExternalIdVerificationPopup(); }); } diff --git a/src/services/login-handler.service.ts b/src/services/login-handler.service.ts index 7b4cce3033..fde867a685 100644 --- a/src/services/login-handler.service.ts +++ b/src/services/login-handler.service.ts @@ -30,6 +30,7 @@ import { } from '@app/services/telemetry-constants'; import { ContainerService } from '@app/services/container.services'; import { Router } from '@angular/router'; +import { AppGlobalService } from './app-global-service.service'; @Injectable() export class LoginHandlerService { @@ -54,7 +55,8 @@ export class LoginHandlerService { private formAndFrameworkUtilService: FormAndFrameworkUtilService, private telemetryGeneratorService: TelemetryGeneratorService, private router: Router, - private events: Events + private events: Events, + private appGlobalService: AppGlobalService ) { this.appVersion.getAppName() @@ -112,6 +114,10 @@ export class LoginHandlerService { }) .then(async () => { await loader.dismiss(); + if (!this.appGlobalService.signinOnboardingLoader) { + this.appGlobalService.signinOnboardingLoader = await this.commonUtilService.getLoader(); + await this.appGlobalService.signinOnboardingLoader.present(); + } that.ngZone.run(() => { that.preferences.putString('SHOW_WELCOME_TOAST', 'true').toPromise().then(); // this.events.publish('UPDATE_TABS'); diff --git a/src/services/telemetry-constants.ts b/src/services/telemetry-constants.ts index 522335da81..92104c29f9 100644 --- a/src/services/telemetry-constants.ts +++ b/src/services/telemetry-constants.ts @@ -117,7 +117,8 @@ export enum PageId { DISTRICT_MAPPING = 'district-mapping', SIGNIN_POPUP = 'signin-popup', EXTERNAL_USER_VERIFICATION_POPUP = 'user-verification-popup', - FAQ_REPORT_ISSUE = 'faq-report-issue' + FAQ_REPORT_ISSUE = 'faq-report-issue', + UPDATE_WEBVIEW_POPUP = 'update-webview-popup', } export enum LogType { NOTIFICATION = 'notification' @@ -377,7 +378,9 @@ export enum InteractSubtype { REPORT_ISSUE_CLICKED = 'report-issue-clicked', STATE_DIST_CHANGED = 'state-dist-changed', STATE_CHANGED = 'state-changed', - DIST_CHANGED = 'dist-changed' + DIST_CHANGED = 'dist-changed', + UPDATE_WEBVIEW_CLICKED = 'update-webview-clicked', + CREDITS_CLICKED = 'credits-clicked' } export enum ID { diff --git a/src/util/content-util.ts b/src/util/content-util.ts index 56baf1960c..cd98fa8cd3 100644 --- a/src/util/content-util.ts +++ b/src/util/content-util.ts @@ -4,9 +4,9 @@ export class ContentUtil { /** * Returns values from ContentData in a comma-separated string - * @param {ContentData} contentData - * @param {string[]} properties - * @returns {string} + * @param ContentData contentData + * @param string[] properties + * @returns string */ public static mergeProperties(contentData: ContentData, properties: string[]): string { @@ -36,11 +36,11 @@ export class ContentUtil { } } - /** + /** * Returns rollup - * @param {HierarchyInfo[]} hierarchyInfoList - * @param {string} identifier - * @returns {Rollup} + * @param HierarchyInfo[] hierarchyInfoList + * @param string identifier + * @returns Rollup */ public static generateRollUp(hierarchyInfoList, identifier): Rollup { const rollUp = new Rollup(); @@ -55,12 +55,12 @@ export class ContentUtil { return rollUp; } - /** + /** * Returns apt app icon - * @param {string} appIcon - * @param {string} basePath - * @param {boolean} isNetworkAvailable - * @returns {string} + * @param string appIcon + * @param string basePath + * @param boolean isNetworkAvailable + * @returns string */ public static getAppIcon(appIcon: string, basePath: string, isNetworkAvailable: boolean): string { if (appIcon) { @@ -91,10 +91,10 @@ export class ContentUtil { return pdf; } - /** + /** * Returns TelemetryObject - * @param {any} content - * @returns {TelemetryObject} + * @param any content + * @returns TelemetryObject */ public static getTelemetryObject(content): TelemetryObject { const identifier = content.identifier;