From 85ddf62e2eee92b8e7165560254cb8376d731916 Mon Sep 17 00:00:00 2001 From: Florent Letendre Date: Mon, 7 Oct 2024 09:45:54 -0400 Subject: [PATCH] test: add unit tests for opf onsite messaging (#19308) CXSPA-8511 --- .../opf-cta-scripts.service.spec.ts | 104 ++++++++- .../opf-cta-scripts.service.ts | 11 +- .../services/opf-dynamic-cta.service.spec.ts | 205 ++++++++++++++++++ .../services/opf-static-cta.service.spec.ts | 101 +++++++++ .../opf/cta/root/model/opf-cta.model.ts | 4 +- 5 files changed, 409 insertions(+), 16 deletions(-) create mode 100644 integration-libs/opf/cta/core/services/opf-dynamic-cta.service.spec.ts create mode 100644 integration-libs/opf/cta/core/services/opf-static-cta.service.spec.ts diff --git a/integration-libs/opf/cta/components/opf-cta-scripts/opf-cta-scripts.service.spec.ts b/integration-libs/opf/cta/components/opf-cta-scripts/opf-cta-scripts.service.spec.ts index 00fd42d5aba..11aae385a99 100644 --- a/integration-libs/opf/cta/components/opf-cta-scripts/opf-cta-scripts.service.spec.ts +++ b/integration-libs/opf/cta/components/opf-cta-scripts/opf-cta-scripts.service.spec.ts @@ -3,6 +3,7 @@ import { CmsService, Page, Product, QueryState } from '@spartacus/core'; import { ActiveConfiguration, OpfBaseFacade, + OpfDynamicScript, OpfPaymentProviderType, OpfResourceLoaderService, } from '@spartacus/opf/base/root'; @@ -29,6 +30,10 @@ const ctaScriptsRequestForPdpMock: CtaScriptsRequest = { paymentAccountIds: [1], scriptLocations: [CtaScriptsLocation.PDP_MESSAGING], }; +const ctaScriptsRequestForCartPageMock: CtaScriptsRequest = { + paymentAccountIds: [1], + scriptLocations: [CtaScriptsLocation.CART_MESSAGING], +}; describe('OpfCtaScriptsService', () => { let service: OpfCtaScriptsService; @@ -97,6 +102,9 @@ describe('OpfCtaScriptsService', () => { opfDynamicCtaServiceMock.fillCtaRequestforProductPage.and.returnValue( of(ctaScriptsRequestForPdpMock) ); + opfDynamicCtaServiceMock.fillCtaRequestforCartPage.and.returnValue( + of(ctaScriptsRequestForCartPageMock) + ); opfDynamicCtaServiceMock.initiateEvents.and.returnValue(); opfDynamicCtaServiceMock.stopEvents.and.returnValue(); opfDynamicCtaServiceMock.registerScriptReadyEvent.and.returnValue(); @@ -112,7 +120,7 @@ describe('OpfCtaScriptsService', () => { opfBaseFacadeMock.getActiveConfigurationsState.and.returnValue( of(activeConfigurationsMock) ); - opfCtaFacadeMock.getCtaScripts.and.returnValue(of(ctaScriptsresponseMock)); + opfCtaFacadeMock.getCtaScripts.and.returnValue(of(ctaScriptsResponseMock)); }); it('should be created', () => { @@ -163,6 +171,22 @@ describe('OpfCtaScriptsService', () => { }); }); + it('should call DynamicCtaService for CTA on Cart page', (done) => { + cmsServiceMock.getCurrentPage.and.returnValue( + of({ ...mockPage, pageId: 'cartPage' }) + ); + + service.getCtaHtmlslList().subscribe((htmlsList) => { + expect(htmlsList[0].html).toContain( + 'Thanks for purchasing our great products' + ); + expect( + opfDynamicCtaServiceMock.fillCtaRequestforCartPage + ).toHaveBeenCalled(); + done(); + }); + }); + it('should throw an error when empty CTA scripts response from OPF server', (done) => { opfCtaFacadeMock.getCtaScripts.and.returnValue(of({ value: [] })); @@ -175,12 +199,16 @@ describe('OpfCtaScriptsService', () => { }); }); - it('should throw an error when empty ScriptLocation is invalid', (done) => { + it('should throw an error when ScriptLocation is invalid', (done) => { cmsServiceMock.getCurrentPage.and.returnValue( of({ ...mockPage, pageId: 'testPage' }) ); service.getCtaHtmlslList().subscribe({ + next: () => { + fail('Invalid script should fail'); + done(); + }, error: (error) => { expect(error).toEqual('Invalid Script Location'); done(); @@ -188,7 +216,77 @@ describe('OpfCtaScriptsService', () => { }); }); - const ctaScriptsresponseMock: CtaScriptsResponse = { + it('should throw an error when empty ScriptLocation', (done) => { + cmsServiceMock.getCurrentPage.and.returnValue( + of({ ...mockPage, pageId: undefined }) + ); + + service.getCtaHtmlslList().subscribe({ + next: () => { + fail('Empty script should fail'); + done(); + }, + error: (error) => { + expect(error).toEqual('Invalid Script Location'); + done(); + }, + }); + }); + + it('should execute script from script response html', (done) => { + service.loadAndRunScript(dynamicScriptMock).then((scriptResponse) => { + expect( + opfResourceLoaderServiceMock.executeScriptFromHtml + ).toHaveBeenCalled(); + expect(scriptResponse?.html).toEqual(dynamicScriptMock.html); + done(); + }); + }); + + it('should not execute script when html from script response is empty', (done) => { + service + .loadAndRunScript({ ...dynamicScriptMock, html: '' }) + .then((scriptResponse) => { + expect( + opfResourceLoaderServiceMock.executeScriptFromHtml + ).not.toHaveBeenCalled(); + expect(scriptResponse).toBeFalsy(); + done(); + }); + }); + + it('should not execute script when resource loading failed', (done) => { + opfResourceLoaderServiceMock.loadProviderResources.and.returnValue( + Promise.reject() + ); + service + .loadAndRunScript({ ...dynamicScriptMock, html: '' }) + .then((scriptResponse) => { + expect( + opfResourceLoaderServiceMock.executeScriptFromHtml + ).not.toHaveBeenCalled(); + expect(scriptResponse).toBeFalsy(); + done(); + }); + }); + + const dynamicScriptMock: OpfDynamicScript = { + html: '

Thanks for purchasing our great products

Please use promo code:123abc for your next purchase

', + cssUrls: [ + { + url: 'https://checkoutshopper-test.adyen.com/checkoutshopper/sdk/4.2.1/adyen.css', + sri: '', + }, + ], + jsUrls: [ + { + url: 'https://checkoutshopper-test.adyen.com/checkoutshopper/sdk/4.2.1/adyen.js', + sri: '', + }, + ], + }; + + const ctaScriptsResponseMock: CtaScriptsResponse = { value: [ { paymentAccountId: 1, diff --git a/integration-libs/opf/cta/components/opf-cta-scripts/opf-cta-scripts.service.ts b/integration-libs/opf/cta/components/opf-cta-scripts/opf-cta-scripts.service.ts index 690ad125b6b..14bc425c71b 100644 --- a/integration-libs/opf/cta/components/opf-cta-scripts/opf-cta-scripts.service.ts +++ b/integration-libs/opf/cta/components/opf-cta-scripts/opf-cta-scripts.service.ts @@ -129,8 +129,6 @@ export class OpfCtaScriptsService { if (!scriptsLocation) { return throwError(() => 'Invalid Script Location'); } - const toBeImplementedException = () => - throwError(() => 'to be implemented'); const locationToFunctionMap: Record< CtaScriptsLocation, () => Observable @@ -153,16 +151,9 @@ export class OpfCtaScriptsService { scriptsLocation, paymentAccountIds ), - [CtaScriptsLocation.CART_QUICK_BUY]: toBeImplementedException, - [CtaScriptsLocation.CHECKOUT_QUICK_BUY]: toBeImplementedException, - [CtaScriptsLocation.PDP_QUICK_BUY]: toBeImplementedException, }; - const selectedFunction = locationToFunctionMap[scriptsLocation]; - - return selectedFunction - ? selectedFunction() - : throwError(() => 'Invalid Script Location'); + return selectedFunction(); } protected getScriptLocation(): Observable { diff --git a/integration-libs/opf/cta/core/services/opf-dynamic-cta.service.spec.ts b/integration-libs/opf/cta/core/services/opf-dynamic-cta.service.spec.ts new file mode 100644 index 00000000000..f8a9df4f40a --- /dev/null +++ b/integration-libs/opf/cta/core/services/opf-dynamic-cta.service.spec.ts @@ -0,0 +1,205 @@ +import { TestBed } from '@angular/core/testing'; +import { ActiveCartFacade, Cart } from '@spartacus/cart/base/root'; +import { + CurrencyService, + EventService, + LanguageService, + Product, + WindowRef, +} from '@spartacus/core'; +import { CtaScriptsLocation, OpfCtaFacade } from '@spartacus/opf/cta/root'; +import { OpfGlobalFunctionsFacade } from '@spartacus/opf/global-functions/root'; +import { CurrentProductService } from '@spartacus/storefront'; +import { of } from 'rxjs'; +import { OpfDynamicCtaService } from './opf-dynamic-cta.service'; + +describe('OpfDynamicCtaService', () => { + let service: OpfDynamicCtaService; + let globalFunctionsFacadeMock: jasmine.SpyObj; + let eventServiceMock: jasmine.SpyObj; + let currencyServiceMock: jasmine.SpyObj; + let languageServiceMock: jasmine.SpyObj; + let activeCartFacadeMock: jasmine.SpyObj; + let opfCtaFacadeMock: jasmine.SpyObj; + let currentProductServiceMock: jasmine.SpyObj; + + beforeEach(() => { + // Prevent external link navigation + window.onbeforeunload = function () { + return ''; + }; + globalFunctionsFacadeMock = jasmine.createSpyObj('GlobalFunctionsFacade', [ + 'registerGlobalFunctions', + 'removeGlobalFunctions', + ]); + + eventServiceMock = jasmine.createSpyObj('EventService', ['get']); + + currencyServiceMock = jasmine.createSpyObj('CurrencyService', [ + 'getActive', + ]); + languageServiceMock = jasmine.createSpyObj('LanguageService', [ + 'getActive', + ]); + opfCtaFacadeMock = jasmine.createSpyObj('OpfCtaFacade', [ + 'listenScriptReadyEvent', + ]); + currentProductServiceMock = jasmine.createSpyObj('CurrentProductService', [ + 'getProduct', + ]); + activeCartFacadeMock = jasmine.createSpyObj('ActiveCartFacade', [ + 'takeActive', + ]); + + TestBed.configureTestingModule({ + providers: [ + WindowRef, + OpfDynamicCtaService, + { + provide: OpfGlobalFunctionsFacade, + useValue: globalFunctionsFacadeMock, + }, + { provide: EventService, useValue: eventServiceMock }, + { provide: CurrencyService, useValue: currencyServiceMock }, + { provide: ActiveCartFacade, useValue: activeCartFacadeMock }, + { provide: LanguageService, useValue: languageServiceMock }, + { provide: OpfCtaFacade, useValue: opfCtaFacadeMock }, + { provide: CurrentProductService, useValue: currentProductServiceMock }, + ], + }); + service = TestBed.inject(OpfDynamicCtaService); + currencyServiceMock.getActive.and.returnValue(of(mockCurrency)); + languageServiceMock.getActive.and.returnValue(of(mockLanguage)); + eventServiceMock.get.and.returnValue(of(true)); + opfCtaFacadeMock.listenScriptReadyEvent.and.returnValue(of(mockScriptId)); + globalFunctionsFacadeMock.registerGlobalFunctions.and.returnValue(); + globalFunctionsFacadeMock.removeGlobalFunctions.and.returnValue(); + currentProductServiceMock.getProduct.and.returnValue(of(mockProduct)); + activeCartFacadeMock.takeActive.and.returnValue(of(mockCart)); + }); + + it('should be created', () => { + expect(service).toBeTruthy(); + }); + + it('should call activeCart on fillCtaRequestforCartPage', (done) => { + service + .fillCtaRequestforCartPage( + CtaScriptsLocation.CART_MESSAGING, + mockAccountIds + ) + .subscribe((ctaRequest) => { + expect(activeCartFacadeMock.takeActive).toHaveBeenCalled(); + expect(languageServiceMock.getActive).toHaveBeenCalled(); + expect(ctaRequest.scriptLocations).toEqual([ + CtaScriptsLocation.CART_MESSAGING, + ]); + done(); + }); + }); + + it('should call productService on fillCtaRequestforProductPage', (done) => { + service + .fillCtaRequestforProductPage( + CtaScriptsLocation.PDP_MESSAGING, + mockAccountIds + ) + .subscribe((ctaRequest) => { + expect(currentProductServiceMock.getProduct).toHaveBeenCalled(); + expect(languageServiceMock.getActive).toHaveBeenCalled(); + expect(ctaRequest.scriptLocations).toEqual([ + CtaScriptsLocation.PDP_MESSAGING, + ]); + expect( + ctaRequest.additionalData?.find( + (keyVal) => keyVal.key === 'scriptIdentifier' + )?.value + ).toEqual(mockScriptId); + done(); + }); + }); + + it('should start cartListener on cart page initiateEvents', (done) => { + service + .fillCtaRequestforCartPage( + CtaScriptsLocation.CART_MESSAGING, + mockAccountIds + ) + .subscribe(() => { + service.initiateEvents(); + expect(eventServiceMock.get).toHaveBeenCalled(); + expect( + globalFunctionsFacadeMock.registerGlobalFunctions + ).toHaveBeenCalled(); + done(); + }); + }); + + it('should not start cartListener on pdp initiateEvents', (done) => { + service + .fillCtaRequestforProductPage( + CtaScriptsLocation.PDP_MESSAGING, + mockAccountIds + ) + .subscribe(() => { + service.initiateEvents(); + expect(eventServiceMock.get).not.toHaveBeenCalled(); + expect( + globalFunctionsFacadeMock.registerGlobalFunctions + ).toHaveBeenCalled(); + done(); + }); + }); + + it('should remove global functions on stopEvents', (done) => { + service + .fillCtaRequestforProductPage( + CtaScriptsLocation.CART_MESSAGING, + mockAccountIds + ) + .subscribe(() => { + service.initiateEvents(); + service.stopEvents(); + expect( + globalFunctionsFacadeMock.removeGlobalFunctions + ).toHaveBeenCalled(); + + done(); + }); + }); + + const mockAccountIds = [51, 22]; + const mockScriptId = '0001'; + + const mockLanguage = 'en'; + const mockCurrency = 'UDS'; + + const mockProduct: Product = { + name: 'mockProduct', + code: 'code1', + stock: { + stockLevel: 333, + stockLevelStatus: 'inStock', + }, + }; + + const mockCart: Cart = { + code: 'xxx', + guid: 'xxx', + totalItems: 0, + entries: [ + { entryNumber: 0, product: { code: '1234' } }, + { entryNumber: 1, product: { code: '01234' } }, + { entryNumber: 2, product: { code: '3234' } }, + ], + totalPrice: { + currencyIso: 'USD', + value: 100, + }, + totalPriceWithTax: { + currencyIso: 'USD', + value: 0, + }, + user: { uid: 'test' }, + }; +}); diff --git a/integration-libs/opf/cta/core/services/opf-static-cta.service.spec.ts b/integration-libs/opf/cta/core/services/opf-static-cta.service.spec.ts new file mode 100644 index 00000000000..e7a14e6b739 --- /dev/null +++ b/integration-libs/opf/cta/core/services/opf-static-cta.service.spec.ts @@ -0,0 +1,101 @@ +import { TestBed } from '@angular/core/testing'; +import { WindowRef } from '@spartacus/core'; +import { CtaScriptsLocation } from '@spartacus/opf/cta/root'; +import { Order, OrderFacade, OrderHistoryFacade } from '@spartacus/order/root'; +import { of } from 'rxjs'; +import { OpfStaticCtaService } from './opf-static-cta.service'; + +describe('OpfStaticCtaService', () => { + let service: OpfStaticCtaService; + let orderFacadeMock: jasmine.SpyObj; + let orderHistoryFacadeMock: jasmine.SpyObj; + + beforeEach(() => { + orderFacadeMock = jasmine.createSpyObj('OrderFacade', ['getOrderDetails']); + orderHistoryFacadeMock = jasmine.createSpyObj('OrderHistoryFacade', [ + 'getOrderDetails', + ]); + TestBed.configureTestingModule({ + providers: [ + WindowRef, + OpfStaticCtaService, + { provide: OrderFacade, useValue: orderFacadeMock }, + { provide: OrderHistoryFacade, useValue: orderHistoryFacadeMock }, + ], + }); + service = TestBed.inject(OpfStaticCtaService); + orderHistoryFacadeMock.getOrderDetails.and.returnValue(of(mockOrder)); + orderFacadeMock.getOrderDetails.and.returnValue(of(mockOrder)); + }); + + it('should be created', () => { + expect(service).toBeTruthy(); + }); + + it('should call OrderHistoryFacade for CTA on order history page', (done) => { + service + .fillCtaRequestforPagesWithOrder( + CtaScriptsLocation.ORDER_HISTORY_PAYMENT_GUIDE + ) + .subscribe((ctaScriptRequest) => { + expect(ctaScriptRequest.cartId).toContain('mockPaymentInfoId'); + expect(orderHistoryFacadeMock.getOrderDetails).toHaveBeenCalled(); + expect(orderFacadeMock.getOrderDetails).not.toHaveBeenCalled(); + done(); + }); + }); + + it('should call orderFacade for CTA on confirmation page', (done) => { + service + .fillCtaRequestforPagesWithOrder( + CtaScriptsLocation.ORDER_CONFIRMATION_PAYMENT_GUIDE + ) + .subscribe((ctaScriptRequest) => { + expect(ctaScriptRequest.cartId).toContain('mockPaymentInfoId'); + expect(orderHistoryFacadeMock.getOrderDetails).not.toHaveBeenCalled(); + expect(orderFacadeMock.getOrderDetails).toHaveBeenCalled(); + done(); + }); + }); + + it('should throw error when no order Id', (done) => { + orderFacadeMock.getOrderDetails.and.returnValue( + of({ ...mockOrder, paymentInfo: undefined }) + ); + service + .fillCtaRequestforPagesWithOrder( + CtaScriptsLocation.ORDER_CONFIRMATION_PAYMENT_GUIDE + ) + .subscribe({ + next: () => { + fail(); + done(); + }, + error: (error) => { + expect(error).toBeTruthy(); + done(); + }, + }); + }); + + const mockOrder: Order = { + code: 'mockOrder', + paymentInfo: { + id: 'mockPaymentInfoId', + }, + entries: [ + { + product: { + code: '11', + }, + quantity: 1, + }, + { + product: { + code: '22', + }, + quantity: 1, + }, + ], + }; +}); diff --git a/integration-libs/opf/cta/root/model/opf-cta.model.ts b/integration-libs/opf/cta/root/model/opf-cta.model.ts index d55a520ebad..02b672b196e 100644 --- a/integration-libs/opf/cta/root/model/opf-cta.model.ts +++ b/integration-libs/opf/cta/root/model/opf-cta.model.ts @@ -33,9 +33,7 @@ export interface CtaProductItem { export enum CtaScriptsLocation { CART_MESSAGING = 'CART_MESSAGING', PDP_MESSAGING = 'PDP_MESSAGING', - PDP_QUICK_BUY = 'PDP_QUICK_BUY', - CART_QUICK_BUY = 'CART_QUICK_BUY', - CHECKOUT_QUICK_BUY = 'CHECKOUT_QUICK_BUY', + ORDER_CONFIRMATION_PAYMENT_GUIDE = 'ORDER_CONFIRMATION_PAYMENT_GUIDE', ORDER_HISTORY_PAYMENT_GUIDE = 'ORDER_HISTORY_PAYMENT_GUIDE', }