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 // Nothing else. 246 expect(Object.keys(props).length).toBe(0); 247 248 // Test a contact 249 expect(data[0]).toEqual( 250 jasmine.objectContaining({ 251 contactType: jasmine.any(String), 252 id: jasmine.any(String), 253 }) 254 ); 255 expect(data[0].imageAvailable).toBeDefined(); 256 }); 257 258 it('Contacts.getContactByIdAsync() skips phone number if not asked', async () => { 259 const fakeContactWithPhoneNumber = { 260 [Contacts.Fields.FirstName]: 'Eric', 261 [Contacts.Fields.LastName]: 'Cartman', 262 [Contacts.Fields.JobTitle]: 'Actor', 263 [Contacts.Fields.PhoneNumbers]: [ 264 { 265 number: '123456789', 266 label: 'work', 267 }, 268 ], 269 }; 270 271 const newContactId = await createContact(fakeContactWithPhoneNumber); 272 273 const getWithPhone = await Contacts.getContactsAsync({ 274 fields: [Contacts.Fields.PhoneNumbers], 275 id: newContactId, 276 }); 277 278 const contactWithPhone = getWithPhone.data[0]; 279 280 expect(contactWithPhone.phoneNumbers).toBeDefined(); 281 expect(contactWithPhone.phoneNumbers.length).toBeGreaterThan(0); 282 expect(contactWithPhone.phoneNumbers[0]).toEqual( 283 jasmine.objectContaining({ 284 id: jasmine.any(String), 285 label: jasmine.any(String), 286 number: jasmine.any(String), 287 }) 288 ); 289 290 const getWithoutPhone = await Contacts.getContactsAsync({ 291 fields: [], 292 id: newContactId, 293 }); 294 295 const contactWithoutPhone = getWithoutPhone.data[0]; 296 expect(contactWithoutPhone.phoneNumbers).toBeUndefined(); 297 }); 298 299 it('Contacts.getContactByIdAsync() respects the page size', async () => { 300 const contacts = await Contacts.getContactsAsync({ 301 fields: [], 302 pageOffset: 0, 303 pageSize: 2, 304 }); 305 expect(contacts.data.length).toBeLessThan(3); 306 }); 307 308 if (Platform.OS === 'android') { 309 it('Contacts.getContactsAsync() sorts contacts by first name', async () => { 310 const { data: contacts } = await Contacts.getContactsAsync({ 311 fields: [Contacts.SortTypes.FirstName], 312 sort: Contacts.SortTypes.FirstName, 313 pageOffset: 0, 314 pageSize: 5, 315 }); 316 317 await sortContacts(contacts, Contacts.SortTypes.FirstName, expect); 318 }); 319 it('Contacts.getContactsAsync()sorts contacts by last name', async () => { 320 const { data: contacts } = await Contacts.getContactsAsync({ 321 fields: [Contacts.SortTypes.LastName], 322 sort: Contacts.SortTypes.LastName, 323 pageOffset: 0, 324 pageSize: 5, 325 }); 326 327 await sortContacts(contacts, Contacts.SortTypes.LastName, expect); 328 }); 329 } 330 331 it('Contacts.getContactsAsync() respects the page offset', async () => { 332 const firstPage = await Contacts.getContactsAsync({ 333 fields: [Contacts.Fields.PhoneNumbers], 334 pageOffset: 0, 335 pageSize: 2, 336 }); 337 const secondPage = await Contacts.getContactsAsync({ 338 fields: [Contacts.Fields.PhoneNumbers], 339 pageOffset: 1, 340 pageSize: 2, 341 }); 342 343 if (firstPage.data.length >= 3) { 344 expect(firstPage.data.length).toBe(2); 345 expect(secondPage.data.length).toBe(2); 346 expect(firstPage.data[0].id).not.toBe(secondPage.data[0].id); 347 expect(firstPage.data[1].id).not.toBe(secondPage.data[1].id); 348 expect(firstPage.data[1].id).toBe(secondPage.data[0].id); 349 } 350 }); 351 352 it('Contacts.getContactByIdAsync() gets a result of right shape', async () => { 353 const fields = { 354 [Contacts.Fields.FirstName]: 'Tommy', 355 [Contacts.Fields.LastName]: 'Wiseau', 356 [Contacts.Fields.JobTitle]: 'Director', 357 [Contacts.Fields.PhoneNumbers]: [ 358 { 359 number: '123456789', 360 label: 'work', 361 }, 362 ], 363 [Contacts.Fields.Emails]: [ 364 { 365 email: '[email protected]', 366 label: 'unknown', 367 }, 368 ], 369 }; 370 371 const fakeContactId = await createContact(fields); 372 373 const contact = await Contacts.getContactByIdAsync(fakeContactId, [ 374 Contacts.Fields.PhoneNumbers, 375 Contacts.Fields.Emails, 376 ]); 377 378 const { phoneNumbers, emails } = contact; 379 380 expect(contact.note).toBeUndefined(); 381 expect(contact.relationships).toBeUndefined(); 382 expect(contact.addresses).toBeUndefined(); 383 384 expect(phoneNumbers[0]).toEqual( 385 jasmine.objectContaining({ 386 id: jasmine.any(String), 387 label: jasmine.any(String), 388 number: jasmine.any(String), 389 }) 390 ); 391 392 expect(contact).toEqual( 393 jasmine.objectContaining({ 394 contactType: jasmine.any(String), 395 name: jasmine.any(String), 396 id: jasmine.any(String), 397 }) 398 ); 399 expect(contact.imageAvailable).toBeDefined(); 400 expect(Array.isArray(emails)).toBe(true); 401 }); 402 403 it('Contacts.getContactByIdAsync() checks shape of the inserted contacts', async () => { 404 expect(createdContactIds.length).toBeGreaterThan(0); 405 406 await Promise.all( 407 createdContactIds.map(async ({ id, contact: expectedContact }) => { 408 const contact = await Contacts.getContactByIdAsync(id); 409 if (contact) { 410 expect(contact).toBeDefined(); 411 expect(compareObjects(contact, expectedContact)).toBe(true); 412 } 413 }) 414 ); 415 }); 416 417 it('Contacts.updateContactAsync() updates contact', async () => { 418 const contactId = await createSimpleContact('Andrew', 'Smith'); 419 420 const updates = { 421 [Contacts.Fields.ID]: contactId, 422 [Contacts.Fields.FirstName]: 'Andy', 423 }; 424 425 const id = await Contacts.updateContactAsync(updates); 426 427 expect(id).toBeDefined(); 428 expect(id).toEqual(contactId); 429 430 const result = await Contacts.getContactByIdAsync(contactId, Contacts.Fields.FirstName); 431 expect(result[Contacts.Fields.FirstName]).toEqual('Andy'); 432 }); 433 434 it('Contacts.removeContactAsync() finishes successfully', async () => { 435 const contactId = await createSimpleContact('Hi', 'Joe'); 436 437 let errorMessage; 438 try { 439 await Contacts.removeContactAsync(contactId); 440 } catch ({ message }) { 441 errorMessage = message; 442 } 443 expect(errorMessage).toBeUndefined(); 444 }); 445 446 it('Contacts.removeContactAsync() cannot get deleted contact', async () => { 447 const contactId = await createSimpleContact('Hi', 'Joe'); 448 await Contacts.removeContactAsync(contactId); 449 const contact = await Contacts.getContactByIdAsync(contactId); 450 expect(contact).toBeUndefined(); 451 }); 452 453 const testGroupName = 'Test Expo Contacts'; 454 let firstGroup; 455 let testGroups = []; 456 457 it(`Contacts.createGroupAsync() creates a group named ${testGroupName}`, async () => { 458 let errorMessage; 459 let groupId; 460 try { 461 groupId = await Contacts.createGroupAsync(testGroupName); 462 } catch ({ message }) { 463 errorMessage = message; 464 } finally { 465 if (isAndroid) { 466 expect(errorMessage).toBe( 467 `The method or property Contacts.createGroupAsync is not available on android, are you sure you've linked all the native dependencies properly?` 468 ); 469 } else { 470 expect(typeof groupId).toBe('string'); 471 } 472 } 473 }); 474 475 it('Contacts.getGroupsAsync() gets all groups', async () => { 476 let errorMessage; 477 let groups; 478 try { 479 groups = await Contacts.getGroupsAsync({}); 480 firstGroup = groups[0]; 481 } catch ({ message }) { 482 errorMessage = message; 483 } finally { 484 if (isAndroid) { 485 expect(errorMessage).toBe( 486 `The method or property Contacts.getGroupsAsync is not available on android, are you sure you've linked all the native dependencies properly?` 487 ); 488 } else { 489 expect(Array.isArray(groups)).toBe(true); 490 expect(groups.length).toBeGreaterThan(0); 491 } 492 } 493 }); 494 495 it(`Contacts.getGroupsAsync() gets groups named "${testGroupName}"`, async () => { 496 let errorMessage; 497 const groupName = testGroupName; 498 let groups; 499 try { 500 groups = await Contacts.getGroupsAsync({ 501 groupName, 502 }); 503 testGroups = groups; 504 } catch ({ message }) { 505 errorMessage = message; 506 } finally { 507 if (isAndroid) { 508 expect(errorMessage).toBe( 509 `The method or property Contacts.getGroupsAsync is not available on android, are you sure you've linked all the native dependencies properly?` 510 ); 511 } else { 512 expect(Array.isArray(groups)).toBe(true); 513 expect(groups.length).toBeGreaterThan(0); 514 515 for (const group of groups) { 516 expect(group.name).toBe(groupName); 517 } 518 } 519 } 520 }); 521 522 it('Contacts.getDefaultContainerIdAsync() gets groups in default container', async () => { 523 let errorMessage; 524 let groups; 525 try { 526 const containerId = await Contacts.getDefaultContainerIdAsync(); 527 groups = await Contacts.getGroupsAsync({ 528 containerId, 529 }); 530 } catch ({ message }) { 531 errorMessage = message; 532 } finally { 533 if (isAndroid) { 534 expect(errorMessage).toBe( 535 `The method or property Contacts.getDefaultContainerIdentifierAsync is not available on android, are you sure you've linked all the native dependencies properly?` 536 ); 537 } else { 538 expect(Array.isArray(groups)).toBe(true); 539 expect(groups.length).toBeGreaterThan(0); 540 } 541 } 542 }); 543 544 if (!isAndroid) { 545 it('Contacts.getGroupsAsync() gets group with ID', async () => { 546 const groups = await Contacts.getGroupsAsync({ 547 groupId: firstGroup.id, 548 }); 549 expect(Array.isArray(groups)).toBe(true); 550 expect(groups.length).toBe(1); 551 expect(groups[0].id).toBe(firstGroup.id); 552 }); 553 } 554 555 it(`Contacts.removeGroupAsync() remove all groups named ${testGroupName}`, async () => { 556 if (isAndroid) { 557 let errorMessage; 558 try { 559 await Contacts.removeGroupAsync('some-value'); 560 } catch ({ message }) { 561 errorMessage = message; 562 } finally { 563 expect(errorMessage).toBe( 564 `The method or property Contacts.removeGroupAsync is not available on android, are you sure you've linked all the native dependencies properly?` 565 ); 566 } 567 } else { 568 for (const group of testGroups) { 569 let errorMessage; 570 try { 571 await Contacts.removeGroupAsync(group.id); 572 } catch ({ message }) { 573 errorMessage = message; 574 } 575 expect(errorMessage).toBeUndefined(); 576 } 577 } 578 }); 579 580 it('Contacts.getDefaultContainerIdAsync() 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