From f83dd0f3e157e8ddd9896eab748f933db08d5318 Mon Sep 17 00:00:00 2001 From: Yu Long Date: Tue, 17 Dec 2024 17:11:00 +0100 Subject: [PATCH] test(UIElement): added tests for sending error event to the analytics --- .../lib/src/components/ANCV/ANCV.test.tsx | 50 ++++++++++++++ packages/lib/src/components/ANCV/ANCV.tsx | 8 ++- .../src/components/Giftcard/Giftcard.test.tsx | 39 +++++++++++ .../lib/src/components/Giftcard/Giftcard.tsx | 1 - .../internal/UIElement/UIElement.test.ts | 65 ++++++++++++++++++- .../internal/UIElement/UIElement.tsx | 2 +- .../lib/src/styles/variable-generator.scss | 3 +- 7 files changed, 160 insertions(+), 8 deletions(-) create mode 100644 packages/lib/src/components/ANCV/ANCV.test.tsx diff --git a/packages/lib/src/components/ANCV/ANCV.test.tsx b/packages/lib/src/components/ANCV/ANCV.test.tsx new file mode 100644 index 000000000..3a1bf780b --- /dev/null +++ b/packages/lib/src/components/ANCV/ANCV.test.tsx @@ -0,0 +1,50 @@ +import { render } from '@testing-library/preact'; +import { mockDeep } from 'jest-mock-extended'; +import { AnalyticsModule } from '../../types/global-types'; +import AdyenCheckoutError from '../../core/Errors/AdyenCheckoutError'; +import { ANALYTICS_ERROR_TYPE, ANALYTICS_EVENT } from '../../core/Analytics/constants'; +import ANCV from './ANCV'; + +const flushPromises = () => new Promise(process.nextTick); + +describe('ANCV', () => { + const resources = global.resources; + const i18n = global.i18n; + + const baseProps = { + amount: { value: 1000, currency: 'EUR' }, + i18n, + loadingContext: 'mock' + }; + + describe('createOrder', () => { + test('should send an error event to the analytics if the createOrder call fails for the session flow', async () => { + const code = 'mockErrorCode'; + const analytics = mockDeep(); + const mockedSendAnalytics = analytics.sendAnalytics as jest.Mock; + + const ancv = new ANCV(global.core, { + ...baseProps, + modules: { + resources, + analytics + }, + onError: () => {}, + // @ts-ignore test only + session: { + createOrder: () => { + return Promise.reject(new AdyenCheckoutError('NETWORK_ERROR', '', { code })); + } + } + }); + render(ancv.render()); + await ancv.createOrder(); + await flushPromises(); + expect(mockedSendAnalytics).toHaveBeenCalledWith( + 'ancv', + { code, errorType: ANALYTICS_ERROR_TYPE.apiError, type: ANALYTICS_EVENT.error }, + undefined + ); + }); + }); +}); diff --git a/packages/lib/src/components/ANCV/ANCV.tsx b/packages/lib/src/components/ANCV/ANCV.tsx index 7f2c5e74d..64662df32 100644 --- a/packages/lib/src/components/ANCV/ANCV.tsx +++ b/packages/lib/src/components/ANCV/ANCV.tsx @@ -56,7 +56,13 @@ export class ANCVElement extends UIElement { }) .catch(error => { this.setStatus(error?.message || 'error'); - if (this.props.onError) this.handleError(new AdyenCheckoutError('ERROR', error)); + if (this.props.onError) { + if (error instanceof AdyenCheckoutError) { + this.handleError(error); + } else { + this.handleError(new AdyenCheckoutError('ERROR', error)); + } + } }); }; diff --git a/packages/lib/src/components/Giftcard/Giftcard.test.tsx b/packages/lib/src/components/Giftcard/Giftcard.test.tsx index 5a4da203c..337e27a89 100644 --- a/packages/lib/src/components/Giftcard/Giftcard.test.tsx +++ b/packages/lib/src/components/Giftcard/Giftcard.test.tsx @@ -1,6 +1,10 @@ import Giftcard from './Giftcard'; import { render, screen } from '@testing-library/preact'; import userEvent from '@testing-library/user-event'; +import AdyenCheckoutError from '../../core/Errors/AdyenCheckoutError'; +import { AnalyticsModule } from '../../types/global-types'; +import { mockDeep } from 'jest-mock-extended'; +import { ANALYTICS_ERROR_TYPE, ANALYTICS_EVENT } from '../../core/Analytics/constants'; const flushPromises = () => new Promise(process.nextTick); @@ -135,6 +139,41 @@ describe('Giftcard', () => { expect(onOrderRequest).toHaveBeenCalled(); }); + test('should send an error event to the analytics if the createOrder call fails for the session flow', async () => { + const code = 'mockErrorCode'; + const analytics = mockDeep(); + const mockedSendAnalytics = analytics.sendAnalytics as jest.Mock; + const onBalanceCheck = jest.fn(resolve => + resolve({ + balance: { value: 500, currency: 'EUR' } + }) + ); + const giftcard = new Giftcard(global.core, { + ...baseProps, + modules: { + resources, + analytics + }, + onError: () => {}, + onBalanceCheck, + // @ts-ignore test only + session: { + createOrder: () => { + return Promise.reject(new AdyenCheckoutError('NETWORK_ERROR', '', { code })); + } + } + }); + render(giftcard.render()); + giftcard.setState({ isValid: true }); + giftcard.balanceCheck(); + await flushPromises(); + expect(mockedSendAnalytics).toHaveBeenCalledWith( + 'giftcard', + { code, errorType: ANALYTICS_ERROR_TYPE.apiError, type: ANALYTICS_EVENT.error }, + undefined + ); + }); + test('if there is enough balance for checkout we should require confirmation', async () => { const onBalanceCheck = jest.fn(resolve => resolve({ diff --git a/packages/lib/src/components/Giftcard/Giftcard.tsx b/packages/lib/src/components/Giftcard/Giftcard.tsx index 3c567e33d..f8f57077a 100644 --- a/packages/lib/src/components/Giftcard/Giftcard.tsx +++ b/packages/lib/src/components/Giftcard/Giftcard.tsx @@ -62,7 +62,6 @@ export class GiftcardElement extends UIElement { return new Promise((resolve, reject) => { void this.props.onOrderRequest(resolve, reject, data); }); - if (this.props.session) { return this.props.session.createOrder(); } diff --git a/packages/lib/src/components/internal/UIElement/UIElement.test.ts b/packages/lib/src/components/internal/UIElement/UIElement.test.ts index 9cba2b8b1..355d6a620 100644 --- a/packages/lib/src/components/internal/UIElement/UIElement.test.ts +++ b/packages/lib/src/components/internal/UIElement/UIElement.test.ts @@ -4,7 +4,9 @@ import { any, mock, mockDeep } from 'jest-mock-extended'; import { AdyenCheckout, ThreeDS2Challenge, ThreeDS2DeviceFingerprint } from '../../../index'; import { UIElementProps } from './types'; import { Resources } from '../../../core/Context/Resources'; -import { PaymentActionsType } from '../../../types/global-types'; +import { AnalyticsModule, PaymentActionsType } from '../../../types/global-types'; +import AdyenCheckoutError from '../../../core/Errors/AdyenCheckoutError'; +import { ANALYTICS_ERROR_TYPE, ANALYTICS_EVENT } from '../../../core/Analytics/constants'; jest.mock('../../../core/Services/get-translations'); @@ -23,6 +25,9 @@ class MyElement extends UIElement { public callOnChange() { super.onChange(); } + public handleAdditionalDetails(data) { + super.handleAdditionalDetails(data); + } render() { return ''; } @@ -32,8 +37,15 @@ const submitMock = jest.fn(); (global as any).HTMLFormElement.prototype.submit = () => submitMock; let core; +let analytics; beforeEach(() => { core = mockDeep(); + analytics = mockDeep(); +}); + +afterEach(() => { + jest.clearAllMocks(); + jest.resetAllMocks(); }); describe('UIElement', () => { @@ -231,7 +243,7 @@ describe('UIElement', () => { test('should trigger showValidation() and not call makePaymentsCall() if component is not valid', () => { const showValidation = jest.fn(); - const element = new MyElement(core); + const element = new MyElement(core, { modules: { analytics } }); // @ts-ignore Checking that internal method is not reached const makePaymentsCallSpy = jest.spyOn(element, 'makePaymentsCall'); @@ -263,6 +275,7 @@ describe('UIElement', () => { }); const element = new MyElement(core, { + modules: { analytics }, onSubmit: onSubmitMock, onPaymentCompleted: onPaymentCompletedMock }); @@ -275,7 +288,7 @@ describe('UIElement', () => { expect(onPaymentCompletedMock).toHaveBeenCalledWith({ resultCode: 'Authorized' }, element); }); - test('should make successfull payment using sessions flow', async () => { + test('should make successful payment using sessions flow', async () => { const onPaymentCompletedMock = jest.fn(); core.session.submitPayment.calledWith(any()).mockResolvedValue({ @@ -293,6 +306,7 @@ describe('UIElement', () => { }); const element = new MyElement(core, { + modules: { analytics }, onPaymentCompleted: onPaymentCompletedMock }); @@ -327,6 +341,7 @@ describe('UIElement', () => { }); const element = new MyElement(core, { + modules: { analytics }, onSubmit: onSubmitMock, onPaymentFailed: onPaymentFailedMock }); @@ -347,6 +362,7 @@ describe('UIElement', () => { jest.spyOn(MyElement.prototype, 'isValid', 'get').mockReturnValue(true); const element = new MyElement(core, { + modules: { analytics }, onSubmit: onSubmitMock, onPaymentFailed: onPaymentFailedMock }); @@ -374,6 +390,7 @@ describe('UIElement', () => { jest.spyOn(MyElement.prototype, 'isValid', 'get').mockReturnValue(true); const element = new MyElement(core, { + modules: { analytics }, onSubmit: onSubmitMock }); @@ -420,6 +437,7 @@ describe('UIElement', () => { }); const element = new MyElement(core, { + modules: { analytics }, onOrderUpdated: onOrderUpdatedMock }); @@ -469,6 +487,7 @@ describe('UIElement', () => { core.session = null; const element = new MyElement(core, { + modules: { analytics }, onSubmit: onSubmitMock, onPaymentMethodsRequest: onPaymentMethodsRequestMock, onOrderUpdated: onOrderUpdatedMock @@ -531,6 +550,7 @@ describe('UIElement', () => { core.session = null; const element = new MyElement(core, { + modules: { analytics }, onSubmit: onSubmitMock, onOrderUpdated: onOrderUpdatedMock, onError: onErrorMock @@ -551,6 +571,26 @@ describe('UIElement', () => { expect(onOrderUpdatedMock).toHaveBeenCalledTimes(1); expect(onOrderUpdatedMock).toHaveBeenCalledWith({ order }); }); + + test('should send an error event to analytic module with correct errorType and error code, if makePayment call fails', async () => { + const errorCode = 'mockedErrorCode'; + const txVariant = 'scheme'; + + core.session.submitPayment.mockImplementation(() => Promise.reject(new AdyenCheckoutError('NETWORK_ERROR', '', { code: errorCode }))); + const analytics = mock(); + const mockedSendAnalytics = analytics.sendAnalytics as jest.Mock; + jest.spyOn(MyElement.prototype, 'isValid', 'get').mockReturnValue(true); + + const element = new MyElement(core, { type: txVariant, modules: { analytics } }); + element.submit(); + await new Promise(process.nextTick); + + expect(mockedSendAnalytics).toHaveBeenCalledWith( + txVariant, + { code: errorCode, errorType: ANALYTICS_ERROR_TYPE.apiError, type: ANALYTICS_EVENT.error }, + undefined + ); + }); }); describe('[Internal] handleAdditionalDetails()', () => { @@ -684,5 +724,24 @@ describe('UIElement', () => { expect(onPaymentFailedMock).toHaveBeenCalledTimes(1); expect(onPaymentFailedMock).toHaveBeenCalledWith(undefined, element); }); + + test('should send an error event to analytic module with correct errorType and error code, if payment/details call fails', async () => { + const errorCode = 'mockedErrorCode'; + const txVariant = 'scheme'; + + core.session.submitDetails.mockImplementation(() => Promise.reject(new AdyenCheckoutError('NETWORK_ERROR', '', { code: errorCode }))); + + const mockedSendAnalytics = analytics.sendAnalytics as jest.Mock; + + const element = new MyElement(core, { type: txVariant, modules: { analytics } }); + element.handleAdditionalDetails({}); + await new Promise(process.nextTick); + + expect(mockedSendAnalytics).toHaveBeenCalledWith( + txVariant, + { code: errorCode, errorType: ANALYTICS_ERROR_TYPE.apiError, type: ANALYTICS_EVENT.error }, + undefined + ); + }); }); }); diff --git a/packages/lib/src/components/internal/UIElement/UIElement.tsx b/packages/lib/src/components/internal/UIElement/UIElement.tsx index d65253fd9..7c0e4497a 100644 --- a/packages/lib/src/components/internal/UIElement/UIElement.tsx +++ b/packages/lib/src/components/internal/UIElement/UIElement.tsx @@ -177,7 +177,7 @@ export abstract class UIElement

exten if (this.constructor['type'] === 'scheme' || this.constructor['type'] === 'bcmc') { return this.constructor['type']; } - return this.props.type; + return this.type; } public submit(): void { diff --git a/packages/lib/src/styles/variable-generator.scss b/packages/lib/src/styles/variable-generator.scss index 08e3ad21d..44d45455a 100644 --- a/packages/lib/src/styles/variable-generator.scss +++ b/packages/lib/src/styles/variable-generator.scss @@ -13,14 +13,13 @@ @return $adyen-output-map; } - @function token($token, $generate-css-var: true) { $adyen-tokens-map: (); @if $generate-css-var { $adyen-tokens-map: adyen-sdk-generate-css-variables($color, $text, $focus-ring, $border, $spacer, $shadow); } @else { - $adyen-tokens-map: map-merge($color, $text, $focus-ring, $border, $spacer, $shadow) + $adyen-tokens-map: map-merge($color, $text, $focus-ring, $border, $spacer, $shadow); } @return map-get($adyen-tokens-map, '#{$token}');