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