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', () => ({
21 default: jest.fn(() => ({
26 jest.mock('../../../hooks/useConfig', () => () => ({ API_URL: 'api' }));
28 jest.mock('@proton/mail/contactEmails/hooks', () => ({
30 ...jest.requireActual('@proton/mail/contactEmails/hooks'),
32 jest.mock('@proton/mail/labels/hooks', () => ({
34 ...jest.requireActual('@proton/mail/labels/hooks'),
36 jest.mock('@proton/account/user/hooks', () => ({
38 ...jest.requireActual('@proton/account/user/hooks'),
40 jest.mock('@proton/mail/mailSettings/hooks', () => ({
42 ...jest.requireActual('@proton/mail/mailSettings/hooks'),
45 jest.mock('@proton/shared/lib/helpers/image.ts', () => ({
46 toImage: (src: string) => ({ src }),
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 } }] };
55 addApiMock('contacts/v4/contacts', (args) => {
56 saveRequestSpy(args.data);
57 return { Contacts: [], Responses: [{ Response: { Code: API_CODES.SINGLE_SUCCESS } }] };
59 addApiMock('contacts/v4/contacts/emails', () => {
60 return { ContactEmails: [] };
62 addApiMock('contacts/v4/contacts/ContactID', (args) => {
63 saveRequestSpy(args.data);
64 return { Code: API_CODES.SINGLE_SUCCESS };
66 return saveRequestSpy;
69 const props: ContactEditProps & ContactEditModalProps = {
70 contactID: 'ContactID',
71 vCardContact: { fn: [] },
73 onSelectImage: jest.fn(),
74 onGroupEdit: jest.fn(),
75 onLimitReached: jest.fn(),
78 const groupName = 'Test group';
79 const mockContactGroup = [
81 ID: '5aBZh7oqeAmyL-5p3gggA_Ji5r0m3vfbGruq7dqE8fw7FjskOWBAWrY4X8o62RqlguaTLRMezIP7Q_C8B9Wy8Q==',
94 const testEmail = 'testy@mctestface.com';
96 describe('ContactEditModal', () => {
98 CryptoProxy.setEndpoint(mockedCryptoApi);
105 afterAll(async () => {
106 await CryptoProxy.releaseEndpoint();
109 it('should prefill all fields with contact values', async () => {
110 const vcard = `BEGIN:VCARD
112 UID:urn:uuid:4fbe8971-0bc3-424c-9c26-36c3e1eff6b1
115 EMAIL:jdoe@example.com
120 PHOTO:https://example.com/myphoto.jpg
123 const vCardContact = parseToVCard(vcard);
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');
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');
147 it('should update basic properties', async () => {
148 const vcard = `BEGIN:VCARD
150 UID:urn:uuid:4fbe8971-0bc3-424c-9c26-36c3e1eff6b1
152 EMAIL:jdoe@example.com
157 const vCardContact = parseToVCard(vcard);
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();
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
198 const encryptedCardContent = cards.find(
199 ({ Type }: { Type: CONTACT_CARD_TYPE }) => Type === CONTACT_CARD_TYPE.ENCRYPTED_AND_SIGNED
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);
216 it('should create a contact', async () => {
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
245 const encryptedCardContent = cards.find(
246 ({ Type }: { Type: CONTACT_CARD_TYPE }) => Type === CONTACT_CARD_TYPE.ENCRYPTED_AND_SIGNED
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;;;');
254 it('should trigger an error if all of the name fields are empty when creating a contact', async () => {
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();
265 it('should trigger an error if display name is empty when editing a contact', async () => {
266 const vcard = `BEGIN:VCARD
268 UID:urn:uuid:4fbe8971-0bc3-424c-9c26-36c3e1eff6b1
269 EMAIL:jdoe@example.com
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();
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],
292 jest.spyOn(mailSettingsHooks, 'useMailSettings').mockImplementation(() => [
293 { RecipientLimit: 100 } as MailSettings,
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();
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);