xref: /expo/apps/test-suite/tests/Contacts.js (revision ab22f034)
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      // 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('Contacts.getContactByIdAsync() 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('Contacts.getContactByIdAsync() 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('Contacts.getContactsAsync() sorts contacts by first name', async () => {
307        const { data: contacts } = await Contacts.getContactsAsync({
308          fields: [Contacts.SortTypes.FirstName],
309          sort: Contacts.SortTypes.FirstName,
310          pageOffset: 0,
311          pageSize: 5,
312        });
313
314        await sortContacts(contacts, Contacts.SortTypes.FirstName, expect);
315      });
316      it('Contacts.getContactsAsync()sorts contacts by last name', async () => {
317        const { data: contacts } = await Contacts.getContactsAsync({
318          fields: [Contacts.SortTypes.LastName],
319          sort: Contacts.SortTypes.LastName,
320          pageOffset: 0,
321          pageSize: 5,
322        });
323
324        await sortContacts(contacts, Contacts.SortTypes.LastName, expect);
325      });
326    }
327
328    it('Contacts.getContactsAsync() respects the page offset', async () => {
329      const firstPage = await Contacts.getContactsAsync({
330        fields: [Contacts.Fields.PhoneNumbers],
331        pageOffset: 0,
332        pageSize: 2,
333      });
334      const secondPage = await Contacts.getContactsAsync({
335        fields: [Contacts.Fields.PhoneNumbers],
336        pageOffset: 1,
337        pageSize: 2,
338      });
339
340      if (firstPage.data.length >= 3) {
341        expect(firstPage.data.length).toBe(2);
342        expect(secondPage.data.length).toBe(2);
343        expect(firstPage.data[0].id).not.toBe(secondPage.data[0].id);
344        expect(firstPage.data[1].id).not.toBe(secondPage.data[1].id);
345        expect(firstPage.data[1].id).toBe(secondPage.data[0].id);
346      }
347    });
348
349    it('Contacts.getContactByIdAsync() gets a result of right shape', async () => {
350      const fields = {
351        [Contacts.Fields.FirstName]: 'Tommy',
352        [Contacts.Fields.LastName]: 'Wiseau',
353        [Contacts.Fields.JobTitle]: 'Director',
354        [Contacts.Fields.PhoneNumbers]: [
355          {
356            number: '123456789',
357            label: 'work',
358          },
359        ],
360        [Contacts.Fields.Emails]: [
361          {
362            email: '[email protected]',
363            label: 'unknown',
364          },
365        ],
366      };
367
368      const fakeContactId = await createContact(fields);
369
370      const contact = await Contacts.getContactByIdAsync(fakeContactId, [
371        Contacts.Fields.PhoneNumbers,
372        Contacts.Fields.Emails,
373      ]);
374
375      const { phoneNumbers, emails } = contact;
376
377      expect(contact.note).toBeUndefined();
378      expect(contact.relationships).toBeUndefined();
379      expect(contact.addresses).toBeUndefined();
380
381      expect(phoneNumbers[0]).toEqual(
382        jasmine.objectContaining({
383          id: jasmine.any(String),
384          label: jasmine.any(String),
385          number: jasmine.any(String),
386        })
387      );
388
389      expect(contact).toEqual(
390        jasmine.objectContaining({
391          contactType: jasmine.any(String),
392          name: jasmine.any(String),
393          id: jasmine.any(String),
394        })
395      );
396      expect(contact.imageAvailable).toBeDefined();
397      expect(Array.isArray(emails)).toBe(true);
398    });
399
400    it('Contacts.getContactByIdAsync() checks shape of the inserted contacts', async () => {
401      expect(createdContactIds.length).toBeGreaterThan(0);
402
403      await Promise.all(
404        createdContactIds.map(async ({ id, contact: expectedContact }) => {
405          const contact = await Contacts.getContactByIdAsync(id);
406          if (contact) {
407            expect(contact).toBeDefined();
408            expect(compareObjects(contact, expectedContact)).toBe(true);
409          }
410        })
411      );
412    });
413
414    it('Contacts.updateContactAsync() updates contact', async () => {
415      const contactId = await createSimpleContact('Andrew', 'Smith');
416
417      const updates = {
418        [Contacts.Fields.ID]: contactId,
419        [Contacts.Fields.FirstName]: 'Andy',
420      };
421
422      const id = await Contacts.updateContactAsync(updates);
423
424      expect(id).toBeDefined();
425      expect(id).toEqual(contactId);
426
427      const result = await Contacts.getContactByIdAsync(contactId, [Contacts.Fields.FirstName]);
428      expect(result[Contacts.Fields.FirstName]).toEqual('Andy');
429    });
430
431    it('Contacts.removeContactAsync() finishes successfully', async () => {
432      const contactId = await createSimpleContact('Hi', 'Joe');
433
434      let errorMessage;
435      try {
436        await Contacts.removeContactAsync(contactId);
437      } catch ({ message }) {
438        errorMessage = message;
439      }
440      expect(errorMessage).toBeUndefined();
441    });
442
443    it('Contacts.removeContactAsync() cannot get deleted contact', async () => {
444      const contactId = await createSimpleContact('Hi', 'Joe');
445      await Contacts.removeContactAsync(contactId);
446      const contact = await Contacts.getContactByIdAsync(contactId);
447      expect(contact).toBeUndefined();
448    });
449
450    const testGroupName = 'Test Expo Contacts';
451    let firstGroup;
452    let testGroups = [];
453
454    it(`Contacts.createGroupAsync() creates a group named ${testGroupName}`, async () => {
455      let errorMessage;
456      let groupId;
457      try {
458        groupId = await Contacts.createGroupAsync(testGroupName);
459      } catch ({ message }) {
460        errorMessage = message;
461      } finally {
462        if (isAndroid) {
463          expect(errorMessage).toBe(
464            `The method or property Contacts.createGroupAsync is not available on android, are you sure you've linked all the native dependencies properly?`
465          );
466        } else {
467          expect(typeof groupId).toBe('string');
468        }
469      }
470    });
471
472    it('Contacts.getGroupsAsync() gets all groups', async () => {
473      let errorMessage;
474      let groups;
475      try {
476        groups = await Contacts.getGroupsAsync({});
477        firstGroup = groups[0];
478      } catch ({ message }) {
479        errorMessage = message;
480      } finally {
481        if (isAndroid) {
482          expect(errorMessage).toBe(
483            `The method or property Contacts.getGroupsAsync is not available on android, are you sure you've linked all the native dependencies properly?`
484          );
485        } else {
486          expect(Array.isArray(groups)).toBe(true);
487          expect(groups.length).toBeGreaterThan(0);
488        }
489      }
490    });
491
492    it(`Contacts.getGroupsAsync() 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
519    it('Contacts.getDefaultContainerIdAsync() gets groups in default container', async () => {
520      let errorMessage;
521      let groups;
522      try {
523        const containerId = await Contacts.getDefaultContainerIdAsync();
524        groups = await Contacts.getGroupsAsync({
525          containerId,
526        });
527      } catch ({ message }) {
528        errorMessage = message;
529      } finally {
530        if (isAndroid) {
531          expect(errorMessage).toBe(
532            `The method or property Contacts.getDefaultContainerIdentifierAsync is not available on android, are you sure you've linked all the native dependencies properly?`
533          );
534        } else {
535          expect(Array.isArray(groups)).toBe(true);
536          expect(groups.length).toBeGreaterThan(0);
537        }
538      }
539    });
540
541    if (!isAndroid) {
542      it('Contacts.getGroupsAsync() gets group with ID', async () => {
543        const groups = await Contacts.getGroupsAsync({
544          groupId: firstGroup.id,
545        });
546        expect(Array.isArray(groups)).toBe(true);
547        expect(groups.length).toBe(1);
548        expect(groups[0].id).toBe(firstGroup.id);
549      });
550    }
551
552    it(`Contacts.removeGroupAsync() remove all groups named ${testGroupName}`, async () => {
553      if (isAndroid) {
554        let errorMessage;
555        try {
556          await Contacts.removeGroupAsync('some-value');
557        } catch ({ message }) {
558          errorMessage = message;
559        } finally {
560          expect(errorMessage).toBe(
561            `The method or property Contacts.removeGroupAsync is not available on android, are you sure you've linked all the native dependencies properly?`
562          );
563        }
564      } else {
565        for (const group of testGroups) {
566          let errorMessage;
567          try {
568            await Contacts.removeGroupAsync(group.id);
569          } catch ({ message }) {
570            errorMessage = message;
571          }
572          expect(errorMessage).toBeUndefined();
573        }
574      }
575    });
576
577    it('Contacts.getDefaultContainerIdAsync() default container exists', async () => {
578      let errorMessage;
579      let defaultContainerId;
580      try {
581        defaultContainerId = await Contacts.getDefaultContainerIdAsync();
582      } catch ({ message }) {
583        errorMessage = message;
584      } finally {
585        if (isAndroid) {
586          expect(errorMessage).toBe(
587            `The method or property Contacts.getDefaultContainerIdentifierAsync is not available on android, are you sure you've linked all the native dependencies properly?`
588          );
589        } else {
590          expect(typeof defaultContainerId).toBe('string');
591        }
592      }
593    });
594  });
595}
596