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