xref: /expo/apps/test-suite/tests/Contacts.js (revision cb0e1ce3)
1'use strict';
2
3import * as Permissions from 'expo-permissions';
4import * as Contacts from 'expo-contacts';
5import { Platform } from 'react-native';
6import * as TestUtils from '../TestUtils';
7
8export const name = 'Contacts';
9
10async function sortContacts(expect, sortField) {
11  const { data: contacts } = await Contacts.getContactsAsync({
12    fields: [sortField],
13    sort: sortField,
14    pageOffset: 0,
15    pageSize: 5,
16  });
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.localeCompare(propB);
23      expect(Math.max(order, 0)).toBe(0);
24    }
25  }
26}
27
28export async function test({ describe, it, xdescribe, jasmine, expect }) {
29  const shouldSkipTestsRequiringPermissions = await TestUtils.shouldSkipTestsRequiringPermissionsAsync();
30  const describeWithPermissions = shouldSkipTestsRequiringPermissions ? xdescribe : describe;
31
32  describeWithPermissions('Contacts', () => {
33    let firstContact;
34    const isAndroid = Platform.OS !== 'ios';
35    const testGroupName = 'Test Expo Contacts';
36    let firstGroup;
37    let testGroups = [];
38
39    if (Platform.OS === 'ios') {
40      let customContactId;
41      describe('Contacts.addContactAsync()', () => {
42        it('creates contact', async () => {
43          customContactId = await Contacts.addContactAsync({
44            [Contacts.Fields.FirstName]: 'Eric',
45            [Contacts.Fields.LastName]: 'Cartman',
46            [Contacts.Fields.JobTitle]: 'Actor',
47          });
48          expect(typeof customContactId).toBe('string');
49        });
50      });
51
52      describe('Contacts.writeContactToFileAsync()', () => {
53        it('returns uri', async () => {
54          const localUri = await Contacts.writeContactToFileAsync({ id: customContactId });
55
56          expect(typeof localUri).toBe('string');
57        });
58      });
59
60      describe('Contacts.removeContactAsync()', () => {
61        it('removes contact', async () => {
62          let errorMessage;
63          try {
64            await Contacts.removeContactAsync(customContactId);
65          } catch ({ message }) {
66            errorMessage = message;
67          }
68          expect(errorMessage).toBeUndefined();
69        });
70      });
71    }
72
73    describe('Contacts.getContactsAsync()', () => {
74      it('gets permission and at least one result, all results of right shape', async () => {
75        await TestUtils.acceptPermissionsAndRunCommandAsync(() => {
76          return Permissions.askAsync(Permissions.CONTACTS);
77        });
78        let contacts = await Contacts.getContactsAsync({
79          fields: [Contacts.Fields.PhoneNumbers, Contacts.Fields.Emails],
80          pageSize: 1,
81        });
82        expect(contacts.data.length > 0).toBe(true);
83        contacts.data.forEach(({ id, name, phoneNumbers, emails }) => {
84          expect(typeof id === 'string' || typeof id === 'number').toBe(true);
85          expect(typeof name === 'string' || typeof name === 'undefined').toBe(true);
86          expect(Array.isArray(phoneNumbers) || typeof phoneNumbers === 'undefined').toBe(true);
87          expect(Array.isArray(emails) || typeof emails === 'undefined').toBe(true);
88        });
89        firstContact = contacts.data[0];
90      });
91
92      it('returns correct shape', async () => {
93        const {
94          data,
95          hasNextPage,
96          hasPreviousPage,
97          total,
98          ...props
99        } = await Contacts.getContactsAsync({
100          pageSize: 1,
101        });
102
103        // Test some constant values
104
105        expect(data).toBeDefined();
106
107        expect(typeof hasNextPage).toBe('boolean');
108        expect(typeof hasPreviousPage).toBe('boolean');
109        expect(typeof total).toBe('number');
110
111        expect(data.length).toBe(1);
112        expect(hasPreviousPage).toBe(false);
113        //This could fail, if the device only has 1 contact
114        expect(hasNextPage).toBe(true);
115        //This could fail, if the device only has 1 contact
116        expect(total).toBeGreaterThan(1);
117
118        // Nothing else.
119        expect(Object.keys(props).length).toBe(0);
120
121        // Test a contact
122        expect(data[0]).toEqual(
123          jasmine.objectContaining({
124            contactType: jasmine.any(String),
125            name: jasmine.any(String),
126            id: jasmine.any(String),
127          })
128        );
129        expect(data[0].imageAvailable).toBeDefined();
130      });
131
132      async function getContactAtIndex(index, fields) {
133        // Make sure the contact has a phone number to skip!
134        const { data } = await Contacts.getContactsAsync({
135          fields,
136          pageSize: 1,
137          pageOffset: index,
138        });
139        return data[0];
140      }
141
142      it('skips phone number if not asked', async () => {
143        // This may need to be tweaked because you cannot add test contacts on android
144        const testIndex = 1;
145        const initialContact = await getContactAtIndex(testIndex, [Contacts.Fields.PhoneNumbers]);
146        expect(initialContact.phoneNumbers).toBeDefined();
147        expect(initialContact.phoneNumbers.length).toBeGreaterThan(0);
148        expect(initialContact.phoneNumbers[0]).toEqual(
149          jasmine.objectContaining({
150            id: jasmine.any(String),
151            label: jasmine.any(String),
152            number: jasmine.any(String),
153          })
154        );
155
156        const initialContactWithoutNumbers = await getContactAtIndex(testIndex, []);
157        expect(initialContactWithoutNumbers.phoneNumbers).toBeUndefined();
158      });
159
160      it('gets a local image', async () => {
161        // This may need to be tweaked because you cannot add test contacts on android
162        const contact = await getContactAtIndex(0, [Contacts.Fields.Image, 'imageBase64']);
163        expect(contact.imageAvailable).toBe(true);
164        expect(contact.thumbnail).toBeUndefined();
165
166        if (isAndroid) {
167          expect(contact.image).toEqual(
168            jasmine.objectContaining({
169              uri: jasmine.any(String),
170            })
171          );
172        } else {
173          expect(contact.image).toEqual(
174            jasmine.objectContaining({
175              uri: jasmine.any(String),
176              height: jasmine.any(Number),
177              width: jasmine.any(Number),
178              base64: jasmine.any(String),
179            })
180          );
181        }
182      });
183
184      it('respects the page size', async () => {
185        const contacts = await Contacts.getContactsAsync({
186          fields: [],
187          pageOffset: 0,
188          pageSize: 2,
189        });
190        expect(contacts.data.length).toBeLessThan(3);
191      });
192
193      if (Platform.OS === 'android') {
194        it('sorts contacts by first name', async () => {
195          await sortContacts(expect, Contacts.SortTypes.FirstName);
196        });
197        it('sorts contacts by last name', async () => {
198          await sortContacts(expect, Contacts.SortTypes.LastName);
199        });
200      }
201
202      it('respects the page offset', async () => {
203        const firstPage = await Contacts.getContactsAsync({
204          fields: [Contacts.Fields.PhoneNumbers],
205          pageOffset: 0,
206          pageSize: 2,
207        });
208        const secondPage = await Contacts.getContactsAsync({
209          fields: [Contacts.Fields.PhoneNumbers],
210          pageOffset: 1,
211          pageSize: 2,
212        });
213
214        if (firstPage.data.length >= 3) {
215          expect(firstPage.data.length).toBe(2);
216          expect(secondPage.data.length).toBe(2);
217          expect(firstPage.data[0].id).not.toBe(secondPage.data[0].id);
218          expect(firstPage.data[1].id).not.toBe(secondPage.data[1].id);
219          expect(firstPage.data[1].id).toBe(secondPage.data[0].id);
220        }
221      });
222    });
223
224    describe('Contacts.getContactByIdAsync()', () => {
225      it('gets a result of right shape', async () => {
226        if (firstContact) {
227          const contact = await Contacts.getContactByIdAsync(firstContact.id, [
228            Contacts.Fields.PhoneNumbers,
229            Contacts.Fields.Emails,
230          ]);
231          const { id, name, phoneNumbers, emails } = contact;
232
233          expect(contact.note).toBeUndefined();
234          expect(contact.relationships).toBeUndefined();
235          expect(contact.addresses).toBeUndefined();
236
237          expect(phoneNumbers[0]).toEqual(
238            jasmine.objectContaining({
239              id: jasmine.any(String),
240              label: jasmine.any(String),
241              number: jasmine.any(String),
242            })
243          );
244
245          expect(contact).toEqual(
246            jasmine.objectContaining({
247              contactType: jasmine.any(String),
248              name: jasmine.any(String),
249              id: jasmine.any(String),
250            })
251          );
252          expect(contact.imageAvailable).toBeDefined();
253          expect(Array.isArray(emails) || typeof emails === 'undefined').toBe(true);
254        }
255      });
256    });
257
258    describe('Contacts.createGroupAsync()', () => {
259      it(`creates a group named ${testGroupName}`, async () => {
260        let errorMessage;
261        let groupId;
262        try {
263          groupId = await Contacts.createGroupAsync(testGroupName);
264        } catch ({ message }) {
265          errorMessage = message;
266        } finally {
267          if (isAndroid) {
268            expect(errorMessage).toBe('Error: Contacts.createGroupAsync: iOS Only');
269          } else {
270            expect(typeof groupId).toBe('string');
271          }
272        }
273      });
274    });
275
276    describe('Contacts.getGroupsAsync()', () => {
277      it('gets all groups', async () => {
278        let errorMessage;
279        let groups;
280        try {
281          groups = await Contacts.getGroupsAsync({});
282          firstGroup = groups[0];
283        } catch ({ message }) {
284          errorMessage = message;
285        } finally {
286          if (isAndroid) {
287            expect(errorMessage).toBe('Error: Contacts.getGroupsAsync: iOS Only');
288          } else {
289            expect(Array.isArray(groups)).toBe(true);
290            expect(groups.length).toBeGreaterThan(0);
291          }
292        }
293      });
294      it(`gets groups named "${testGroupName}"`, async () => {
295        let errorMessage;
296        const groupName = testGroupName;
297        let groups;
298        try {
299          groups = await Contacts.getGroupsAsync({
300            groupName,
301          });
302          testGroups = groups;
303        } catch ({ message }) {
304          errorMessage = message;
305        } finally {
306          if (isAndroid) {
307            expect(errorMessage).toBe('Error: Contacts.getGroupsAsync: iOS Only');
308          } else {
309            expect(Array.isArray(groups)).toBe(true);
310            expect(groups.length).toBeGreaterThan(0);
311
312            for (const group of groups) {
313              expect(group.name).toBe(groupName);
314            }
315          }
316        }
317      });
318      it('gets groups in default container', async () => {
319        let errorMessage;
320        let groups;
321        try {
322          const containerId = await Contacts.getDefaultContainerIdAsync();
323          groups = await Contacts.getGroupsAsync({
324            containerId,
325          });
326        } catch ({ message }) {
327          errorMessage = message;
328        } finally {
329          if (isAndroid) {
330            expect(errorMessage).toBe('Error: Contacts.getDefaultContainerIdAsync: iOS Only');
331          } else {
332            expect(Array.isArray(groups)).toBe(true);
333            expect(groups.length).toBeGreaterThan(0);
334          }
335        }
336      });
337
338      if (!isAndroid) {
339        it('gets group with ID', async () => {
340          const groups = await Contacts.getGroupsAsync({
341            groupId: firstGroup.id,
342          });
343          expect(Array.isArray(groups)).toBe(true);
344          expect(groups.length).toBe(1);
345          expect(groups[0].id).toBe(firstGroup.id);
346        });
347      }
348    });
349
350    describe('Contacts.removeGroupAsync()', () => {
351      it(`remove all groups named ${testGroupName}`, async () => {
352        if (isAndroid) {
353          let errorMessage;
354          try {
355            const success = await Contacts.removeGroupAsync('some-value');
356          } catch ({ message }) {
357            errorMessage = message;
358          } finally {
359            expect(errorMessage).toBe('Error: Contacts.removeGroupAsync: iOS Only');
360          }
361        } else {
362          for (let group of testGroups) {
363            let errorMessage;
364            try {
365              await Contacts.removeGroupAsync(group.id);
366            } catch ({ message }) {
367              errorMessage = message;
368            }
369            expect(errorMessage).toBeUndefined();
370          }
371        }
372      });
373    });
374
375    describe('Contacts.getDefaultContainerIdAsync()', () => {
376      it('default container exists', async () => {
377        let errorMessage;
378        let defaultContainerId;
379        try {
380          defaultContainerId = await Contacts.getDefaultContainerIdAsync();
381        } catch ({ message }) {
382          errorMessage = message;
383        } finally {
384          if (isAndroid) {
385            expect(errorMessage).toBe('Error: Contacts.getDefaultContainerIdAsync: iOS Only');
386          } else {
387            expect(typeof defaultContainerId).toBe('string');
388          }
389        }
390      });
391    });
392  });
393}
394