1 import { fireEvent, render, waitFor } from '@testing-library/react';
2 import userEvent from '@testing-library/user-event';
4 import { PAYMENT_TOKEN_STATUS } from '@proton/payments';
5 import { buyCredit, createTokenV4 } from '@proton/shared/lib/api/payments';
6 import { wait } from '@proton/shared/lib/helpers/promise';
20 } from '@proton/testing';
22 import CreditsModal from './CreditsModal';
23 // eslint-disable-next-line @typescript-eslint/no-unused-vars
24 import PaymentMethodSelector from './methods/PaymentMethodSelector';
26 jest.mock('@proton/components/components/portal/Portal');
28 const createTokenMock = jest.fn((request) => {
29 const type = request?.data?.Payment?.Type ?? '';
31 if (type === 'paypal') {
32 Token = 'paypal-payment-token-123';
33 } else if (type === 'paypal-credit') {
34 Token = 'paypal-credit-payment-token-123';
36 Token = 'payment-token-123';
41 Status: PAYMENT_TOKEN_STATUS.STATUS_CHARGEABLE,
45 const buyCreditUrl = buyCredit({} as any, 'v4').url;
46 const buyCreditMock = jest.fn().mockResolvedValue({});
51 // That's an unresolved issue of jsdom https://github.com/jsdom/jsdom/issues/918
52 (window as any).SVGElement.prototype.getBBox = jest.fn().mockReturnValue({ width: 0 });
54 addApiMock(createTokenV4({} as any).url, createTokenMock);
55 addApiMock(buyCreditUrl, buyCreditMock);
58 mockPaymentMethods().noSaved();
61 const ContextCreditsModal = applyHOCs(
67 withDeprecatedModals(),
71 const status = {} as any;
73 it.skip('should render', () => {
74 const { container } = render(<ContextCreditsModal status={status} open={true} />);
76 expect(container).not.toBeEmptyDOMElement();
79 it.skip('should display the credit card form by default', async () => {
80 const { findByTestId } = render(<ContextCreditsModal status={status} open={true} />);
81 const ccnumber = await findByTestId('ccnumber');
82 expect(ccnumber).toBeTruthy();
85 it.skip('should display the payment method selector', async () => {
86 const { queryByTestId } = render(<ContextCreditsModal status={status} open={true} />);
90 * That's essentially internals of {@link PaymentMethodSelector}
92 expect(queryByTestId('payment-method-selector')).toBeTruthy();
96 function selectMethod(container: HTMLElement, value: string) {
97 const dropdownButton = container.querySelector('#select-method') as HTMLButtonElement;
99 fireEvent.click(dropdownButton);
100 const button = container.querySelector(`button[title="${value}"]`) as HTMLButtonElement;
101 fireEvent.click(button);
105 const input = container.querySelector(`#${value}`) as HTMLInputElement;
109 it.skip('should select the payment method when user clicks it', async () => {
110 const { container, queryByTestId } = render(<ContextCreditsModal status={status} open={true} />);
111 await waitFor(() => {
112 selectMethod(container, 'PayPal');
116 expect(queryByTestId('payment-method-selector')).toHaveTextContent('PayPal');
118 // check that the credit card form is not displayed
119 expect(queryByTestId('ccnumber')).toBeFalsy();
121 expect(queryByTestId('paypal-view')).toBeTruthy();
122 expect(queryByTestId('paypal-button')).toBeTruthy();
124 // switching back to credit card
125 selectMethod(container, 'New credit/debit card');
126 expect(queryByTestId('ccnumber')).toBeTruthy();
127 expect(queryByTestId('paypal-button')).toBeFalsy();
128 expect(queryByTestId('top-up-button')).toBeTruthy();
131 it.skip('should display the credit card form initially', async () => {
132 const { queryByTestId } = render(<ContextCreditsModal status={status} open={true} />);
133 await waitFor(() => {
134 expect(queryByTestId('ccnumber')).toBeTruthy();
138 it.skip('should remember credit card details when switching back and forth', async () => {
139 const { container, getByTestId, findByTestId } = render(<ContextCreditsModal status={status} open={true} />);
140 await waitFor(() => {});
142 const ccnumber = await findByTestId('ccnumber');
143 const exp = getByTestId('exp') as HTMLInputElement;
144 const cvc = getByTestId('cvc') as HTMLInputElement;
146 await userEvent.type(ccnumber, '4242424242424242');
147 await userEvent.type(exp, '1232');
148 await userEvent.type(cvc, '123');
150 // switching to paypal
151 selectMethod(container, 'PayPal');
152 expect(getByTestId('paypal-view')).toBeTruthy();
154 // switching back to credit card
155 selectMethod(container, 'New credit/debit card');
157 expect((getByTestId('ccnumber') as HTMLInputElement).value).toBe('4242 4242 4242 4242');
158 expect((getByTestId('exp') as HTMLInputElement).value).toBe('12/32');
159 expect((getByTestId('cvc') as HTMLInputElement).value).toBe('123');
162 it.skip('should display validation errors after user submits credit card', async () => {
163 const { container, findByTestId, queryByTestId } = render(<ContextCreditsModal status={status} open={true} />);
164 await waitFor(() => {});
165 const ccnumber = queryByTestId('ccnumber') as HTMLInputElement;
166 const exp = queryByTestId('exp') as HTMLInputElement;
167 const cvc = queryByTestId('cvc') as HTMLInputElement;
169 await userEvent.type(ccnumber, '1234567812345678');
170 await userEvent.type(exp, '1212');
171 await userEvent.type(cvc, '123');
173 const cardError = 'Invalid card number';
174 const zipError = 'Invalid postal code';
176 expect(container).not.toHaveTextContent(cardError);
177 expect(container).not.toHaveTextContent(zipError);
179 const topUpButton = await findByTestId('top-up-button');
180 fireEvent.click(topUpButton);
182 expect(container).toHaveTextContent(cardError);
183 expect(container).toHaveTextContent(zipError);
186 // todo: this test is no longer valid after Chargebee migration. For the update, the only viable option seems to
187 // mock the CB iframe response.
188 it.skip('should display invalid expiration date error', async () => {
189 const { container, findByTestId, queryByTestId } = render(<ContextCreditsModal status={status} open={true} />);
190 await waitFor(() => {});
192 const ccnumber = queryByTestId('ccnumber') as HTMLInputElement;
193 const exp = queryByTestId('exp') as HTMLInputElement;
194 const cvc = queryByTestId('cvc') as HTMLInputElement;
196 await userEvent.type(ccnumber, '4242424242424242');
197 await userEvent.type(exp, '1212');
198 await userEvent.type(cvc, '123');
200 const expError = 'Invalid expiration date';
202 expect(container).not.toHaveTextContent(expError);
204 const topUpButton = await findByTestId('top-up-button');
205 fireEvent.click(topUpButton);
207 expect(container).toHaveTextContent(expError);
210 // todo: this test is no longer valid after Chargebee migration. For the update, the only viable option seems to
211 // mock the CB iframe response.
212 it.skip('should create payment token and then buy credits with it', async () => {
213 const onClose = jest.fn();
214 const { findByTestId, queryByTestId } = render(
215 <ContextCreditsModal status={status} open={true} onClose={onClose} />
217 await waitFor(() => {});
219 const ccnumber = queryByTestId('ccnumber') as HTMLInputElement;
220 const exp = queryByTestId('exp') as HTMLInputElement;
221 const cvc = queryByTestId('cvc') as HTMLInputElement;
222 const postalCode = queryByTestId('postalCode') as HTMLInputElement;
224 await userEvent.type(ccnumber, '4242424242424242');
225 await userEvent.type(exp, '1232');
226 await userEvent.type(cvc, '123');
227 await userEvent.type(postalCode, '11111');
229 const topUpButton = await findByTestId('top-up-button');
230 fireEvent.click(topUpButton);
232 await waitFor(() => {
233 expect(buyCreditMock).toHaveBeenCalled();
236 await waitFor(() => {
237 expect(buyCreditMock).toHaveBeenCalledWith(
238 expect.objectContaining({
239 data: expect.objectContaining({
240 Payment: expect.objectContaining({
242 Details: expect.objectContaining({
243 Token: 'payment-token-123',
255 await waitFor(() => {
256 expect(mockEventManager.call).toHaveBeenCalled();
258 await waitFor(() => {
259 expect(onClose).toHaveBeenCalled();
263 // todo: this test is no longer valid after Chargebee migration. For the update, the only viable option seems to
264 // mock the CB iframe response.
265 it.skip('should create payment token and then buy credits with it - custom amount', async () => {
266 const onClose = jest.fn();
267 const { findByTestId, queryByTestId } = render(
268 <ContextCreditsModal status={status} open={true} onClose={onClose} />
270 await waitFor(() => {});
272 const otherAmountInput = queryByTestId('other-amount') as HTMLInputElement;
273 await userEvent.type(otherAmountInput, '123');
275 const ccnumber = queryByTestId('ccnumber') as HTMLInputElement;
276 const exp = queryByTestId('exp') as HTMLInputElement;
277 const cvc = queryByTestId('cvc') as HTMLInputElement;
278 const postalCode = queryByTestId('postalCode') as HTMLInputElement;
280 await userEvent.type(ccnumber, '4242424242424242');
281 await userEvent.type(exp, '1232');
282 await userEvent.type(cvc, '123');
283 await userEvent.type(postalCode, '11111');
286 const topUpButton = await findByTestId('top-up-button');
287 fireEvent.click(topUpButton);
289 await waitFor(() => {
290 expect(buyCreditMock).toHaveBeenCalled();
293 await waitFor(() => {
294 expect(buyCreditMock).toHaveBeenCalledWith(
295 expect.objectContaining({
296 data: expect.objectContaining({
297 Payment: expect.objectContaining({
299 Details: expect.objectContaining({
300 Token: 'payment-token-123',
312 await waitFor(() => {
313 expect(mockEventManager.call).toHaveBeenCalled();
315 await waitFor(() => {
316 expect(onClose).toHaveBeenCalled();
320 it.skip('should create payment token for paypal and then buy credits with it', async () => {
321 const onClose = jest.fn();
322 const { container, queryByTestId } = render(<ContextCreditsModal status={status} open={true} onClose={onClose} />);
323 await waitFor(() => {
324 selectMethod(container, 'PayPal');
327 const paypalButton = queryByTestId('paypal-button') as HTMLButtonElement;
329 fireEvent.click(paypalButton);
331 await waitFor(() => {
332 expect(buyCreditMock).toHaveBeenCalled();
335 await waitFor(() => {
336 expect(buyCreditMock).toHaveBeenCalledWith(
337 expect.objectContaining({
338 data: expect.objectContaining({
339 Payment: expect.objectContaining({
341 Details: expect.objectContaining({
342 Token: 'paypal-payment-token-123',
355 await waitFor(() => {
356 expect(mockEventManager.call).toHaveBeenCalled();
358 await waitFor(() => {
359 expect(onClose).toHaveBeenCalled();
363 it.skip('should create payment token for paypal and then buy credits with it - custom amount', async () => {
364 const onClose = jest.fn();
365 const { container, queryByTestId } = render(<ContextCreditsModal status={status} open={true} onClose={onClose} />);
366 await waitFor(() => {
367 selectMethod(container, 'PayPal');
370 const otherAmountInput = queryByTestId('other-amount') as HTMLInputElement;
371 await userEvent.type(otherAmountInput, '123');
374 const paypalButton = queryByTestId('paypal-button') as HTMLButtonElement;
375 fireEvent.click(paypalButton);
377 await waitFor(() => {
378 expect(buyCreditMock).toHaveBeenCalled();
381 await waitFor(() => {
382 expect(buyCreditMock).toHaveBeenCalledWith(
383 expect.objectContaining({
384 data: expect.objectContaining({
385 Payment: expect.objectContaining({
387 Details: expect.objectContaining({
388 Token: 'paypal-payment-token-123',
401 await waitFor(() => {
402 expect(mockEventManager.call).toHaveBeenCalled();
404 await waitFor(() => {
405 expect(onClose).toHaveBeenCalled();
409 it.skip('should disable paypal button while the amount is debouncing', async () => {
410 const onClose = jest.fn();
411 const { container, queryByTestId } = render(<ContextCreditsModal status={status} open={true} onClose={onClose} />);
412 await waitFor(() => {
413 selectMethod(container, 'PayPal');
416 const otherAmountInput = queryByTestId('other-amount') as HTMLInputElement;
417 await userEvent.type(otherAmountInput, '123');
418 expect(queryByTestId('paypal-button')).toBeDisabled();
419 expect(queryByTestId('paypal-credit-button')).toBeDisabled();
422 expect(queryByTestId('paypal-button')).not.toBeDisabled();
423 expect(queryByTestId('paypal-credit-button')).not.toBeDisabled();
426 it.skip('should disable paypal button if the amount is too high', async () => {
427 const onClose = jest.fn();
428 const { container, queryByTestId } = render(<ContextCreditsModal status={status} open={true} onClose={onClose} />);
429 await waitFor(() => {
430 selectMethod(container, 'PayPal');
433 const otherAmountInput = queryByTestId('other-amount') as HTMLInputElement;
434 await userEvent.type(otherAmountInput, '40001');
437 expect(queryByTestId('paypal-button')).toBeDisabled();
438 expect(queryByTestId('paypal-credit-button')).toBeDisabled();
440 await userEvent.clear(otherAmountInput);
441 await userEvent.type(otherAmountInput, '40000');
444 expect(queryByTestId('paypal-button')).not.toBeDisabled();
445 expect(queryByTestId('paypal-credit-button')).not.toBeDisabled();
448 it.skip('should create payment token for paypal-credit and then buy credits with it', async () => {
449 const onClose = jest.fn();
450 const { container, queryByTestId } = render(<ContextCreditsModal status={status} open={true} onClose={onClose} />);
451 await waitFor(() => {
452 selectMethod(container, 'PayPal');
455 const paypalCreditButton = queryByTestId('paypal-credit-button') as HTMLButtonElement;
457 fireEvent.click(paypalCreditButton);
459 await waitFor(() => {
460 expect(buyCreditMock).toHaveBeenCalled();
463 await waitFor(() => {
464 expect(buyCreditMock).toHaveBeenCalledWith(
465 expect.objectContaining({
466 data: expect.objectContaining({
467 Payment: expect.objectContaining({
469 Details: expect.objectContaining({
470 Token: 'paypal-credit-payment-token-123',
475 type: 'paypal-credit',
483 await waitFor(() => {
484 expect(mockEventManager.call).toHaveBeenCalled();
486 await waitFor(() => {
487 expect(onClose).toHaveBeenCalled();
491 const paypalPayerId = 'AAAAAAAAAAAAA';
492 const paypalShort = `PayPal - ${paypalPayerId}`;
494 it.skip('should display the saved credit cards', async () => {
495 mockPaymentMethods().noSaved().withCard({
505 const { container, queryByTestId } = render(<ContextCreditsModal status={status} open={true} />);
507 await waitFor(() => {
508 expect(container.querySelector('#select-method')).toBeTruthy();
510 await waitFor(() => {
511 expect(queryByTestId('existing-credit-card')).toBeTruthy();
514 expect(container).toHaveTextContent('Visa ending in 4242');
515 expect(container).toHaveTextContent('•••• •••• •••• 4242');
516 expect(container).toHaveTextContent('John Smith');
517 expect(container).toHaveTextContent('01/2025');
520 it.skip('should display the saved paypal account', async () => {
521 mockPaymentMethods().noSaved().withPaypal({
522 BillingAgreementID: 'B-22222222222222222',
523 PayerID: paypalPayerId,
527 const { container, queryByTestId } = render(<ContextCreditsModal status={status} open={true} />);
529 await waitFor(() => {
530 expect(container.querySelector('#select-method')).toBeTruthy();
531 selectMethod(container, paypalShort);
533 expect(queryByTestId('existing-paypal')).toBeTruthy();
535 expect(container).toHaveTextContent('PayPal - AAAAAAAAAAAAA');
538 it.skip('should create payment token for saved card and then buy credits with it', async () => {
539 mockPaymentMethods().noSaved().withCard({
549 const onClose = jest.fn();
550 const { container, findByTestId } = render(<ContextCreditsModal status={status} open={true} onClose={onClose} />);
551 await waitFor(() => {
552 selectMethod(container, 'Visa ending in 4242');
554 const topUpButton = await findByTestId('top-up-button');
555 fireEvent.click(topUpButton);
557 await waitFor(() => {
558 expect(buyCreditMock).toHaveBeenCalled();
560 await waitFor(() => {
561 expect(buyCreditMock).toHaveBeenCalledWith(
562 expect.objectContaining({
563 data: expect.objectContaining({
564 Payment: expect.objectContaining({
566 Details: expect.objectContaining({
567 Token: 'payment-token-123',
579 await waitFor(() => {
580 expect(mockEventManager.call).toHaveBeenCalled();
582 await waitFor(() => {
583 expect(onClose).toHaveBeenCalled();
587 it.skip('should create payment token for saved paypal and then buy credits with it', async () => {
588 mockPaymentMethods().noSaved().withPaypal({
589 BillingAgreementID: 'B-22222222222222222',
590 PayerID: paypalPayerId,
594 const onClose = jest.fn();
595 const { container, findByTestId } = render(<ContextCreditsModal status={status} open={true} onClose={onClose} />);
597 await waitFor(() => {
598 selectMethod(container, paypalShort);
601 const topUpButton = await findByTestId('top-up-button');
602 fireEvent.click(topUpButton);
604 await waitFor(() => {
605 expect(buyCreditMock).toHaveBeenCalled();
607 await waitFor(() => {
608 expect(buyCreditMock).toHaveBeenCalledWith(
609 expect.objectContaining({
610 data: expect.objectContaining({
611 Payment: expect.objectContaining({
613 Details: expect.objectContaining({
614 // The saved paypal method isn't handled by paypal hook.
615 // It's handled by the same hook as the saved credit card.
616 // That's why the mocked token isn't taken from the paypal mock this time.
617 Token: 'payment-token-123',
629 await waitFor(() => {
630 expect(mockEventManager.call).toHaveBeenCalled();
632 await waitFor(() => {
633 expect(onClose).toHaveBeenCalled();