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({ describe, it, xdescribe, jasmine, expect, afterAll }) { 23 const shouldSkipTestsRequiringPermissions = 24 await TestUtils.shouldSkipTestsRequiringPermissionsAsync(); 25 const describeWithPermissions = shouldSkipTestsRequiringPermissions ? xdescribe : describe; 26 27 function compareArrays(array, expected) { 28 return expected.reduce( 29 (result, expectedItem) => 30 result && array.filter((item) => compareObjects(item, expectedItem)).length, 31 true 32 ); 33 } 34 35 function compareObjects(object, expected) { 36 for (const prop in expected) { 37 if (prop === Contacts.Fields.Image || prop === 'lookupKey' || prop === 'id') { 38 continue; 39 } 40 if (Array.isArray(object[prop])) { 41 if (!compareArrays(object[prop], expected[prop])) { 42 return false; 43 } 44 } else if (typeof object[prop] === 'object') { 45 if (!compareObjects(object[prop], expected[prop])) { 46 return false; 47 } 48 } else if (object[prop] !== expected[prop]) { 49 expect(object[prop]).toEqual(expected[prop]); 50 return false; 51 } 52 } 53 54 return true; 55 } 56 57 describeWithPermissions('Contacts', () => { 58 const isAndroid = Platform.OS !== 'ios'; 59 60 it('Contacts.requestPermissionsAsync', async () => { 61 const results = await Contacts.requestPermissionsAsync(); 62 63 expect(results.granted).toBe(true); 64 expect(results.status).toBe('granted'); 65 }); 66 67 it('Contacts.getPermissionsAsync', async () => { 68 const results = await Contacts.getPermissionsAsync(); 69 expect(results.granted).toBe(true); 70 expect(results.status).toBe('granted'); 71 }); 72 73 const createdContactIds = []; 74 const createContact = async (contact) => { 75 const id = await Contacts.addContactAsync(contact); 76 createdContactIds.push({ id, contact }); 77 return id; 78 }; 79 80 afterAll(async () => { 81 await Promise.all(createdContactIds.map(async ({ id }) => Contacts.removeContactAsync(id))); 82 }); 83 84 it('Contacts.createContactsAsync()', async () => { 85 const contacts = [ 86 { 87 [Contacts.Fields.FirstName]: 'Eric', 88 [Contacts.Fields.LastName]: 'Cartman', 89 [Contacts.Fields.JobTitle]: 'Actor', 90 [Contacts.Fields.PhoneNumbers]: [ 91 { 92 number: '123456789', 93 label: 'work', 94 }, 95 ], 96 [Contacts.Fields.Emails]: [ 97 { 98 email: '[email protected]', 99 label: 'unknown', 100 }, 101 ], 102 }, 103 { 104 [Contacts.Fields.FirstName]: 'Kyle', 105 [Contacts.Fields.LastName]: 'Broflovski', 106 [Contacts.Fields.JobTitle]: 'Actor', 107 [Contacts.Fields.PhoneNumbers]: [ 108 { 109 number: '987654321', 110 label: 'unknown', 111 }, 112 ], 113 }, 114 ]; 115 116 await Promise.all( 117 contacts.map(async (contact) => { 118 const id = await createContact(contact); 119 expect(typeof id).toBe('string'); 120 }) 121 ); 122 }); 123 124 async function createSimpleContact(firstName, lastName) { 125 const fields = { 126 [Contacts.Fields.FirstName]: firstName, 127 [Contacts.Fields.LastName]: lastName, 128 }; 129 130 return createContact(fields); 131 } 132 133 async function createContactWithImage() { 134 const image = Asset.fromModule(require('../assets/icons/app.png')); 135 await image.downloadAsync(); 136 137 const fields = { 138 [Contacts.Fields.Image]: image.localUri, 139 [Contacts.Fields.FirstName]: 'Kenny', 140 [Contacts.Fields.LastName]: 'McCormick', 141 }; 142 143 return createContact(fields); 144 } 145 146 it('Contacts.createContactAsync() with image', async () => { 147 const contactId = await createContactWithImage(); 148 expect(typeof contactId).toBe('string'); 149 }); 150 151 it('Contacts.writeContactToFileAsync() returns uri', async () => { 152 createdContactIds.map(async ({ id }) => { 153 const localUri = await Contacts.writeContactToFileAsync({ id }); 154 expect(typeof localUri).toBe('string'); 155 }); 156 }); 157 158 it("Contacts.getContactByIdAsync() returns undefined when contact doesn't exist", async () => { 159 const contact = await Contacts.getContactByIdAsync('-1'); 160 expect(contact).toBeUndefined(); 161 }); 162 163 it('Contacts.getContactByIdAsync() checks shape of all results', async () => { 164 const contacts = await Contacts.getContactsAsync({ 165 fields: [Contacts.Fields.PhoneNumbers, Contacts.Fields.Emails], 166 pageSize: 1, 167 }); 168 169 expect(contacts.data.length > 0).toBe(true); 170 contacts.data.forEach(({ id, name, phoneNumbers, emails }) => { 171 expect(typeof id === 'string' || typeof id === 'number').toBe(true); 172 expect(typeof name === 'string' || typeof name === 'undefined').toBe(true); 173 expect(Array.isArray(phoneNumbers) || typeof phoneNumbers === 'undefined').toBe(true); 174 expect(Array.isArray(emails) || typeof emails === 'undefined').toBe(true); 175 }); 176 }); 177 178 it('Contacts.getContactByIdAsync() retrieves image', async () => { 179 const contactId = await createContactWithImage(); 180 const contact = await Contacts.getContactByIdAsync(contactId, [ 181 Contacts.Fields.Image, 182 'imageBase64', 183 ]); 184 185 expect(contact.imageAvailable).toBe(true); 186 expect(contact.thumbnail).toBeUndefined(); 187 188 if (isAndroid) { 189 expect(contact.image).toEqual( 190 jasmine.objectContaining({ 191 uri: jasmine.any(String), 192 }) 193 ); 194 } else { 195 expect(contact.image).toEqual( 196 jasmine.objectContaining({ 197 uri: jasmine.any(String), 198 height: jasmine.any(Number), 199 width: jasmine.any(Number), 200 base64: jasmine.any(String), 201 }) 202 ); 203 } 204 }); 205 206 it('Contacts.getContactByIdAsync() returns correct shape', async () => { 207 const contact = { 208 [Contacts.Fields.FirstName]: 'Eric', 209 [Contacts.Fields.LastName]: 'Cartman', 210 [Contacts.Fields.JobTitle]: 'Actor', 211 [Contacts.Fields.PhoneNumbers]: [ 212 { 213 number: '123456789', 214 label: 'work', 215 }, 216 ], 217 }; 218 219 const newContactId = await createContact(contact); 220 221 const { data, hasNextPage, hasPreviousPage } = await Contacts.getContactsAsync({ 222 id: newContactId, 223 }); 224 225 // Test some constant values 226 227 expect(data).toBeDefined(); 228 229 expect(typeof hasNextPage).toBe('boolean'); 230 expect(typeof hasPreviousPage).toBe('boolean'); 231 232 expect(data.length).toBe(1); 233 expect(hasPreviousPage).toBe(false); 234 expect(hasNextPage).toBe(false); 235 236 // Test a contact 237 expect(data[0]).toEqual( 238 jasmine.objectContaining({ 239 contactType: jasmine.any(String), 240 id: jasmine.any(String), 241 }) 242 ); 243 expect(data[0].imageAvailable).toBeDefined(); 244 }); 245 246 it('Contacts.getContactByIdAsync() skips phone number if not asked', async () => { 247 const fakeContactWithPhoneNumber = { 248 [Contacts.Fields.FirstName]: 'Eric', 249 [Contacts.Fields.LastName]: 'Cartman', 250 [Contacts.Fields.JobTitle]: 'Actor', 251 [Contacts.Fields.PhoneNumbers]: [ 252 { 253 number: '123456789', 254 label: 'work', 255 }, 256 ], 257 }; 258 259 const newContactId = await createContact(fakeContactWithPhoneNumber); 260 261 const getWithPhone = await Contacts.getContactsAsync({ 262 fields: [Contacts.Fields.PhoneNumbers], 263 id: newContactId, 264 }); 265 266 const contactWithPhone = getWithPhone.data[0]; 267 268 expect(contactWithPhone.phoneNumbers).toBeDefined(); 269 expect(contactWithPhone.phoneNumbers.length).toBeGreaterThan(0); 270 expect(contactWithPhone.phoneNumbers[0]).toEqual( 271 jasmine.objectContaining({ 272 id: jasmine.any(String), 273 label: jasmine.any(String), 274 number: jasmine.any(String), 275 }) 276 ); 277 278 const getWithoutPhone = await Contacts.getContactsAsync({ 279 fields: [], 280 id: newContactId, 281 }); 282 283 const contactWithoutPhone = getWithoutPhone.data[0]; 284 expect(contactWithoutPhone.phoneNumbers).toBeUndefined(); 285 }); 286 287 it('Contacts.getContactByIdAsync() respects the page size', async () => { 288 const contacts = await Contacts.getContactsAsync({ 289 fields: [], 290 pageOffset: 0, 291 pageSize: 2, 292 }); 293 expect(contacts.data.length).toBeLessThan(3); 294 }); 295 296 if (Platform.OS === 'android') { 297 it('Contacts.getContactsAsync() sorts contacts by first name', async () => { 298 const { data: contacts } = await Contacts.getContactsAsync({ 299 fields: [Contacts.SortTypes.FirstName], 300 sort: Contacts.SortTypes.FirstName, 301 pageOffset: 0, 302 pageSize: 5, 303 }); 304 305 await sortContacts(contacts, Contacts.SortTypes.FirstName, expect); 306 }); 307 it('Contacts.getContactsAsync()sorts contacts by last name', async () => { 308 const { data: contacts } = await Contacts.getContactsAsync({ 309 fields: [Contacts.SortTypes.LastName], 310 sort: Contacts.SortTypes.LastName, 311 pageOffset: 0, 312 pageSize: 5, 313 }); 314 315 await sortContacts(contacts, Contacts.SortTypes.LastName, expect); 316 }); 317 } 318 319 it('Contacts.getContactsAsync() respects the page offset', async () => { 320 const firstPage = await Contacts.getContactsAsync({ 321 fields: [Contacts.Fields.PhoneNumbers], 322 pageOffset: 0, 323 pageSize: 2, 324 }); 325 const secondPage = await Contacts.getContactsAsync({ 326 fields: [Contacts.Fields.PhoneNumbers], 327 pageOffset: 1, 328 pageSize: 2, 329 }); 330 331 if (firstPage.data.length >= 3) { 332 expect(firstPage.data.length).toBe(2); 333 expect(secondPage.data.length).toBe(2); 334 expect(firstPage.data[0].id).not.toBe(secondPage.data[0].id); 335 expect(firstPage.data[1].id).not.toBe(secondPage.data[1].id); 336 expect(firstPage.data[1].id).toBe(secondPage.data[0].id); 337 } 338 }); 339 340 it('Contacts.getContactByIdAsync() gets a result of right shape', async () => { 341 const fields = { 342 [Contacts.Fields.FirstName]: 'Tommy', 343 [Contacts.Fields.LastName]: 'Wiseau', 344 [Contacts.Fields.JobTitle]: 'Director', 345 [Contacts.Fields.PhoneNumbers]: [ 346 { 347 number: '123456789', 348 label: 'work', 349 }, 350 ], 351 [Contacts.Fields.Emails]: [ 352 { 353 email: '[email protected]', 354 label: 'unknown', 355 }, 356 ], 357 }; 358 359 const fakeContactId = await createContact(fields); 360 361 const contact = await Contacts.getContactByIdAsync(fakeContactId, [ 362 Contacts.Fields.PhoneNumbers, 363 Contacts.Fields.Emails, 364 ]); 365 366 const { phoneNumbers, emails } = contact; 367 368 expect(contact.note).toBeUndefined(); 369 expect(contact.relationships).toBeUndefined(); 370 expect(contact.addresses).toBeUndefined(); 371 372 expect(phoneNumbers[0]).toEqual( 373 jasmine.objectContaining({ 374 id: jasmine.any(String), 375 label: jasmine.any(String), 376 number: jasmine.any(String), 377 }) 378 ); 379 380 expect(contact).toEqual( 381 jasmine.objectContaining({ 382 contactType: jasmine.any(String), 383 name: jasmine.any(String), 384 id: jasmine.any(String), 385 }) 386 ); 387 expect(contact.imageAvailable).toBeDefined(); 388 expect(Array.isArray(emails)).toBe(true); 389 }); 390 391 it('Contacts.getContactByIdAsync() checks shape of the inserted contacts', async () => { 392 expect(createdContactIds.length).toBeGreaterThan(0); 393 394 await Promise.all( 395 createdContactIds.map(async ({ id, contact: expectedContact }) => { 396 const contact = await Contacts.getContactByIdAsync(id); 397 if (contact) { 398 expect(contact).toBeDefined(); 399 expect(compareObjects(contact, expectedContact)).toBe(true); 400 } 401 }) 402 ); 403 }); 404 405 it('Contacts.updateContactAsync() updates contact', async () => { 406 const contactId = await createSimpleContact('Andrew', 'Smith'); 407 408 const updates = { 409 [Contacts.Fields.ID]: contactId, 410 [Contacts.Fields.FirstName]: 'Andy', 411 }; 412 413 const id = await Contacts.updateContactAsync(updates); 414 415 expect(id).toBeDefined(); 416 expect(id).toEqual(contactId); 417 418 const result = await Contacts.getContactByIdAsync(contactId, [Contacts.Fields.FirstName]); 419 expect(result[Contacts.Fields.FirstName]).toEqual('Andy'); 420 }); 421 422 it('Contacts.removeContactAsync() finishes successfully', async () => { 423 const contactId = await createSimpleContact('Hi', 'Joe'); 424 425 let errorMessage; 426 try { 427 await Contacts.removeContactAsync(contactId); 428 } catch ({ message }) { 429 errorMessage = message; 430 } 431 expect(errorMessage).toBeUndefined(); 432 }); 433 434 it('Contacts.removeContactAsync() cannot get deleted contact', async () => { 435 const contactId = await createSimpleContact('Hi', 'Joe'); 436 await Contacts.removeContactAsync(contactId); 437 const contact = await Contacts.getContactByIdAsync(contactId); 438 expect(contact).toBeUndefined(); 439 }); 440 441 const testGroupName = 'Test Expo Contacts'; 442 let firstGroup; 443 let testGroups = []; 444 445 it(`Contacts.createGroupAsync() creates a group named ${testGroupName}`, async () => { 446 let errorMessage; 447 let groupId; 448 try { 449 groupId = await Contacts.createGroupAsync(testGroupName); 450 } catch ({ message }) { 451 errorMessage = message; 452 } finally { 453 if (isAndroid) { 454 expect(errorMessage).toBe( 455 `The method or property Contacts.createGroupAsync is not available on android, are you sure you've linked all the native dependencies properly?` 456 ); 457 } else { 458 expect(typeof groupId).toBe('string'); 459 } 460 } 461 }); 462 463 it('Contacts.getGroupsAsync() gets all groups', async () => { 464 let errorMessage; 465 let groups; 466 try { 467 groups = await Contacts.getGroupsAsync({}); 468 firstGroup = groups[0]; 469 } catch ({ message }) { 470 errorMessage = message; 471 } finally { 472 if (isAndroid) { 473 expect(errorMessage).toBe( 474 `The method or property Contacts.getGroupsAsync is not available on android, are you sure you've linked all the native dependencies properly?` 475 ); 476 } else { 477 expect(Array.isArray(groups)).toBe(true); 478 expect(groups.length).toBeGreaterThan(0); 479 } 480 } 481 }); 482 483 it(`Contacts.getGroupsAsync() gets groups named "${testGroupName}"`, async () => { 484 let errorMessage; 485 const groupName = testGroupName; 486 let groups; 487 try { 488 groups = await Contacts.getGroupsAsync({ 489 groupName, 490 }); 491 testGroups = groups; 492 } catch ({ message }) { 493 errorMessage = message; 494 } finally { 495 if (isAndroid) { 496 expect(errorMessage).toBe( 497 `The method or property Contacts.getGroupsAsync is not available on android, are you sure you've linked all the native dependencies properly?` 498 ); 499 } else { 500 expect(Array.isArray(groups)).toBe(true); 501 expect(groups.length).toBeGreaterThan(0); 502 503 for (const group of groups) { 504 expect(group.name).toBe(groupName); 505 } 506 } 507 } 508 }); 509 510 it('Contacts.getDefaultContainerIdAsync() gets groups in default container', async () => { 511 let errorMessage; 512 let groups; 513 try { 514 const containerId = await Contacts.getDefaultContainerIdAsync(); 515 groups = await Contacts.getGroupsAsync({ 516 containerId, 517 }); 518 } catch ({ message }) { 519 errorMessage = message; 520 } finally { 521 if (isAndroid) { 522 expect(errorMessage).toBe( 523 `The method or property Contacts.getDefaultContainerIdentifierAsync is not available on android, are you sure you've linked all the native dependencies properly?` 524 ); 525 } else { 526 expect(Array.isArray(groups)).toBe(true); 527 expect(groups.length).toBeGreaterThan(0); 528 } 529 } 530 }); 531 532 if (!isAndroid) { 533 it('Contacts.getGroupsAsync() gets group with ID', async () => { 534 const groups = await Contacts.getGroupsAsync({ 535 groupId: firstGroup.id, 536 }); 537 expect(Array.isArray(groups)).toBe(true); 538 expect(groups.length).toBe(1); 539 expect(groups[0].id).toBe(firstGroup.id); 540 }); 541 } 542 543 it(`Contacts.removeGroupAsync() remove all groups named ${testGroupName}`, async () => { 544 if (isAndroid) { 545 let errorMessage; 546 try { 547 await Contacts.removeGroupAsync('some-value'); 548 } catch (e) { 549 errorMessage = e.message; 550 } finally { 551 expect(errorMessage).toBe( 552 `The method or property Contacts.removeGroupAsync is not available on android, are you sure you've linked all the native dependencies properly?` 553 ); 554 } 555 } else { 556 for (const group of testGroups) { 557 let errorMessage; 558 try { 559 await Contacts.removeGroupAsync(group.id); 560 } catch (e) { 561 errorMessage = e.message; 562 } 563 expect(errorMessage).toBeUndefined(); 564 } 565 } 566 }); 567 568 it('Contacts.getDefaultContainerIdAsync() default container exists', async () => { 569 let errorMessage; 570 let defaultContainerId; 571 try { 572 defaultContainerId = await Contacts.getDefaultContainerIdAsync(); 573 } catch ({ message }) { 574 errorMessage = message; 575 } finally { 576 if (isAndroid) { 577 expect(errorMessage).toBe( 578 `The method or property Contacts.getDefaultContainerIdentifierAsync is not available on android, are you sure you've linked all the native dependencies properly?` 579 ); 580 } else { 581 expect(typeof defaultContainerId).toBe('string'); 582 } 583 } 584 }); 585 }); 586} 587