1 import { fireEvent } from '@testing-library/dom';
3 import type { BinData } from '../lib';
4 import type { AuthorizedPaymentIntent, DirectDebitCustomer, PaymentIntent } from '../lib/types';
5 import { resetChargebee } from './chargebee';
6 import { formatCustomer, initialize } from './chargebee-entry';
7 import type { DirectDebitSubmitEvent, GetHeightEvent, SetConfigurationEvent } from './message-bus';
8 import { getMessageBus } from './message-bus';
10 jest.mock('./ui-utils');
12 const createFieldMock = jest.fn().mockReturnValue({
13 at: jest.fn().mockReturnValue({
23 const mountMock = jest.fn();
24 const onMock = jest.fn();
25 const getBinDataMock = jest.fn();
27 const authorizeWith3dsCatchMock = jest.fn();
28 const authorizeWith3dsThenMock = jest.fn().mockReturnValue({
29 catch: authorizeWith3dsCatchMock,
31 const authorizeWith3dsMock = jest.fn().mockReturnValue({
32 then: authorizeWith3dsThenMock,
35 const createComponentMock = jest.fn().mockReturnValue({
36 createField: createFieldMock,
39 getBinData: getBinDataMock,
40 authorizeWith3ds: authorizeWith3dsMock,
43 const directDebitHandlerMock = {
44 setPaymentIntent: jest.fn(),
45 handlePayment: jest.fn(),
48 const loadMock = jest.fn().mockResolvedValue(directDebitHandlerMock);
50 const chargebeeInitMock = jest.fn().mockReturnValue({
53 createComponent: createComponentMock,
60 createFieldMock.mockClear();
61 mountMock.mockClear();
63 createComponentMock.mockClear();
64 chargebeeInitMock.mockClear();
66 (global as any).Chargebee = {
67 init: chargebeeInitMock,
74 window.document.body.innerHTML = `
76 <div id="chargebee-form-wrapper"></div>
82 getMessageBus().destroy();
85 const defaultSetConfigurationEvent: SetConfigurationEvent = {
86 type: 'set-configuration',
87 correlationId: 'id-1',
88 paymentMethodType: 'card',
93 '--signal-danger': '#000000',
94 '--border-radius-md': '#000000',
95 '--border-norm': '#000000',
96 '--focus-outline': '#000000',
97 '--focus-ring': '#000000',
98 '--field-norm': '#000000',
99 '--field-background-color': '#000000',
100 '--field-focus-background-color': '#000000',
101 '--field-focus-text-color': '#000000',
102 '--field-placeholder-color': '#000000',
103 '--field-text-color': '#000000',
104 '--selection-text-color': '#000000',
105 '--selection-background-color': '#000000',
108 cardNumberPlaceholder: '0000 0000 0000 0000',
109 cardExpiryPlaceholder: 'MM/YY',
110 cardCvcPlaceholder: '000',
111 invalidCardNumberMessage: 'Invalid card number',
112 invalidCardExpiryMessage: 'Invalid card expiry',
113 invalidCardCvcMessage: 'Invalid card cvc',
115 renderMode: 'one-line',
118 function sendEventToChargebee(event: any) {
121 new MessageEvent('message', {
127 function initChargebee(setConfiguration: SetConfigurationEvent = defaultSetConfigurationEvent) {
128 const initPromise = initialize();
129 sendEventToChargebee(setConfiguration);
133 function receiveMessage(type: string) {
134 return new Promise<any>((resolve) => {
135 window.addEventListener('message', (event: MessageEvent) => {
137 const data = JSON.parse(event.data);
138 if (data.type === type) {
146 describe('initialize', () => {
147 it('should create message bus with onSetConfiguration and onGetHeight handlers', async () => {
148 const result = await initChargebee();
149 const messageBus = getMessageBus();
150 expect(messageBus.onSetConfiguration).toBeDefined();
151 expect(messageBus.onGetHeight).toBeDefined();
153 expect(result.chargebeeMock).toEqual(true);
157 describe('height test', () => {
158 let initialScrollHeight: number;
161 initialScrollHeight = document.body.scrollHeight;
163 Object.defineProperty(document.body, 'scrollHeight', {
170 Object.defineProperty(document.body, 'scrollHeight', {
171 value: initialScrollHeight,
176 it('should return the height of the form', async () => {
177 await initChargebee();
179 const getHeight: GetHeightEvent = {
181 correlationId: 'id-1',
184 sendEventToChargebee(getHeight);
186 const heightResponse = await receiveMessage('get-height-response');
188 expect(heightResponse.data).toMatchObject({
189 // the total height must include the extra bottom too
196 function extractAuthorizeWith3dsCallbacks() {
197 const callbacks = authorizeWith3dsMock.mock.calls[0][2];
198 const challengeCallback = callbacks.challenge;
200 const thenCallback = authorizeWith3dsThenMock.mock.calls[0][0];
201 const catchCallback = authorizeWith3dsCatchMock.mock.calls[0][0];
210 describe('Credit card', () => {
211 it('should render the template', async () => {
212 await initChargebee();
213 expect(document.getElementById('chargebee-form-wrapper')).toBeDefined();
214 expect(document.querySelector('.card-input')).toBeDefined();
217 it('should initialize the form', async () => {
218 const chargebee = await initChargebee();
219 expect(chargebee.load).toHaveBeenCalledWith('components');
221 expect(chargebee.createComponent).toHaveBeenCalledTimes(1);
222 expect(chargebee.createComponent.mock.calls[0][0]).toEqual('card');
224 expect(createFieldMock).toHaveBeenCalledTimes(3);
225 expect(createFieldMock.mock.calls[0][0]).toEqual('number');
226 expect(createFieldMock.mock.calls[1][0]).toEqual('expiry');
227 expect(createFieldMock.mock.calls[2][0]).toEqual('cvv');
229 expect(mountMock).toHaveBeenCalledTimes(1);
232 it('should return bin', async () => {
233 await initChargebee();
235 const binData: BinData = {
239 getBinDataMock.mockReturnValueOnce(binData);
241 sendEventToChargebee({
243 correlationId: 'id-2',
246 const binResponse = await receiveMessage('get-bin-response');
247 expect(binResponse.data).toEqual(binData);
250 it('should submit the form', async () => {
251 await initChargebee();
253 sendEventToChargebee({
254 type: 'chargebee-submit',
255 correlationId: 'id-3',
258 object_type: 'payment_intent',
264 expect(authorizeWith3dsMock).toHaveBeenCalledTimes(1);
265 // the first arg must be the payment intent
266 expect(authorizeWith3dsMock.mock.calls[0][0]).toEqual({
268 object_type: 'payment_intent',
270 // the second one must be the billing details
271 expect(authorizeWith3dsMock.mock.calls[0][1]).toEqual({
278 const { challengeCallback, thenCallback, catchCallback } = extractAuthorizeWith3dsCallbacks();
280 expect(challengeCallback).toBeDefined();
281 expect(thenCallback).toBeDefined();
282 expect(catchCallback).toBeDefined();
285 it('should send 3ds challenge response', async () => {
286 await initChargebee();
288 sendEventToChargebee({
289 type: 'chargebee-submit',
290 correlationId: 'id-3',
293 object_type: 'payment_intent',
299 const { challengeCallback } = extractAuthorizeWith3dsCallbacks();
301 const url = 'https://proton.me/3ds-challenge';
302 challengeCallback(url);
304 const message = await receiveMessage('3ds-challenge');
305 expect(message).toEqual({
306 type: '3ds-challenge',
308 correlationId: 'id-3', // the same as the original submit message
313 it('should send submission success', async () => {
314 await initChargebee();
316 sendEventToChargebee({
317 type: 'chargebee-submit',
318 correlationId: 'id-3',
321 object_type: 'payment_intent',
327 const { thenCallback } = extractAuthorizeWith3dsCallbacks();
328 const authorizedPaymentIntent = {
330 object_type: 'payment_intent',
331 status: 'authorized',
333 thenCallback(authorizedPaymentIntent);
335 const message = await receiveMessage('chargebee-submit-response');
336 expect(message).toEqual(
337 expect.objectContaining({
338 type: 'chargebee-submit-response',
340 correlationId: 'id-3',
343 authorizedPaymentIntent,
349 it('should send submission error', async () => {
350 await initChargebee();
352 sendEventToChargebee({
353 type: 'chargebee-submit',
354 correlationId: 'id-3',
357 object_type: 'payment_intent',
363 const { catchCallback } = extractAuthorizeWith3dsCallbacks();
364 const error = new Error('some error');
365 catchCallback(error);
367 const message = await receiveMessage('chargebee-submit-response');
368 expect(message).toMatchObject({
369 type: 'chargebee-submit-response',
371 correlationId: 'id-3',
373 message: error.message,
381 describe('formatCustomer', () => {
382 const baseCustomer: DirectDebitCustomer = {
383 email: 'test@example.com',
384 company: 'Test Company',
387 customerNameType: 'individual',
389 addressLine1: '123 Test St',
392 it('should format customer with individual name type', () => {
393 const customer: DirectDebitCustomer = {
395 customerNameType: 'individual',
398 const formattedCustomer = formatCustomer(customer);
400 expect(formattedCustomer).toEqual({
401 email: 'test@example.com',
406 addressLine1: '123 Test St',
411 it('should format customer with company name type', () => {
412 const customer: DirectDebitCustomer = {
414 customerNameType: 'company',
417 const formattedCustomer = formatCustomer(customer);
419 expect(formattedCustomer).toEqual({
420 email: 'test@example.com',
421 company: 'Test Company',
424 addressLine1: '123 Test St',
429 it('should handle missing optional fields', () => {
430 const customer: DirectDebitCustomer = {
432 customerNameType: 'individual',
437 const formattedCustomer = formatCustomer(customer);
439 expect(formattedCustomer).toEqual({
440 email: 'test@example.com',
450 it('should handle all optional fields being present', () => {
451 const customer: DirectDebitCustomer = {
453 customerNameType: 'company',
455 addressLine1: '456 Test Ave',
458 const formattedCustomer = formatCustomer(customer);
460 expect(formattedCustomer).toEqual({
461 email: 'test@example.com',
462 company: 'Test Company',
465 addressLine1: '456 Test Ave',
471 describe('Direct Debit', () => {
472 beforeEach(async () => {
473 // Set up the configuration for direct debit
474 const directDebitConfig = {
475 ...defaultSetConfigurationEvent,
476 paymentMethodType: 'direct-debit' as const,
478 await initChargebee(directDebitConfig);
481 it('should initialize direct debit handler', async () => {
482 expect(loadMock).toHaveBeenCalledWith('direct_debit');
485 it('should handle direct debit submission', async () => {
486 const paymentIntent: PaymentIntent = {
490 currency_code: 'USD',
491 gateway_account_id: 'ga_123',
493 customer_id: 'cust_123',
494 payment_method_type: 'card',
495 expires_at: 1234567890,
496 created_at: 1234567890,
497 modified_at: 1234567890,
498 updated_at: 1234567890,
499 resource_version: 1234567890,
500 object: 'payment_intent',
503 const customer: DirectDebitCustomer = {
504 email: 'test@example.com',
505 company: 'Test Company',
508 customerNameType: 'individual',
510 addressLine1: '123 Test St',
513 const bankAccount = {
514 iban: 'DE89370400440532013000',
517 const directDebitSubmitEvent: DirectDebitSubmitEvent = {
518 type: 'direct-debit-submit',
519 correlationId: 'dd-123',
525 sendEventToChargebee(directDebitSubmitEvent);
527 // Wait for the message bus to process the event
528 await new Promise((resolve) => setTimeout(resolve, 0));
530 expect(directDebitHandlerMock.setPaymentIntent).toHaveBeenCalledWith(paymentIntent);
531 expect(directDebitHandlerMock.handlePayment).toHaveBeenCalledWith(
535 email: 'test@example.com',
540 addressLine1: '123 Test St',
544 expect.objectContaining({
545 challenge: expect.any(Function),
546 success: expect.any(Function),
547 error: expect.any(Function),
552 it('should handle direct debit success', async () => {
553 const directDebitSubmitEvent: DirectDebitSubmitEvent = {
554 type: 'direct-debit-submit',
555 correlationId: 'dd-123',
556 paymentIntent: {} as PaymentIntent,
557 customer: {} as DirectDebitCustomer,
558 bankAccount: { iban: 'DE89370400440532013000' },
561 sendEventToChargebee(directDebitSubmitEvent);
563 // Wait for the message bus to process the event
564 await new Promise((resolve) => setTimeout(resolve, 0));
566 const successCallback = directDebitHandlerMock.handlePayment.mock.calls[0][1].success;
567 const authorizedPaymentIntent: AuthorizedPaymentIntent = {
569 status: 'authorized',
571 currency_code: 'USD',
572 gateway_account_id: 'ga_123',
574 customer_id: 'cust_123',
575 payment_method_type: 'card',
576 expires_at: 1234567890,
577 created_at: 1234567890,
578 modified_at: 1234567890,
579 updated_at: 1234567890,
580 resource_version: 1234567890,
581 object: 'payment_intent',
582 active_payment_attempt: {
584 status: 'authorized',
585 payment_method_type: 'card',
586 id_at_gateway: 'ch_123',
587 created_at: 1234567890,
588 modified_at: 1234567890,
589 object: 'payment_attempt',
593 successCallback(authorizedPaymentIntent);
595 const message = await receiveMessage('direct-debit-submit-response');
596 expect(message).toEqual(
597 expect.objectContaining({
598 type: 'direct-debit-submit-response',
600 correlationId: 'dd-123',
601 data: authorizedPaymentIntent,
606 it('should handle direct debit error', async () => {
607 const directDebitSubmitEvent: DirectDebitSubmitEvent = {
608 type: 'direct-debit-submit',
609 correlationId: 'dd-123',
610 paymentIntent: {} as PaymentIntent,
611 customer: {} as DirectDebitCustomer,
612 bankAccount: { iban: 'DE89370400440532013000' },
615 sendEventToChargebee(directDebitSubmitEvent);
617 // Wait for the message bus to process the event
618 await new Promise((resolve) => setTimeout(resolve, 0));
620 const errorCallback = directDebitHandlerMock.handlePayment.mock.calls[0][1].error;
621 const error = new Error('Direct debit payment failed');
623 errorCallback(error);
625 const message = await receiveMessage('direct-debit-submit-response');
626 expect(message).toEqual(
627 expect.objectContaining({
628 type: 'direct-debit-submit-response',
630 correlationId: 'dd-123',
631 error: expect.objectContaining({
632 message: 'Direct debit payment failed',