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