Skip to content

Commit

Permalink
test: added test
Browse files Browse the repository at this point in the history
  • Loading branch information
longyulongyu committed Nov 20, 2024
1 parent 2e954c5 commit 78080a9
Show file tree
Hide file tree
Showing 6 changed files with 111 additions and 33 deletions.
5 changes: 5 additions & 0 deletions .changeset/new-bags-crash.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@adyen/adyen-web': patch
---

Send redirection error events to analytics.
81 changes: 79 additions & 2 deletions packages/lib/src/components/Redirect/Redirect.test.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
import { mount } from 'enzyme';
import { render, waitFor, screen } from '@testing-library/preact';
import { h } from 'preact';
import Redirect from './Redirect';
import RedirectShopper from './components/RedirectShopper';
import RedirectElement from './Redirect';
import Analytics from '../../core/Analytics';
import { RedirectConfiguration } from './types';

jest.mock('../../utils/detectInIframeInSameOrigin', () => {
return jest.fn().mockImplementation(() => {
Expand All @@ -13,7 +15,7 @@ jest.mock('../../utils/detectInIframeInSameOrigin', () => {
describe('Redirect', () => {
describe('isValid', () => {
test('Is always valid', () => {
const redirect = new Redirect(global.core, { type: 'redirect' });
const redirect = new RedirectElement(global.core, { type: 'redirect' });
expect(redirect.isValid).toBe(true);
});
});
Expand Down Expand Up @@ -57,3 +59,78 @@ describe('Redirect', () => {
});
});
});

describe('Redirect error', () => {
const oldWindowLocation = window.location;

beforeAll(() => {
delete window.location;
// @ts-ignore test only
window.location = Object.defineProperties(
{},
{
...Object.getOwnPropertyDescriptors(oldWindowLocation),
assign: {
configurable: true,
value: jest.fn()
}
}
);
});

afterAll(() => {
window.location = oldWindowLocation;
});

test('should send an error event to the analytics module if beforeRedirect rejects', async () => {
const analytics = Analytics({ analytics: {}, loadingContext: '', locale: '', clientKey: '', bundleType: '' });
analytics.sendAnalytics = jest.fn(() => {});
const props: RedirectConfiguration = {
url: 'test',
method: 'POST',
paymentMethodType: 'ideal',
modules: { analytics },
beforeRedirect: (_, reject) => {
return reject();
}
};

const redirectElement = new RedirectElement(global.core, props);
render(redirectElement.render());
await waitFor(() => {
expect(screen.getByTestId('redirect-shopper-form')).toBeInTheDocument();
});

expect(analytics.sendAnalytics).toHaveBeenCalledWith(
'ideal',
{ code: '600', component: 'ideal', errorType: 'Redirect', type: 'error' },
undefined
);
});

test('should send an error event to the analytics module if the redirection failed', async () => {
(window.location.assign as jest.Mock).mockImplementation(() => {
throw new Error('Mock error');
});

const analytics = Analytics({ analytics: {}, loadingContext: '', locale: '', clientKey: '', bundleType: '' });
analytics.sendAnalytics = jest.fn(() => {});
const props: RedirectConfiguration = {
url: 'test',
method: 'GET',
paymentMethodType: 'ideal',
modules: { analytics }
};

const redirectElement = new RedirectElement(global.core, props);
render(redirectElement.render());

await waitFor(() => {
expect(analytics.sendAnalytics).toHaveBeenCalledWith(
'ideal',
{ code: '600', component: 'ideal', errorType: 'Redirect', type: 'error' },
undefined
);
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@ class RedirectShopper extends Component<RedirectShopperProps> {
return (
<form
method="post"
data-testid="redirect-shopper-form"
action={url}
style={{ display: 'none' }}
ref={ref => {
Expand Down
2 changes: 1 addition & 1 deletion packages/lib/src/core/Analytics/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ export const ANALYTICS_EVENT = {

export const ANALYTICS_ERROR_TYPE = {
network: 'Network',
implementation: 'Implementation',
implementation: 'ImplementationError',
internal: 'Internal',
apiError: 'ApiError',
sdkError: 'SdkError',
Expand Down
4 changes: 1 addition & 3 deletions packages/lib/src/core/Analytics/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -115,9 +115,7 @@ export type CreateAnalyticsEventObject = {

export type EventQueueProps = Pick<AnalyticsConfig, 'analyticsContext' | 'clientKey'> & { analyticsPath: string };

export interface SendAnalyticsObject extends Omit<AnalyticsObject, 'timestamp' | 'id' | 'component'> {
component?: string;
}
export type SendAnalyticsObject = Omit<AnalyticsObject, 'timestamp' | 'id' | 'component'> & { component?: string };

export type FieldErrorAnalyticsObject = {
fieldType: string;
Expand Down
51 changes: 24 additions & 27 deletions packages/lib/src/core/Analytics/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,33 +30,30 @@ export const getUTCTimestamp = () => Date.now();
*
* All objects can also have a "metadata" object of key-value pairs
*/

export const createAnalyticsObject = (aObj: CreateAnalyticsObject): AnalyticsObject => {
return {
timestamp: String(getUTCTimestamp()),
component: aObj.component,
id: uuid(),
/** ERROR */
...(aObj.event === 'error' && { code: aObj.code, errorType: aObj.errorType, message: aObj.message }), // error event
/** LOG */
...(aObj.event === 'log' && { type: aObj.type, message: aObj.message }), // log event
...(aObj.event === 'log' && (aObj.type === ANALYTICS_ACTION_STR || aObj.type === THREEDS2_FULL) && { subType: aObj.subtype }), // only added if we have a log event of Action type or ThreeDS2
...(aObj.event === 'log' && aObj.type === THREEDS2_FULL && { result: aObj.result }), // only added if we have a log event of ThreeDS2 type
/** INFO */
...(aObj.event === 'info' && { type: aObj.type, target: aObj.target }), // info event
...(aObj.event === 'info' && aObj.issuer && { issuer: aObj.issuer }), // relates to issuerLists
...(aObj.event === 'info' && { isExpress: aObj.isExpress, expressPage: aObj.expressPage }), // relates to Plugins & detecting Express PMs
...(aObj.event === 'info' && aObj.isStoredPaymentMethod && { isStoredPaymentMethod: aObj.isStoredPaymentMethod, brand: aObj.brand }), // only added if we have an info event about a storedPM
...(aObj.event === 'info' &&
aObj.type === ANALYTICS_VALIDATION_ERROR_STR && {
validationErrorCode: mapErrorCodesForAnalytics(aObj.validationErrorCode, aObj.target),
validationErrorMessage: aObj.validationErrorMessage
}), // only added if we have an info event describing a validation error
...(aObj.configData && { configData: aObj.configData }),
/** All */
...(aObj.metadata && { metadata: aObj.metadata })
};
};
export const createAnalyticsObject = (aObj: CreateAnalyticsObject): AnalyticsObject => ({
timestamp: String(getUTCTimestamp()),
component: aObj.component,
id: uuid(),
/** ERROR */
...(aObj.event === 'error' && { code: aObj.code, errorType: aObj.errorType, message: aObj.message }), // error event
/** LOG */
...(aObj.event === 'log' && { type: aObj.type, message: aObj.message }), // log event
...(aObj.event === 'log' && (aObj.type === ANALYTICS_ACTION_STR || aObj.type === THREEDS2_FULL) && { subType: aObj.subtype }), // only added if we have a log event of Action type or ThreeDS2
...(aObj.event === 'log' && aObj.type === THREEDS2_FULL && { result: aObj.result }), // only added if we have a log event of ThreeDS2 type
/** INFO */
...(aObj.event === 'info' && { type: aObj.type, target: aObj.target }), // info event
...(aObj.event === 'info' && aObj.issuer && { issuer: aObj.issuer }), // relates to issuerLists
...(aObj.event === 'info' && { isExpress: aObj.isExpress, expressPage: aObj.expressPage }), // relates to Plugins & detecting Express PMs
...(aObj.event === 'info' && aObj.isStoredPaymentMethod && { isStoredPaymentMethod: aObj.isStoredPaymentMethod, brand: aObj.brand }), // only added if we have an info event about a storedPM
...(aObj.event === 'info' &&
aObj.type === ANALYTICS_VALIDATION_ERROR_STR && {
validationErrorCode: mapErrorCodesForAnalytics(aObj.validationErrorCode, aObj.target),
validationErrorMessage: aObj.validationErrorMessage
}), // only added if we have an info event describing a validation error
...(aObj.configData && { configData: aObj.configData }),
/** All */
...(aObj.metadata && { metadata: aObj.metadata })
});

const mapErrorCodesForAnalytics = (errorCode: string, target: string) => {
// Some of the more generic error codes required combination with target to retrieve a specific code
Expand Down

0 comments on commit 78080a9

Please sign in to comment.