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