xref: /expo/apps/test-suite/tests/Contacts.js (revision 4e758f24)
1'use strict';
2
3import { Asset } from 'expo-asset';
4import * as Contacts from 'expo-contacts';
5import { Platform } from 'react-native';
6
7import * as TestUtils from '../TestUtils';
8
9export const name = 'Contacts';
10
11async function sortContacts(contacts, sortField, expect) {
12  for (let i = 1; i < contacts.length; i++) {
13    const { [sortField]: propA } = contacts[i - 1];
14    const { [sortField]: propB } = contacts[i];
15    if (propA && propB) {
16      const order = propA.toLowerCase().localeCompare(propB.toLowerCase());
17      expect(Math.max(order, 0)).toBe(0);
18    }
19  }
20}
21
22export async function test({
23  describe,
24  it,
25  xdescribe,
26  xit,
27  fit,
28  jasmine,
29  expect,
30  afterAll,
31  beforeAll,
32}) {
33  const shouldSkipTestsRequiringPermissions = await TestUtils.shouldSkipTestsRequiringPermissionsAsync();
34  const describeWithPermissions = shouldSkipTestsRequiringPermissions ? xdescribe : describe;
35
36  function compareArrays(array, expected) {
37    return expected.reduce(
38      (result, expectedItem) =>
39        result && array.filter(item => compareObjects(item, expectedItem)).length,
40      true
41    );
42  }
43
44  function compareObjects(object, expected) {
45    for (const prop in expected) {
46      if (prop === Contacts.Fields.Image || prop === 'lookupKey' || prop === 'id') {
47        continue;
48      }
49      if (Array.isArray(object[prop])) {
50        if (!compareArrays(object[prop], expected[prop])) {
51          return false;
52        }
53      } else if (typeof object[prop] === 'object') {
54        if (!compareObjects(object[prop], expected[prop])) {
55          return false;
56        }
57      } else if (object[prop] !== expected[prop]) {
58        expect(object[prop]).toEqual(expected[prop]);
59        return false;
60      }
61    }
62
63    return true;
64  }
65
66  describeWithPermissions('Contacts', () => {
67    const isAndroid = Platform.OS !== 'ios';
68
69    it('Contacts.requestPermissionsAsync', async () => {
70      const results = await Contacts.requestPermissionsAsync();
71
72      expect(results.granted).toBe(true);
73      expect(results.status).toBe('granted');
74    });
75
76    it('Contacts.getPermissionsAsync', async () => {
77      const results = await Contacts.getPermissionsAsync();
78      expect(results.granted).toBe(true);
79      expect(results.status).toBe('granted');
80    });
81
82    const createdContactIds = [];
83    const createContact = async contact => {
84      const id = await Contacts.addContactAsync(contact);
85      createdContactIds.push({ id, contact });
86      return id;
87    };
88
89    afterAll(async () => {
90      await Promise.all(createdContactIds.map(async ({ id }) => Contacts.removeContactAsync(id)));
91    });
92
93    it('Contacts.createContactsAsync()', async () => {
94      const contacts = [
95        {
96          [Contacts.Fields.FirstName]: 'Eric',
97          [Contacts.Fields.LastName]: 'Cartman',
98          [Contacts.Fields.JobTitle]: 'Actor',
99          [Contacts.Fields.PhoneNumbers]: [
100            {
101              number: '123456789',
102              label: 'work',
103            },
104          ],
105          [Contacts.Fields.Emails]: [
106            {
107              email: '[email protected]',
108              label: 'unknown',
109            },
110          ],
111        },
112        {
113          [Contacts.Fields.FirstName]: 'Kyle',
114          [Contacts.Fields.LastName]: 'Broflovski',
115          [Contacts.Fields.JobTitle]: 'Actor',
116          [Contacts.Fields.PhoneNumbers]: [
117            {
118              number: '987654321',
119              label: 'unknown',
120            },
121          ],
122        },
123      ];
124
125      await Promise.all(
126        contacts.map(async contact => {
127          const id = await createContact(contact);
128          expect(typeof id).toBe('string');
129        })
130      );
131    });
132
133    async function createSimpleContact(firstName, lastName) {
134      const fields = {
135        [Contacts.Fields.FirstName]: firstName,
136        [Contacts.Fields.LastName]: lastName,
137      };
138
139      return createContact(fields);
140    }
141
142    async function createContactWithImage() {
143      const image = Asset.fromModule(require('../assets/icons/app.png'));
144      await image.downloadAsync();
145
146      const fields = {
147        [Contacts.Fields.Image]: image.localUri,
148        [Contacts.Fields.FirstName]: 'Kenny',
149        [Contacts.Fields.LastName]: 'McCormick',
150      };
151
152      return createContact(fields);
153    }
154
155    it('Contacts.createContactAsync() with image', async () => {
156      const contactId = await createContactWithImage();
157      expect(typeof contactId).toBe('string');
158    });
159
160    it('Contacts.writeContactToFileAsync() returns uri', async () => {
161      createdContactIds.map(async ({ id }) => {
162        const localUri = await Contacts.writeContactToFileAsync({ id });
163        expect(typeof localUri).toBe('string');
164      });
165    });
166
167    it("Contacts.getContactByIdAsync() returns undefined when contact doesn't exist", async () => {
168      const contact = await Contacts.getContactByIdAsync('-1');
169      expect(contact).toBeUndefined();
170    });
171
172    it('Contacts.getContactByIdAsync() checks shape of all results', async () => {
173      const contacts = await Contacts.getContactsAsync({
174        fields: [Contacts.Fields.PhoneNumbers, Contacts.Fields.Emails],
175        pageSize: 1,
176      });
177
178      expect(contacts.data.length > 0).toBe(true);
179      contacts.data.forEach(({ id, name, phoneNumbers, emails }) => {
180        expect(typeof id === 'string' || typeof id === 'number').toBe(true);
181        expect(typeof name === 'string' || typeof name === 'undefined').toBe(true);
182        expect(Array.isArray(phoneNumbers) || typeof phoneNumbers === 'undefined').toBe(true);
183        expect(Array.isArray(emails) || typeof emails === 'undefined').toBe(true);
184      });
185    });
186
187    it('Contacts.getContactByIdAsync() retrieves image', async () => {
188      const contactId = await createContactWithImage();
189      const contact = await Contacts.getContactByIdAsync(contactId, [
190        Contacts.Fields.Image,
191        'imageBase64',
192      ]);
193
194      expect(contact.imageAvailable).toBe(true);
195      expect(contact.thumbnail).toBeUndefined();
196
197      if (isAndroid) {
198        expect(contact.image).toEqual(
199          jasmine.objectContaining({
200            uri: jasmine.any(String),
201          })
202        );
203      } else {
204        expect(contact.image).toEqual(
205          jasmine.objectContaining({
206            uri: jasmine.any(String),
207            height: jasmine.any(Number),
208            width: jasmine.any(Number),
209            base64: jasmine.any(String),
210          })
211        );
212      }
213    });
214
215    it('Contacts.getContactByIdAsync() returns correct shape', async () => {
216      const contact = {
217        [Contacts.Fields.FirstName]: 'Eric',
218        [Contacts.Fields.LastName]: 'Cartman',
219        [Contacts.Fields.JobTitle]: 'Actor',
220        [Contacts.Fields.PhoneNumbers]: [
221          {
222            number: '123456789',
223            label: 'work',
224          },
225        ],
226      };
227
228      const newContactId = await createContact(contact);
229
230      const { data, hasNextPage, hasPreviousPage, ...props } = await Contacts.getContactsAsync({
231        id: newContactId,
232      });
233
234      // Test some constant values
235
236      expect(data).toBeDefined();
237
238      expect(typeof hasNextPage).toBe('boolean');
239      expect(typeof hasPreviousPage).toBe('boolean');
240
241      expect(data.length).toBe(1);
242      expect(hasPreviousPage).toBe(false);
243      expect(hasNextPage).toBe(false);
244
245      // Nothing else.
246      expect(Object.keys(props).length).toBe(0);
247
248      // Test a contact
249      expect(data[0]).toEqual(
250        jasmine.objectContaining({
251          contactType: jasmine.any(String),
252          id: jasmine.any(String),
253        })
254      );
255      expect(data[0].imageAvailable).toBeDefined();
256    });
257
258    it('Contacts.getContactByIdAsync() skips phone number if not asked', async () => {
259      const fakeContactWithPhoneNumber = {
260        [Contacts.Fields.FirstName]: 'Eric',
261        [Contacts.Fields.LastName]: 'Cartman',
262        [Contacts.Fields.JobTitle]: 'Actor',
263        [Contacts.Fields.PhoneNumbers]: [
264          {
265            number: '123456789',
266            label: 'work',
267          },
268        ],
269      };
270
271      const newContactId = await createContact(fakeContactWithPhoneNumber);
272
273      const getWithPhone = await Contacts.getContactsAsync({
274        fields: [Contacts.Fields.PhoneNumbers],
275        id: newContactId,
276      });
277
278      const contactWithPhone = getWithPhone.data[0];
279
280      expect(contactWithPhone.phoneNumbers).toBeDefined();
281      expect(contactWithPhone.phoneNumbers.length).toBeGreaterThan(0);
282      expect(contactWithPhone.phoneNumbers[0]).toEqual(
283        jasmine.objectContaining({
284          id: jasmine.any(String),
285          label: jasmine.any(String),
286          number: jasmine.any(String),
287        })
288      );
289
290      const getWithoutPhone = await Contacts.getContactsAsync({
291        fields: [],
292        id: newContactId,
293      });
294
295      const contactWithoutPhone = getWithoutPhone.data[0];
296      expect(contactWithoutPhone.phoneNumbers).toBeUndefined();
297    });
298
299    it('Contacts.getContactByIdAsync() respects the page size', async () => {
300      const contacts = await Contacts.getContactsAsync({
301        fields: [],
302        pageOffset: 0,
303        pageSize: 2,
304      });
305      expect(contacts.data.length).toBeLessThan(3);
306    });
307
308    if (Platform.OS === 'android') {
309      it('Contacts.getContactsAsync() sorts contacts by first name', async () => {
310        const { data: contacts } = await Contacts.getContactsAsync({
311          fields: [Contacts.SortTypes.FirstName],
312          sort: Contacts.SortTypes.FirstName,
313          pageOffset: 0,
314          pageSize: 5,
315        });
316
317        await sortContacts(contacts, Contacts.SortTypes.FirstName, expect);
318      });
319      it('Contacts.getContactsAsync()sorts contacts by last name', async () => {
320        const { data: contacts } = await Contacts.getContactsAsync({
321          fields: [Contacts.SortTypes.LastName],
322          sort: Contacts.SortTypes.LastName,
323          pageOffset: 0,
324          pageSize: 5,
325        });
326
327        await sortContacts(contacts, Contacts.SortTypes.LastName, expect);
328      });
329    }
330
331    it('Contacts.getContactsAsync() respects the page offset', async () => {
332      const firstPage = await Contacts.getContactsAsync({
333        fields: [Contacts.Fields.PhoneNumbers],
334        pageOffset: 0,
335        pageSize: 2,
336      });
337      const secondPage = await Contacts.getContactsAsync({
338        fields: [Contacts.Fields.PhoneNumbers],
339        pageOffset: 1,
340        pageSize: 2,
341      });
342
343      if (firstPage.data.length >= 3) {
344        expect(firstPage.data.length).toBe(2);
345        expect(secondPage.data.length).toBe(2);
346        expect(firstPage.data[0].id).not.toBe(secondPage.data[0].id);
347        expect(firstPage.data[1].id).not.toBe(secondPage.data[1].id);
348        expect(firstPage.data[1].id).toBe(secondPage.data[0].id);
349      }
350    });
351
352    it('Contacts.getContactByIdAsync() gets a result of right shape', async () => {
353      const fields = {
354        [Contacts.Fields.FirstName]: 'Tommy',
355        [Contacts.Fields.LastName]: 'Wiseau',
356        [Contacts.Fields.JobTitle]: 'Director',
357        [Contacts.Fields.PhoneNumbers]: [
358          {
359            number: '123456789',
360            label: 'work',
361          },
362        ],
363        [Contacts.Fields.Emails]: [
364          {
365            email: '[email protected]',
366            label: 'unknown',
367          },
368        ],
369      };
370
371      const fakeContactId = await createContact(fields);
372
373      const contact = await Contacts.getContactByIdAsync(fakeContactId, [
374        Contacts.Fields.PhoneNumbers,
375        Contacts.Fields.Emails,
376      ]);
377
378      const { phoneNumbers, emails } = contact;
379
380      expect(contact.note).toBeUndefined();
381      expect(contact.relationships).toBeUndefined();
382      expect(contact.addresses).toBeUndefined();
383
384      expect(phoneNumbers[0]).toEqual(
385        jasmine.objectContaining({
386          id: jasmine.any(String),
387          label: jasmine.any(String),
388          number: jasmine.any(String),
389        })
390      );
391
392      expect(contact).toEqual(
393        jasmine.objectContaining({
394          contactType: jasmine.any(String),
395          name: jasmine.any(String),
396          id: jasmine.any(String),
397        })
398      );
399      expect(contact.imageAvailable).toBeDefined();
400      expect(Array.isArray(emails)).toBe(true);
401    });
402
403    it('Contacts.getContactByIdAsync() checks shape of the inserted contacts', async () => {
404      expect(createdContactIds.length).toBeGreaterThan(0);
405
406      await Promise.all(
407        createdContactIds.map(async ({ id, contact: expectedContact }) => {
408          const contact = await Contacts.getContactByIdAsync(id);
409          if (contact) {
410            expect(contact).toBeDefined();
411            expect(compareObjects(contact, expectedContact)).toBe(true);
412          }
413        })
414      );
415    });
416
417    it('Contacts.updateContactAsync() updates contact', async () => {
418      const contactId = await createSimpleContact('Andrew', 'Smith');
419
420      const updates = {
421        [Contacts.Fields.ID]: contactId,
422        [Contacts.Fields.FirstName]: 'Andy',
423      };
424
425      const id = await Contacts.updateContactAsync(updates);
426
427      expect(id).toBeDefined();
428      expect(id).toEqual(contactId);
429
430      const result = await Contacts.getContactByIdAsync(contactId, Contacts.Fields.FirstName);
431      expect(result[Contacts.Fields.FirstName]).toEqual('Andy');
432    });
433
434    it('Contacts.removeContactAsync() finishes successfully', async () => {
435      const contactId = await createSimpleContact('Hi', 'Joe');
436
437      let errorMessage;
438      try {
439        await Contacts.removeContactAsync(contactId);
440      } catch ({ message }) {
441        errorMessage = message;
442      }
443      expect(errorMessage).toBeUndefined();
444    });
445
446    it('Contacts.removeContactAsync() cannot get deleted contact', async () => {
447      const contactId = await createSimpleContact('Hi', 'Joe');
448      await Contacts.removeContactAsync(contactId);
449      const contact = await Contacts.getContactByIdAsync(contactId);
450      expect(contact).toBeUndefined();
451    });
452
453    const testGroupName = 'Test Expo Contacts';
454    let firstGroup;
455    let testGroups = [];
456
457    it(`Contacts.createGroupAsync() creates a group named ${testGroupName}`, async () => {
458      let errorMessage;
459      let groupId;
460      try {
461        groupId = await Contacts.createGroupAsync(testGroupName);
462      } catch ({ message }) {
463        errorMessage = message;
464      } finally {
465        if (isAndroid) {
466          expect(errorMessage).toBe(
467            `The method or property Contacts.createGroupAsync is not available on android, are you sure you've linked all the native dependencies properly?`
468          );
469        } else {
470          expect(typeof groupId).toBe('string');
471        }
472      }
473    });
474
475    it('Contacts.getGroupsAsync() gets all groups', async () => {
476      let errorMessage;
477      let groups;
478      try {
479        groups = await Contacts.getGroupsAsync({});
480        firstGroup = groups[0];
481      } catch ({ message }) {
482        errorMessage = message;
483      } finally {
484        if (isAndroid) {
485          expect(errorMessage).toBe(
486            `The method or property Contacts.getGroupsAsync is not available on android, are you sure you've linked all the native dependencies properly?`
487          );
488        } else {
489          expect(Array.isArray(groups)).toBe(true);
490          expect(groups.length).toBeGreaterThan(0);
491        }
492      }
493    });
494
495    it(`Contacts.getGroupsAsync() gets groups named "${testGroupName}"`, async () => {
496      let errorMessage;
497      const groupName = testGroupName;
498      let groups;
499      try {
500        groups = await Contacts.getGroupsAsync({
501          groupName,
502        });
503        testGroups = groups;
504      } catch ({ message }) {
505        errorMessage = message;
506      } finally {
507        if (isAndroid) {
508          expect(errorMessage).toBe(
509            `The method or property Contacts.getGroupsAsync is not available on android, are you sure you've linked all the native dependencies properly?`
510          );
511        } else {
512          expect(Array.isArray(groups)).toBe(true);
513          expect(groups.length).toBeGreaterThan(0);
514
515          for (const group of groups) {
516            expect(group.name).toBe(groupName);
517          }
518        }
519      }
520    });
521
522    it('Contacts.getDefaultContainerIdAsync() gets groups in default container', async () => {
523      let errorMessage;
524      let groups;
525      try {
526        const containerId = await Contacts.getDefaultContainerIdAsync();
527        groups = await Contacts.getGroupsAsync({
528          containerId,
529        });
530      } catch ({ message }) {
531        errorMessage = message;
532      } finally {
533        if (isAndroid) {
534          expect(errorMessage).toBe(
535            `The method or property Contacts.getDefaultContainerIdentifierAsync is not available on android, are you sure you've linked all the native dependencies properly?`
536          );
537        } else {
538          expect(Array.isArray(groups)).toBe(true);
539          expect(groups.length).toBeGreaterThan(0);
540        }
541      }
542    });
543
544    if (!isAndroid) {
545      it('Contacts.getGroupsAsync() gets group with ID', async () => {
546        const groups = await Contacts.getGroupsAsync({
547          groupId: firstGroup.id,
548        });
549        expect(Array.isArray(groups)).toBe(true);
550        expect(groups.length).toBe(1);
551        expect(groups[0].id).toBe(firstGroup.id);
552      });
553    }
554
555    it(`Contacts.removeGroupAsync() remove all groups named ${testGroupName}`, async () => {
556      if (isAndroid) {
557        let errorMessage;
558        try {
559          await Contacts.removeGroupAsync('some-value');
560        } catch ({ message }) {
561          errorMessage = message;
562        } finally {
563          expect(errorMessage).toBe(
564            `The method or property Contacts.removeGroupAsync is not available on android, are you sure you've linked all the native dependencies properly?`
565          );
566        }
567      } else {
568        for (const group of testGroups) {
569          let errorMessage;
570          try {
571            await Contacts.removeGroupAsync(group.id);
572          } catch ({ message }) {
573            errorMessage = message;
574          }
575          expect(errorMessage).toBeUndefined();
576        }
577      }
578    });
579
580    it('Contacts.getDefaultContainerIdAsync() default container exists', async () => {
581      let errorMessage;
582      let defaultContainerId;
583      try {
584        defaultContainerId = await Contacts.getDefaultContainerIdAsync();
585      } catch ({ message }) {
586        errorMessage = message;
587      } finally {
588        if (isAndroid) {
589          expect(errorMessage).toBe(
590            `The method or property Contacts.getDefaultContainerIdentifierAsync is not available on android, are you sure you've linked all the native dependencies properly?`
591          );
592        } else {
593          expect(typeof defaultContainerId).toBe('string');
594        }
595      }
596    });
597  });
598}
599