Cleanup - unused files / unused exports / duplicate exports
[ProtonMail-WebClient.git] / packages / components / containers / payments / CreditsModal.test.tsx
blob784d80be9cd12e05a002aad4263e0c3ab3ba7c86
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';
7 import {
8     addApiMock,
9     applyHOCs,
10     mockEventManager,
11     mockPaymentMethods,
12     mockPaymentStatus,
13     withApi,
14     withAuthentication,
15     withCache,
16     withConfig,
17     withDeprecatedModals,
18     withEventManager,
19     withNotifications,
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 ?? '';
30     let Token: string;
31     if (type === 'paypal') {
32         Token = 'paypal-payment-token-123';
33     } else if (type === 'paypal-credit') {
34         Token = 'paypal-credit-payment-token-123';
35     } else {
36         Token = 'payment-token-123';
37     }
39     return {
40         Token,
41         Status: PAYMENT_TOKEN_STATUS.STATUS_CHARGEABLE,
42     };
43 });
45 const buyCreditUrl = buyCredit({} as any, 'v4').url;
46 const buyCreditMock = jest.fn().mockResolvedValue({});
48 beforeEach(() => {
49     jest.clearAllMocks();
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);
57     mockPaymentStatus();
58     mockPaymentMethods().noSaved();
59 });
61 const ContextCreditsModal = applyHOCs(
62     withConfig(),
63     withNotifications(),
64     withEventManager(),
65     withApi(),
66     withCache(),
67     withDeprecatedModals(),
68     withAuthentication()
69 )(CreditsModal);
71 const status = {} as any;
73 it.skip('should render', () => {
74     const { container } = render(<ContextCreditsModal status={status} open={true} />);
76     expect(container).not.toBeEmptyDOMElement();
77 });
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();
83 });
85 it.skip('should display the payment method selector', async () => {
86     const { queryByTestId } = render(<ContextCreditsModal status={status} open={true} />);
88     await waitFor(() => {
89         /**
90          * That's essentially internals of {@link PaymentMethodSelector}
91          **/
92         expect(queryByTestId('payment-method-selector')).toBeTruthy();
93     });
94 });
96 function selectMethod(container: HTMLElement, value: string) {
97     const dropdownButton = container.querySelector('#select-method') as HTMLButtonElement;
98     if (dropdownButton) {
99         fireEvent.click(dropdownButton);
100         const button = container.querySelector(`button[title="${value}"]`) as HTMLButtonElement;
101         fireEvent.click(button);
102         return;
103     }
105     const input = container.querySelector(`#${value}`) as HTMLInputElement;
106     input.click();
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');
113     });
115     // secondary check
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();
135     });
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} />
216     );
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();
234     });
236     await waitFor(() => {
237         expect(buyCreditMock).toHaveBeenCalledWith(
238             expect.objectContaining({
239                 data: expect.objectContaining({
240                     Payment: expect.objectContaining({
241                         Type: 'token',
242                         Details: expect.objectContaining({
243                             Token: 'payment-token-123',
244                         }),
245                     }),
246                     Amount: 5000,
247                     Currency: 'EUR',
248                 }),
249                 method: 'post',
250                 url: buyCreditUrl,
251             })
252         );
253     });
255     await waitFor(() => {
256         expect(mockEventManager.call).toHaveBeenCalled();
257     });
258     await waitFor(() => {
259         expect(onClose).toHaveBeenCalled();
260     });
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} />
269     );
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');
285     await wait(500);
286     const topUpButton = await findByTestId('top-up-button');
287     fireEvent.click(topUpButton);
289     await waitFor(() => {
290         expect(buyCreditMock).toHaveBeenCalled();
291     });
293     await waitFor(() => {
294         expect(buyCreditMock).toHaveBeenCalledWith(
295             expect.objectContaining({
296                 data: expect.objectContaining({
297                     Payment: expect.objectContaining({
298                         Type: 'token',
299                         Details: expect.objectContaining({
300                             Token: 'payment-token-123',
301                         }),
302                     }),
303                     Amount: 12300,
304                     Currency: 'EUR',
305                 }),
306                 method: 'post',
307                 url: buyCreditUrl,
308             })
309         );
310     });
312     await waitFor(() => {
313         expect(mockEventManager.call).toHaveBeenCalled();
314     });
315     await waitFor(() => {
316         expect(onClose).toHaveBeenCalled();
317     });
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');
325     });
327     const paypalButton = queryByTestId('paypal-button') as HTMLButtonElement;
329     fireEvent.click(paypalButton);
331     await waitFor(() => {
332         expect(buyCreditMock).toHaveBeenCalled();
333     });
335     await waitFor(() => {
336         expect(buyCreditMock).toHaveBeenCalledWith(
337             expect.objectContaining({
338                 data: expect.objectContaining({
339                     Payment: expect.objectContaining({
340                         Type: 'token',
341                         Details: expect.objectContaining({
342                             Token: 'paypal-payment-token-123',
343                         }),
344                     }),
345                     Amount: 5000,
346                     Currency: 'EUR',
347                     type: 'paypal',
348                 }),
349                 method: 'post',
350                 url: buyCreditUrl,
351             })
352         );
353     });
355     await waitFor(() => {
356         expect(mockEventManager.call).toHaveBeenCalled();
357     });
358     await waitFor(() => {
359         expect(onClose).toHaveBeenCalled();
360     });
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');
368     });
370     const otherAmountInput = queryByTestId('other-amount') as HTMLInputElement;
371     await userEvent.type(otherAmountInput, '123');
373     await wait(1000);
374     const paypalButton = queryByTestId('paypal-button') as HTMLButtonElement;
375     fireEvent.click(paypalButton);
377     await waitFor(() => {
378         expect(buyCreditMock).toHaveBeenCalled();
379     });
381     await waitFor(() => {
382         expect(buyCreditMock).toHaveBeenCalledWith(
383             expect.objectContaining({
384                 data: expect.objectContaining({
385                     Payment: expect.objectContaining({
386                         Type: 'token',
387                         Details: expect.objectContaining({
388                             Token: 'paypal-payment-token-123',
389                         }),
390                     }),
391                     Amount: 12300,
392                     Currency: 'EUR',
393                     type: 'paypal',
394                 }),
395                 method: 'post',
396                 url: buyCreditUrl,
397             })
398         );
399     });
401     await waitFor(() => {
402         expect(mockEventManager.call).toHaveBeenCalled();
403     });
404     await waitFor(() => {
405         expect(onClose).toHaveBeenCalled();
406     });
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');
414     });
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();
421     await wait(1000);
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');
431     });
433     const otherAmountInput = queryByTestId('other-amount') as HTMLInputElement;
434     await userEvent.type(otherAmountInput, '40001');
436     await wait(1000);
437     expect(queryByTestId('paypal-button')).toBeDisabled();
438     expect(queryByTestId('paypal-credit-button')).toBeDisabled();
440     await userEvent.clear(otherAmountInput);
441     await userEvent.type(otherAmountInput, '40000');
442     await wait(1000);
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');
453     });
455     const paypalCreditButton = queryByTestId('paypal-credit-button') as HTMLButtonElement;
457     fireEvent.click(paypalCreditButton);
459     await waitFor(() => {
460         expect(buyCreditMock).toHaveBeenCalled();
461     });
463     await waitFor(() => {
464         expect(buyCreditMock).toHaveBeenCalledWith(
465             expect.objectContaining({
466                 data: expect.objectContaining({
467                     Payment: expect.objectContaining({
468                         Type: 'token',
469                         Details: expect.objectContaining({
470                             Token: 'paypal-credit-payment-token-123',
471                         }),
472                     }),
473                     Amount: 5000,
474                     Currency: 'EUR',
475                     type: 'paypal-credit',
476                 }),
477                 method: 'post',
478                 url: buyCreditUrl,
479             })
480         );
481     });
483     await waitFor(() => {
484         expect(mockEventManager.call).toHaveBeenCalled();
485     });
486     await waitFor(() => {
487         expect(onClose).toHaveBeenCalled();
488     });
491 const paypalPayerId = 'AAAAAAAAAAAAA';
492 const paypalShort = `PayPal - ${paypalPayerId}`;
494 it.skip('should display the saved credit cards', async () => {
495     mockPaymentMethods().noSaved().withCard({
496         Last4: '4242',
497         Brand: 'Visa',
498         ExpMonth: '01',
499         ExpYear: '2025',
500         Name: 'John Smith',
501         Country: 'US',
502         ZIP: '11111',
503     });
505     const { container, queryByTestId } = render(<ContextCreditsModal status={status} open={true} />);
507     await waitFor(() => {
508         expect(container.querySelector('#select-method')).toBeTruthy();
509     });
510     await waitFor(() => {
511         expect(queryByTestId('existing-credit-card')).toBeTruthy();
512     });
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,
524         Payer: '',
525     });
527     const { container, queryByTestId } = render(<ContextCreditsModal status={status} open={true} />);
529     await waitFor(() => {
530         expect(container.querySelector('#select-method')).toBeTruthy();
531         selectMethod(container, paypalShort);
532     });
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({
540         Last4: '4242',
541         Brand: 'Visa',
542         ExpMonth: '01',
543         ExpYear: '2025',
544         Name: 'John Smith',
545         Country: 'US',
546         ZIP: '11111',
547     });
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');
553     });
554     const topUpButton = await findByTestId('top-up-button');
555     fireEvent.click(topUpButton);
557     await waitFor(() => {
558         expect(buyCreditMock).toHaveBeenCalled();
559     });
560     await waitFor(() => {
561         expect(buyCreditMock).toHaveBeenCalledWith(
562             expect.objectContaining({
563                 data: expect.objectContaining({
564                     Payment: expect.objectContaining({
565                         Type: 'token',
566                         Details: expect.objectContaining({
567                             Token: 'payment-token-123',
568                         }),
569                     }),
570                     Amount: 5000,
571                     Currency: 'EUR',
572                 }),
573                 method: 'post',
574                 url: buyCreditUrl,
575             })
576         );
577     });
579     await waitFor(() => {
580         expect(mockEventManager.call).toHaveBeenCalled();
581     });
582     await waitFor(() => {
583         expect(onClose).toHaveBeenCalled();
584     });
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,
591         Payer: '',
592     });
594     const onClose = jest.fn();
595     const { container, findByTestId } = render(<ContextCreditsModal status={status} open={true} onClose={onClose} />);
597     await waitFor(() => {
598         selectMethod(container, paypalShort);
599     });
601     const topUpButton = await findByTestId('top-up-button');
602     fireEvent.click(topUpButton);
604     await waitFor(() => {
605         expect(buyCreditMock).toHaveBeenCalled();
606     });
607     await waitFor(() => {
608         expect(buyCreditMock).toHaveBeenCalledWith(
609             expect.objectContaining({
610                 data: expect.objectContaining({
611                     Payment: expect.objectContaining({
612                         Type: 'token',
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',
618                         }),
619                     }),
620                     Amount: 5000,
621                     Currency: 'EUR',
622                 }),
623                 method: 'post',
624                 url: buyCreditUrl,
625             })
626         );
627     });
629     await waitFor(() => {
630         expect(mockEventManager.call).toHaveBeenCalled();
631     });
632     await waitFor(() => {
633         expect(onClose).toHaveBeenCalled();
634     });