Remove payments API routing initialization
[ProtonMail-WebClient.git] / packages / components / containers / contacts / edit / ContactEditModal.test.tsx
blobf8cfc4d228ca96538f33c2139544406938efe12b
1 import { screen, waitFor } from '@testing-library/react';
2 import userEvent from '@testing-library/user-event';
4 import * as userHooks from '@proton/account/user/hooks';
5 import { CryptoProxy } from '@proton/crypto';
6 import * as useContactEmailHooks from '@proton/mail/contactEmails/hooks';
7 import * as mailLabelHooks from '@proton/mail/labels/hooks';
8 import * as mailSettingsHooks from '@proton/mail/mailSettings/hooks';
9 import { API_CODES, CONTACT_CARD_TYPE } from '@proton/shared/lib/constants';
10 import { parseToVCard } from '@proton/shared/lib/contacts/vcard';
11 import type { MailSettings, UserModel } from '@proton/shared/lib/interfaces';
12 import type { ContactEmail, ContactGroup } from '@proton/shared/lib/interfaces/contacts';
13 import { addApiMock } from '@proton/testing';
15 import { clearAll, minimalCache, mockedCryptoApi, notificationManager, renderWithProviders } from '../tests/render';
16 import type { ContactEditModalProps, ContactEditProps } from './ContactEditModal';
17 import ContactEditModal from './ContactEditModal';
19 jest.mock('../../../hooks/useAuthentication', () => ({
20     __esModule: true,
21     default: jest.fn(() => ({
22         getUID: jest.fn(),
23     })),
24 }));
26 jest.mock('../../../hooks/useConfig', () => () => ({ API_URL: 'api' }));
28 jest.mock('@proton/mail/contactEmails/hooks', () => ({
29     __esModule: true,
30     ...jest.requireActual('@proton/mail/contactEmails/hooks'),
31 }));
32 jest.mock('@proton/mail/labels/hooks', () => ({
33     __esModule: true,
34     ...jest.requireActual('@proton/mail/labels/hooks'),
35 }));
36 jest.mock('@proton/account/user/hooks', () => ({
37     __esModule: true,
38     ...jest.requireActual('@proton/account/user/hooks'),
39 }));
40 jest.mock('@proton/mail/mailSettings/hooks', () => ({
41     __esModule: true,
42     ...jest.requireActual('@proton/mail/mailSettings/hooks'),
43 }));
45 jest.mock('@proton/shared/lib/helpers/image.ts', () => ({
46     toImage: (src: string) => ({ src }),
47 }));
49 const setupApiMocks = () => {
50     const saveRequestSpy = jest.fn();
51     addApiMock('contacts/v4/contacts/emails/label', (args) => {
52         saveRequestSpy(args.data);
53         return { ContactEmailIDs: [], Responses: [{ Response: { Code: API_CODES.SINGLE_SUCCESS } }] };
54     });
55     addApiMock('contacts/v4/contacts', (args) => {
56         saveRequestSpy(args.data);
57         return { Contacts: [], Responses: [{ Response: { Code: API_CODES.SINGLE_SUCCESS } }] };
58     });
59     addApiMock('contacts/v4/contacts/emails', () => {
60         return { ContactEmails: [] };
61     });
62     addApiMock('contacts/v4/contacts/ContactID', (args) => {
63         saveRequestSpy(args.data);
64         return { Code: API_CODES.SINGLE_SUCCESS };
65     });
66     return saveRequestSpy;
69 const props: ContactEditProps & ContactEditModalProps = {
70     contactID: 'ContactID',
71     vCardContact: { fn: [] },
72     onUpgrade: jest.fn(),
73     onSelectImage: jest.fn(),
74     onGroupEdit: jest.fn(),
75     onLimitReached: jest.fn(),
78 const groupName = 'Test group';
79 const mockContactGroup = [
80     {
81         ID: '5aBZh7oqeAmyL-5p3gggA_Ji5r0m3vfbGruq7dqE8fw7FjskOWBAWrY4X8o62RqlguaTLRMezIP7Q_C8B9Wy8Q==',
82         Name: groupName,
83         Path: groupName,
84         Type: 2,
85         Color: '#54473f',
86         Order: 1,
87         Notify: 0,
88         Exclusive: 0,
89         Expanded: 0,
90         Sticky: 0,
91         Display: 1,
92     } as ContactGroup,
94 const testEmail = 'testy@mctestface.com';
96 describe('ContactEditModal', () => {
97     beforeAll(() => {
98         CryptoProxy.setEndpoint(mockedCryptoApi);
99     });
101     beforeEach(() => {
102         clearAll();
103     });
105     afterAll(async () => {
106         await CryptoProxy.releaseEndpoint();
107     });
109     it('should prefill all fields with contact values', async () => {
110         const vcard = `BEGIN:VCARD
111 VERSION:4.0
112 UID:urn:uuid:4fbe8971-0bc3-424c-9c26-36c3e1eff6b1
113 FN:J. Doe
114 FN:FN2
115 EMAIL:jdoe@example.com
116 NOTE:TestNote
117 ADR:1;2;3;4;5;6;7
118 ADR:;;;;;;testadr
119 TEL:testtel
120 PHOTO:https://example.com/myphoto.jpg
121 END:VCARD`;
123         const vCardContact = parseToVCard(vcard);
125         minimalCache();
126         setupApiMocks();
128         renderWithProviders(<ContactEditModal open={true} {...props} vCardContact={vCardContact} />);
130         screen.getByDisplayValue('J. Doe');
131         // Wait for image to be loaded
132         await waitFor(() => {
133             expect(screen.getByRole('presentation')).toHaveAttribute('src', 'https://example.com/myphoto.jpg');
134         });
135         screen.getByDisplayValue('FN2');
136         screen.getByDisplayValue('jdoe@example.com');
137         screen.getByDisplayValue('testtel');
138         screen.getByDisplayValue('1');
139         screen.getByDisplayValue('4');
140         screen.getByDisplayValue('5');
141         screen.getByDisplayValue('6');
142         screen.getByDisplayValue('7');
143         screen.getByDisplayValue('testadr');
144         screen.getByDisplayValue('TestNote');
145     });
147     it('should update basic properties', async () => {
148         const vcard = `BEGIN:VCARD
149 VERSION:4.0
150 UID:urn:uuid:4fbe8971-0bc3-424c-9c26-36c3e1eff6b1
151 FN:J. Doe
152 EMAIL:jdoe@example.com
153 TEL:testtel
154 NOTE:TestNote
155 END:VCARD`;
157         const vCardContact = parseToVCard(vcard);
159         minimalCache();
160         const saveRequestSpy = setupApiMocks();
162         renderWithProviders(<ContactEditModal open={true} {...props} vCardContact={vCardContact} />);
164         const name = screen.getByDisplayValue('J. Doe');
165         await userEvent.clear(name);
166         await userEvent.type(name, 'New name');
168         const email = screen.getByDisplayValue('jdoe@example.com');
169         await userEvent.clear(email);
170         await userEvent.type(email, 'new@email.com');
172         const tel = screen.getByDisplayValue('testtel');
173         await userEvent.clear(tel);
174         await userEvent.type(tel, 'newtel');
176         const note = screen.getByDisplayValue('TestNote');
177         await userEvent.clear(note);
178         await userEvent.type(note, 'NewNote');
180         await userEvent.click(screen.getByTestId('add-other'));
181         await userEvent.click(screen.getByTestId('create-contact:other-info-select'));
182         await userEvent.click(screen.getByTestId('create-contact:dropdown-item-Title'));
183         await userEvent.type(screen.getByTestId('Title'), 'NewTitle');
185         await userEvent.click(screen.getByRole('button', { name: 'Save' }));
187         await waitFor(() => {
188             expect(notificationManager.createNotification).toHaveBeenCalled();
189         });
191         const sentData = saveRequestSpy.mock.calls[0][0];
192         const cards = sentData.Cards;
194         const signedCardContent = cards.find(
195             ({ Type }: { Type: CONTACT_CARD_TYPE }) => Type === CONTACT_CARD_TYPE.SIGNED
196         ).Data;
198         const encryptedCardContent = cards.find(
199             ({ Type }: { Type: CONTACT_CARD_TYPE }) => Type === CONTACT_CARD_TYPE.ENCRYPTED_AND_SIGNED
200         ).Data;
202         expect(signedCardContent.includes('VERSION:4.0')).toBe(true);
203         expect(signedCardContent.includes('UID:urn:uuid:4fbe8971-0bc3-424c-9c26-36c3e1eff6b1')).toBe(true);
204         expect(signedCardContent.includes('FN;PREF=1:New name')).toBe(true);
205         expect(signedCardContent.includes('ITEM1.EMAIL;PREF=1:new@email.com')).toBe(true);
206         expect(signedCardContent.includes('END:VCARD')).toBe(true);
208         expect(encryptedCardContent.includes('VERSION:4.0')).toBe(true);
209         expect(encryptedCardContent.includes('TEL;PREF=1:newtel')).toBe(true);
210         expect(encryptedCardContent.includes('NOTE:NewNote')).toBe(true);
211         expect(encryptedCardContent.includes('TITLE:NewTitle')).toBe(true);
212         expect(encryptedCardContent.includes('N:;;;;')).toBe(true);
213         expect(encryptedCardContent.includes('END:VCARD')).toBe(true);
214     });
216     it('should create a contact', async () => {
217         minimalCache();
218         const saveRequestSpy = setupApiMocks();
219         renderWithProviders(<ContactEditModal open={true} {...props} />);
221         const firstName = screen.getByTestId('First name');
222         await userEvent.type(firstName, 'Bruno');
224         const lastName = screen.getByTestId('Last name');
225         await userEvent.type(lastName, 'Mars');
227         const displayName = screen.getByTestId('Enter a display name or nickname');
228         await userEvent.type(displayName, 'New name');
230         const email = screen.getByTestId('Email');
231         await userEvent.type(email, 'new@email.com');
233         const saveButton = screen.getByRole('button', { name: 'Save' });
234         await userEvent.click(saveButton);
236         await waitFor(() => expect(notificationManager.createNotification).toHaveBeenCalled());
238         const sentData = saveRequestSpy.mock.calls[0][0];
239         const cards = sentData.Cards;
241         const signedCardContent = cards.find(
242             ({ Type }: { Type: CONTACT_CARD_TYPE }) => Type === CONTACT_CARD_TYPE.SIGNED
243         ).Data;
245         const encryptedCardContent = cards.find(
246             ({ Type }: { Type: CONTACT_CARD_TYPE }) => Type === CONTACT_CARD_TYPE.ENCRYPTED_AND_SIGNED
247         ).Data;
249         expect(signedCardContent).toContain('FN;PREF=1:New name');
250         expect(signedCardContent).toContain('ITEM1.EMAIL;PREF=1:new@email.com');
251         expect(encryptedCardContent).toContain('N:Mars;Bruno;;;');
252     });
254     it('should trigger an error if all of the name fields are empty when creating a contact', async () => {
255         setupApiMocks();
256         renderWithProviders(<ContactEditModal open={true} {...props} />);
258         const saveButton = screen.getByRole('button', { name: 'Save' });
259         await userEvent.click(saveButton);
261         const errorZone = screen.getByText('Please provide either a first name, a last name or a display name');
262         expect(errorZone).toBeVisible();
263     });
265     it('should trigger an error if display name is empty when editing a contact', async () => {
266         const vcard = `BEGIN:VCARD
267 VERSION:4.0
268 UID:urn:uuid:4fbe8971-0bc3-424c-9c26-36c3e1eff6b1
269 EMAIL:jdoe@example.com
270 END:VCARD`;
272         setupApiMocks();
273         const vCardContact = parseToVCard(vcard);
275         renderWithProviders(<ContactEditModal open={true} {...props} vCardContact={vCardContact} />);
277         const saveButton = screen.getByRole('button', { name: 'Save' });
278         await userEvent.click(saveButton);
280         const errorZone = screen.getByText('This field is required');
281         expect(errorZone).toBeVisible();
282     });
284     it('should add user to a group at creation', async () => {
285         // Mocking is a code smell (https://medium.com/javascript-scene/mocking-is-a-code-smell-944a70c90a6a)
286         // but I don't think we could test this without these mocks.
287         jest.spyOn(mailLabelHooks, 'useContactGroups').mockImplementation(() => [mockContactGroup, true]);
288         jest.spyOn(useContactEmailHooks, 'useContactEmails').mockReturnValue([
289             [{ Email: testEmail, Name: 'Testy McTestFace' } as ContactEmail],
290             false,
291         ]);
292         jest.spyOn(mailSettingsHooks, 'useMailSettings').mockImplementation(() => [
293             { RecipientLimit: 100 } as MailSettings,
294             true,
295         ]);
296         jest.spyOn(userHooks, 'useUser').mockImplementation(() => [{ hasPaidMail: true } as UserModel, true]);
298         const saveRequestSpy = setupApiMocks();
300         renderWithProviders(<ContactEditModal open={true} {...{ ...props, contactID: '' }} />);
302         await userEvent.type(screen.getByTestId('First name'), 'Testy');
303         await userEvent.type(screen.getByTestId('Last name'), 'McTestFace');
304         await userEvent.type(screen.getByTestId('Email'), testEmail);
305         await userEvent.click(screen.getByRole('button', { name: 'Contact group' }));
306         await userEvent.click(screen.getByRole('checkbox', { name: groupName }));
307         await userEvent.click(screen.getByRole('button', { name: 'Apply' }));
308         await userEvent.click(screen.getByRole('button', { name: 'Save' }));
310         await waitFor(() => {
311             expect(notificationManager.createNotification).toHaveBeenCalled();
312         });
314         // The first call, to the contacts endpoint to save the contact, always happens.
315         // The second call only happens when adding a user to a group. Therefore, checking
316         // the call length is 2 is enough to verify that the user has been added to a group.
317         expect(saveRequestSpy.mock.calls).toHaveLength(2);
318     });