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