1'use strict'; 2 3import { Platform } from '@unimodules/core'; 4import Constants from 'expo-constants'; 5import * as Device from 'expo-device'; 6import * as FileSystem from 'expo-file-system'; 7import * as Notifications from 'expo-notifications'; 8import { Alert, AppState } from 'react-native'; 9 10import * as TestUtils from '../TestUtils'; 11import { isInteractive } from '../utils/Environment'; 12import { waitFor } from './helpers'; 13 14export const name = 'Notifications'; 15 16export async function test(t) { 17 const shouldSkipTestsRequiringPermissions = await TestUtils.shouldSkipTestsRequiringPermissionsAsync(); 18 const describeWithPermissions = shouldSkipTestsRequiringPermissions ? t.xdescribe : t.describe; 19 20 t.describe('Notifications', () => { 21 t.describe('getDevicePushTokenAsync', () => { 22 let subscription = null; 23 let tokenFromEvent = null; 24 let tokenFromMethodCall = null; 25 26 t.beforeAll(() => { 27 subscription = Notifications.addPushTokenListener(newEvent => { 28 tokenFromEvent = newEvent; 29 }); 30 }); 31 32 t.afterAll(() => { 33 if (subscription) { 34 subscription.remove(); 35 subscription = null; 36 } 37 }); 38 39 if (Platform.OS === 'android' || Platform.OS === 'ios') { 40 t.it('resolves with a string', async () => { 41 const devicePushToken = await Notifications.getDevicePushTokenAsync(); 42 t.expect(typeof devicePushToken.data).toBe('string'); 43 tokenFromMethodCall = devicePushToken; 44 }); 45 } 46 47 if (Platform.OS === 'web') { 48 t.it('resolves with an object', async () => { 49 const devicePushToken = await Notifications.getDevicePushTokenAsync(); 50 t.expect(typeof devicePushToken.data).toBe('object'); 51 tokenFromMethodCall = devicePushToken; 52 }); 53 } 54 55 t.it('emits an event with token (or not, if getDevicePushTokenAsync failed)', async () => { 56 // It would be better to do `if (!tokenFromMethodCall) { pending(); } else { ... }` 57 // but `t.pending()` still doesn't work. 58 await waitFor(500); 59 t.expect(tokenFromEvent).toEqual(tokenFromMethodCall); 60 }); 61 62 t.it('resolves when multiple calls are issued', async () => { 63 const results = await Promise.all([ 64 Notifications.getDevicePushTokenAsync(), 65 Notifications.getDevicePushTokenAsync(), 66 ]); 67 t.expect(results[0].data).toBeDefined(); 68 t.expect(results[0].data).toBe(results[1].data); 69 }); 70 71 // Not running this test on web since Expo push notification doesn't yet support web. 72 const itWithExpoPushToken = ['ios', 'android'].includes(Platform.OS) ? t.it : t.xit; 73 itWithExpoPushToken('fetches Expo push token', async () => { 74 let experienceId = undefined; 75 if (!Constants.manifest) { 76 // Absence of manifest means we're running out of managed workflow 77 // in bare-expo. @exponent/bare-expo "experience" has been configured 78 // to use Apple Push Notification key that will work in bare-expo. 79 experienceId = '@exponent/bare-expo'; 80 } 81 const expoPushToken = await Notifications.getExpoPushTokenAsync({ 82 experienceId, 83 }); 84 t.expect(expoPushToken.type).toBe('expo'); 85 t.expect(typeof expoPushToken.data).toBe('string'); 86 }); 87 88 itWithExpoPushToken('resolves when mixed multiple calls are issued', async () => { 89 let experienceId = undefined; 90 if (!Constants.manifest) { 91 // Absence of manifest means we're running out of managed workflow 92 // in bare-expo. @exponent/bare-expo "experience" has been configured 93 // to use Apple Push Notification key that will work in bare-expo. 94 experienceId = '@exponent/bare-expo'; 95 } 96 await Promise.all([ 97 Notifications.getExpoPushTokenAsync({ experienceId }), 98 Notifications.getDevicePushTokenAsync(), 99 ]); 100 }); 101 }); 102 103 // Not running those tests on web since Expo push notification doesn't yet support web. 104 const describeWithExpoPushToken = ['ios', 'android'].includes(Platform.OS) 105 ? t.describe 106 : t.xdescribe; 107 108 describeWithExpoPushToken('when a push notification is sent', () => { 109 let notificationToHandle; 110 let handleSuccessEvent; 111 let handleErrorEvent; 112 113 let receivedEvent = null; 114 let receivedSubscription = null; 115 116 let expoPushToken; 117 118 let handleFuncOverride; 119 120 t.beforeAll(async () => { 121 let experienceId = undefined; 122 if (!Constants.manifest) { 123 // Absence of manifest means we're running out of managed workflow 124 // in bare-expo. @exponent/bare-expo "experience" has been configured 125 // to use Apple Push Notification key that will work in bare-expo. 126 experienceId = '@exponent/bare-expo'; 127 } 128 const pushToken = await Notifications.getExpoPushTokenAsync({ 129 experienceId, 130 }); 131 expoPushToken = pushToken.data; 132 133 Notifications.setNotificationHandler({ 134 handleNotification: async notification => { 135 notificationToHandle = notification; 136 if (handleFuncOverride) { 137 return await handleFuncOverride(notification); 138 } else { 139 return { 140 shouldPlaySound: false, 141 shouldSetBadge: false, 142 shouldShowAlert: true, 143 }; 144 } 145 }, 146 handleSuccess: event => { 147 handleSuccessEvent = event; 148 }, 149 handleError: (...event) => { 150 handleErrorEvent = event; 151 }, 152 }); 153 154 receivedSubscription = Notifications.addNotificationReceivedListener(event => { 155 receivedEvent = event; 156 }); 157 }); 158 159 t.beforeEach(async () => { 160 receivedEvent = null; 161 handleErrorEvent = null; 162 handleSuccessEvent = null; 163 notificationToHandle = null; 164 await sendTestPushNotification(expoPushToken); 165 }); 166 167 t.afterAll(() => { 168 if (receivedSubscription) { 169 receivedSubscription.remove(); 170 receivedSubscription = null; 171 } 172 Notifications.setNotificationHandler(null); 173 }); 174 175 t.it('calls the `handleNotification` callback of the notification handler', async () => { 176 let iterations = 0; 177 while (iterations < 5) { 178 iterations += 1; 179 if (notificationToHandle) { 180 break; 181 } 182 await waitFor(1000); 183 } 184 t.expect(notificationToHandle).not.toBeNull(); 185 }); 186 187 t.it('emits a “notification received” event', async () => { 188 let iterations = 0; 189 while (iterations < 5) { 190 iterations += 1; 191 if (receivedEvent) { 192 break; 193 } 194 await waitFor(1000); 195 } 196 t.expect(receivedEvent).not.toBeNull(); 197 }); 198 199 t.it('the notification has proper `data` value', async () => { 200 let iterations = 0; 201 while (iterations < 5) { 202 iterations += 1; 203 if (receivedEvent) { 204 break; 205 } 206 await waitFor(1000); 207 } 208 t.expect(receivedEvent.request.content.data.fieldTestedInDataContentsTest).toBe(42); 209 }); 210 211 t.describe('if handler responds in time', async () => { 212 t.it( 213 'calls `handleSuccess` callback of the notification handler', 214 async () => { 215 let iterations = 0; 216 while (iterations < 5) { 217 iterations += 1; 218 if (handleSuccessEvent) { 219 break; 220 } 221 await waitFor(1000); 222 } 223 t.expect(handleSuccessEvent).not.toBeNull(); 224 t.expect(handleErrorEvent).toBeNull(); 225 }, 226 10000 227 ); 228 }); 229 230 t.describe('if handler fails to respond in time', async () => { 231 t.beforeAll(() => { 232 handleFuncOverride = async () => { 233 await waitFor(3000); 234 return { 235 shouldPlaySound: false, 236 shouldSetBadge: false, 237 shouldShowAlert: true, 238 }; 239 }; 240 }); 241 242 t.afterAll(() => { 243 handleFuncOverride = null; 244 }); 245 246 t.it( 247 'calls `handleError` callback of the notification handler', 248 async () => { 249 let iterations = 0; 250 while (iterations < 5) { 251 iterations += 1; 252 if (handleErrorEvent) { 253 break; 254 } 255 await waitFor(1000); 256 } 257 t.expect(handleErrorEvent).not.toBeNull(); 258 t.expect(typeof handleErrorEvent[0]).toBe('string'); 259 t.expect(handleSuccessEvent).toBeNull(); 260 }, 261 10000 262 ); 263 }); 264 }); 265 266 t.describe('getPermissionsAsync', () => { 267 t.it('resolves with an object', async () => { 268 const permissions = await Notifications.getPermissionsAsync(); 269 t.expect(permissions).toBeDefined(); 270 t.expect(typeof permissions).toBe('object'); 271 }); 272 }); 273 274 describeWithPermissions('requestPermissionsAsync', () => { 275 t.it('resolves without any arguments', async () => { 276 const permissions = await Notifications.requestPermissionsAsync(); 277 t.expect(permissions).toBeDefined(); 278 t.expect(typeof permissions).toBe('object'); 279 }); 280 281 t.it('resolves with specific permissions requested', async () => { 282 const permissions = await Notifications.requestPermissionsAsync({ 283 providesAppNotificationSettings: true, 284 allowsAlert: true, 285 allowsBadge: true, 286 allowsSound: true, 287 }); 288 t.expect(permissions).toBeDefined(); 289 t.expect(typeof permissions).toBe('object'); 290 t.expect(typeof permissions.status).toBe('string'); 291 }); 292 }); 293 294 t.describe('presentNotificationAsync', () => { 295 t.it('presents a simple notification', async () => { 296 await Notifications.presentNotificationAsync({ 297 title: 'Sample title', 298 subtitle: 'What an event!', 299 body: 'An interesting event has just happened', 300 badge: 1, 301 }); 302 }); 303 304 t.it('presents a notification with attachments', async () => { 305 const fileUri = FileSystem.documentDirectory + 'expo-notifications-test-image.jpg'; 306 await FileSystem.downloadAsync('http://placekitten.com/200/300', fileUri); 307 await Notifications.presentNotificationAsync({ 308 title: 'Look at that kitten! ➡️', 309 body: 'What a cutie!', 310 attachments: [{ uri: fileUri }], 311 }); 312 }); 313 }); 314 315 t.describe('Notification channels', () => { 316 // Implementation detail! 317 const fallbackChannelId = 'expo_notifications_fallback_notification_channel'; 318 const fallbackChannelName = 'Miscellaneous'; 319 const testChannelId = 'test-channel-id'; 320 const testChannel = { 321 name: 'Test channel', 322 }; 323 324 t.describe('getNotificationChannelAsync()', () => { 325 t.it('returns null if there is no such channel', async () => { 326 const channel = await Notifications.getNotificationChannelAsync( 327 'non-existent-channel-id' 328 ); 329 t.expect(channel).toBe(null); 330 }); 331 332 // Test push notifications sent without a channel ID should create a fallback channel 333 if (Platform.OS === 'android' && Device.platformApiLevel >= 26) { 334 t.it('returns an object if there is such channel', async () => { 335 const channel = await Notifications.getNotificationChannelAsync(fallbackChannelId); 336 t.expect(channel).toBeDefined(); 337 }); 338 } 339 }); 340 341 t.describe('getNotificationChannelsAsync()', () => { 342 t.it('returns an array', async () => { 343 const channels = await Notifications.getNotificationChannelsAsync(); 344 t.expect(channels).toEqual(t.jasmine.any(Array)); 345 }); 346 347 // Test push notifications sent without a channel ID should create a fallback channel 348 if (Platform.OS === 'android' && Device.platformApiLevel >= 26) { 349 t.it('contains the fallback channel', async () => { 350 const channels = await Notifications.getNotificationChannelsAsync(); 351 t.expect(channels).toContain( 352 t.jasmine.objectContaining({ 353 // Implementation detail! 354 id: fallbackChannelId, 355 name: fallbackChannelName, 356 }) 357 ); 358 }); 359 } 360 }); 361 362 t.describe('setNotificationChannelAsync()', () => { 363 t.afterEach(async () => { 364 await Notifications.deleteNotificationChannelAsync(testChannelId); 365 }); 366 367 if (Platform.OS === 'android' && Device.platformApiLevel >= 26) { 368 t.it('returns the modified channel', async () => { 369 const channel = await Notifications.setNotificationChannelAsync( 370 testChannelId, 371 testChannel 372 ); 373 t.expect(channel).toEqual( 374 t.jasmine.objectContaining({ ...testChannel, id: testChannelId }) 375 ); 376 }); 377 378 t.it('creates a channel', async () => { 379 const preChannels = await Notifications.getNotificationChannelsAsync(); 380 const channelSpec = t.jasmine.objectContaining({ ...testChannel, id: testChannelId }); 381 t.expect(preChannels).not.toContain(channelSpec); 382 await Notifications.setNotificationChannelAsync(testChannelId, testChannel); 383 const postChannels = await Notifications.getNotificationChannelsAsync(); 384 t.expect(postChannels).toContain(channelSpec); 385 t.expect(postChannels.length).toBeGreaterThan(preChannels.length); 386 }); 387 388 t.it('sets custom properties', async () => { 389 const spec = { 390 name: 'Name', 391 importance: Notifications.AndroidImportance.MIN, 392 bypassDnd: true, 393 description: 'Test channel', 394 lightColor: '#FF231F7C', 395 lockscreenVisibility: Notifications.AndroidNotificationVisibility.SECRET, 396 showBadge: false, 397 sound: null, 398 audioAttributes: { 399 usage: Notifications.AndroidAudioUsage.NOTIFICATION_COMMUNICATION_INSTANT, 400 contentType: Notifications.AndroidAudioContentType.SONIFICATION, 401 flags: { 402 enforceAudibility: true, 403 requestHardwareAudioVideoSynchronization: true, 404 }, 405 }, 406 vibrationPattern: [500, 500], 407 enableLights: true, 408 enableVibrate: true, 409 }; 410 const channel = await Notifications.setNotificationChannelAsync(testChannelId, spec); 411 t.expect(channel).toEqual(t.jasmine.objectContaining({ ...spec, id: testChannelId })); 412 }); 413 414 t.it('assigns a channel to a group', async () => { 415 const groupId = 'test-group-id'; 416 try { 417 await Notifications.setNotificationChannelGroupAsync(groupId, { name: 'Test group' }); 418 const channel = await Notifications.setNotificationChannelAsync(testChannelId, { 419 ...testChannel, 420 groupId, 421 }); 422 t.expect(channel.groupId).toBe(groupId); 423 const group = await Notifications.getNotificationChannelGroupAsync(groupId); 424 t.expect(group.channels).toContain(t.jasmine.objectContaining(testChannel)); 425 } catch (e) { 426 await Notifications.deleteNotificationChannelAsync(testChannelId); 427 await Notifications.deleteNotificationChannelGroupAsync(groupId); 428 throw e; 429 } 430 }); 431 432 t.it('updates a channel', async () => { 433 await Notifications.setNotificationChannelAsync(testChannelId, { 434 name: 'Name before change', 435 }); 436 await Notifications.setNotificationChannelAsync(testChannelId, { 437 name: 'Name after change', 438 }); 439 const channels = await Notifications.getNotificationChannelsAsync(); 440 t.expect(channels).toContain( 441 t.jasmine.objectContaining({ 442 name: 'Name after change', 443 id: testChannelId, 444 }) 445 ); 446 t.expect(channels).not.toContain( 447 t.jasmine.objectContaining({ 448 name: 'Name before change', 449 id: testChannelId, 450 }) 451 ); 452 }); 453 } else { 454 t.it("doesn't throw an error", async () => { 455 await Notifications.setNotificationChannelAsync(testChannelId, testChannel); 456 }); 457 } 458 }); 459 460 t.describe('deleteNotificationChannelAsync()', () => { 461 if (Platform.OS === 'android' && Device.platformApiLevel >= 26) { 462 t.it('deletes a channel', async () => { 463 const preChannels = await Notifications.getNotificationChannelsAsync(); 464 const channelSpec = t.jasmine.objectContaining({ ...testChannel, id: testChannelId }); 465 t.expect(preChannels).not.toContain(channelSpec); 466 await Notifications.setNotificationChannelAsync(testChannelId, testChannel); 467 const postChannels = await Notifications.getNotificationChannelsAsync(); 468 await Notifications.deleteNotificationChannelAsync(testChannelId); 469 t.expect(postChannels).toContain(channelSpec); 470 t.expect(postChannels.length).toBeGreaterThan(preChannels.length); 471 }); 472 } else { 473 t.it("doesn't throw an error", async () => { 474 await Notifications.deleteNotificationChannelAsync(testChannelId, testChannel); 475 }); 476 } 477 }); 478 }); 479 480 t.describe('Notification channel groups', () => { 481 const testChannelGroupId = 'test-channel-group-id'; 482 const testChannelGroup = { name: 'Test channel group' }; 483 484 t.describe('getNotificationChannelGroupAsync()', () => { 485 t.it('returns null if there is no such channel group', async () => { 486 const channelGroup = await Notifications.getNotificationChannelGroupAsync( 487 'non-existent-channel-group-id' 488 ); 489 t.expect(channelGroup).toBe(null); 490 }); 491 492 if (Platform.OS === 'android' && Device.platformApiLevel >= 26) { 493 t.it('returns an object if there is such channel group', async () => { 494 await Notifications.setNotificationChannelGroupAsync( 495 testChannelGroupId, 496 testChannelGroup 497 ); 498 const channel = await Notifications.getNotificationChannelGroupAsync( 499 testChannelGroupId 500 ); 501 await Notifications.deleteNotificationChannelGroupAsync(testChannelGroupId); 502 t.expect(channel).toBeDefined(); 503 }); 504 } 505 }); 506 507 t.describe('getNotificationChannelGroupsAsync()', () => { 508 if (Platform.OS === 'android' && Device.platformApiLevel >= 28) { 509 t.it('returns an array', async () => { 510 const channels = await Notifications.getNotificationChannelGroupsAsync(); 511 t.expect(channels).toEqual(t.jasmine.any(Array)); 512 }); 513 514 t.it('returns existing channel groups', async () => { 515 const channel = await Notifications.setNotificationChannelGroupAsync( 516 testChannelGroupId, 517 testChannelGroup 518 ); 519 const channels = await Notifications.getNotificationChannelGroupsAsync(); 520 await Notifications.deleteNotificationChannelGroupAsync(testChannelGroupId); 521 t.expect(channels).toContain(channel); 522 }); 523 } else { 524 t.it("doesn't throw an error", async () => { 525 await Notifications.getNotificationChannelGroupsAsync(); 526 }); 527 } 528 }); 529 530 t.describe('setNotificationChannelGroupsAsync()', () => { 531 t.afterEach(async () => { 532 await Notifications.deleteNotificationChannelGroupAsync(testChannelGroupId); 533 }); 534 535 if (Platform.OS === 'android' && Device.platformApiLevel >= 26) { 536 t.it('returns the modified channel group', async () => { 537 const group = await Notifications.setNotificationChannelGroupAsync( 538 testChannelGroupId, 539 testChannelGroup 540 ); 541 t.expect(group).toEqual( 542 t.jasmine.objectContaining({ ...testChannelGroup, id: testChannelGroupId }) 543 ); 544 }); 545 546 t.it('creates a channel group', async () => { 547 const preChannelGroups = await Notifications.getNotificationChannelGroupsAsync(); 548 const channelGroupSpec = t.jasmine.objectContaining({ 549 ...testChannelGroup, 550 id: testChannelGroupId, 551 }); 552 t.expect(preChannelGroups).not.toContain(channelGroupSpec); 553 await Notifications.setNotificationChannelGroupAsync( 554 testChannelGroupId, 555 testChannelGroup 556 ); 557 const postChannelGroups = await Notifications.getNotificationChannelGroupsAsync(); 558 t.expect(postChannelGroups).toContain(channelGroupSpec); 559 t.expect(postChannelGroups.length).toBeGreaterThan(preChannelGroups.length); 560 }); 561 562 t.it('sets custom properties', async () => { 563 const createSpec = { 564 name: 'Test channel group', 565 description: 'Used by `test-suite`', 566 }; 567 const channelGroup = await Notifications.setNotificationChannelGroupAsync( 568 testChannelGroupId, 569 createSpec 570 ); 571 const groupSpec = { ...createSpec, id: testChannelGroupId }; 572 if (Device.platformApiLevel < 28) { 573 // Groups descriptions is only supported on API 28+ 574 delete groupSpec.description; 575 } 576 t.expect(channelGroup).toEqual( 577 t.jasmine.objectContaining({ ...groupSpec, id: testChannelGroupId }) 578 ); 579 }); 580 581 t.it('updates a channel group', async () => { 582 await Notifications.setNotificationChannelGroupAsync(testChannelGroupId, { 583 name: 'Name before change', 584 }); 585 await Notifications.setNotificationChannelGroupAsync(testChannelGroupId, { 586 name: 'Name after change', 587 }); 588 const channelGroups = await Notifications.getNotificationChannelGroupsAsync(); 589 t.expect(channelGroups).toContain( 590 t.jasmine.objectContaining({ 591 name: 'Name after change', 592 id: testChannelGroupId, 593 }) 594 ); 595 t.expect(channelGroups).not.toContain( 596 t.jasmine.objectContaining({ 597 name: 'Name before change', 598 id: testChannelGroupId, 599 }) 600 ); 601 }); 602 } else { 603 t.it("doesn't throw an error", async () => { 604 await Notifications.setNotificationChannelGroupAsync( 605 testChannelGroupId, 606 testChannelGroup 607 ); 608 }); 609 } 610 }); 611 }); 612 613 t.describe('Notification Categories', () => { 614 const vanillaButton = { 615 identifier: 'vanillaButton', 616 buttonTitle: 'Plain Option', 617 options: { 618 isDestructive: true, 619 isAuthenticationRequired: true, 620 opensAppToForeground: false, 621 }, 622 }; 623 const textResponseButton = { 624 identifier: 'textResponseButton', 625 buttonTitle: 'Click to Respond with Text', 626 options: { 627 isDestructive: true, 628 isAuthenticationRequired: true, 629 opensAppToForeground: true, 630 }, 631 textInput: { submitButtonTitle: 'Send', placeholder: 'Type Something' }, 632 }; 633 634 const testCategory1 = { 635 identifier: 'testNotificationCategory1', 636 actions: [vanillaButton], 637 options: { 638 previewPlaceholder: 'preview goes here', 639 customDismissAction: false, 640 allowInCarPlay: false, 641 showTitle: false, 642 showSubtitle: false, 643 allowAnnouncement: false, 644 }, 645 }; 646 const testCategory2 = { 647 identifier: 'testNotificationCategory2', 648 actions: [vanillaButton, textResponseButton], 649 options: { 650 customDismissAction: false, 651 allowInCarPlay: false, 652 showTitle: true, 653 showSubtitle: true, 654 allowAnnouncement: false, 655 }, 656 }; 657 658 const allTestCategoryIds = ['testNotificationCategory1', 'testNotificationCategory2']; 659 660 t.describe('getNotificationCategoriesAsync()', () => { 661 let existingCategoriesCount = 0; 662 t.beforeAll(async () => { 663 existingCategoriesCount = (await Notifications.getNotificationCategoriesAsync()).length; 664 }); 665 666 t.afterEach(async () => { 667 allTestCategoryIds.forEach(async id => { 668 await Notifications.deleteNotificationCategoryAsync(id); 669 }); 670 }); 671 672 t.it('returns an empty array if there are no categories', async () => { 673 t.expect((await Notifications.getNotificationCategoriesAsync()).length).toEqual( 674 existingCategoriesCount 675 ); 676 }); 677 678 t.it('returns an array with the just-created categories', async () => { 679 await Notifications.setNotificationCategoryAsync( 680 testCategory1.identifier, 681 testCategory1.actions, 682 testCategory1.options 683 ); 684 await Notifications.setNotificationCategoryAsync( 685 testCategory2.identifier, 686 testCategory2.actions, 687 testCategory2.options 688 ); 689 t.expect((await Notifications.getNotificationCategoriesAsync()).length).toEqual( 690 existingCategoriesCount + 2 691 ); 692 }); 693 }); 694 695 t.describe('setNotificationCategoriesAsync()', () => { 696 t.afterEach(async () => { 697 allTestCategoryIds.forEach(async id => { 698 await Notifications.deleteNotificationCategoryAsync(id); 699 }); 700 }); 701 t.it('creates a category with one action successfully', async () => { 702 const resultCategory = await Notifications.setNotificationCategoryAsync( 703 testCategory1.identifier, 704 testCategory1.actions, 705 testCategory1.options 706 ); 707 708 t.expect(testCategory1.identifier).toEqual(resultCategory.identifier); 709 testCategory1.actions.forEach((action, i) => { 710 t.expect(action.identifier).toEqual(resultCategory.actions[i].identifier); 711 t.expect(action.buttonTitle).toEqual(resultCategory.actions[i].buttonTitle); 712 t.expect(action.options).toEqual( 713 t.jasmine.objectContaining(resultCategory.actions[i].options) 714 ); 715 }); 716 t.expect(testCategory1.options).toEqual( 717 t.jasmine.objectContaining(resultCategory.options) 718 ); 719 }); 720 721 t.it('creates a category with two actions successfully', async () => { 722 const resultCategory = await Notifications.setNotificationCategoryAsync( 723 testCategory2.identifier, 724 testCategory2.actions, 725 testCategory2.options 726 ); 727 728 t.expect(testCategory2.identifier).toEqual(resultCategory.identifier); 729 testCategory2.actions.forEach((action, i) => { 730 t.expect(action.identifier).toEqual(resultCategory.actions[i].identifier); 731 t.expect(action.buttonTitle).toEqual(resultCategory.actions[i].buttonTitle); 732 t.expect(action.options).toEqual( 733 t.jasmine.objectContaining(resultCategory.actions[i].options) 734 ); 735 }); 736 t.expect(testCategory2.options).toEqual( 737 t.jasmine.objectContaining(resultCategory.options) 738 ); 739 }); 740 }); 741 742 t.describe('deleteNotificationCategoriesAsync()', () => { 743 t.afterEach(async () => { 744 allTestCategoryIds.forEach(async id => { 745 await Notifications.deleteNotificationCategoryAsync(id); 746 }); 747 }); 748 t.it('deleting a category that does not exist returns false', async () => { 749 const categoriesBefore = await Notifications.getNotificationCategoriesAsync(); 750 t.expect( 751 await Notifications.deleteNotificationCategoryAsync('nonExistentCategoryId') 752 ).toBe(false); 753 const categoriesAfter = await Notifications.getNotificationCategoriesAsync(); 754 t.expect(categoriesAfter.length).toEqual(categoriesBefore.length); 755 }); 756 757 t.it('deleting a category that does exist returns true', async () => { 758 await Notifications.setNotificationCategoryAsync( 759 testCategory2.identifier, 760 testCategory2.actions, 761 testCategory2.options 762 ); 763 t.expect( 764 await Notifications.deleteNotificationCategoryAsync('testNotificationCategory2') 765 ).toBe(true); 766 }); 767 768 t.it('returns an array of length 1 after creating 2 categories & deleting 1', async () => { 769 await Notifications.setNotificationCategoryAsync( 770 testCategory1.identifier, 771 testCategory1.actions, 772 testCategory1.options 773 ); 774 await Notifications.setNotificationCategoryAsync( 775 testCategory2.identifier, 776 testCategory2.actions, 777 testCategory2.options 778 ); 779 const categoriesBefore = await Notifications.getNotificationCategoriesAsync(); 780 await Notifications.deleteNotificationCategoryAsync('testNotificationCategory1'); 781 const categoriesAfter = await Notifications.getNotificationCategoriesAsync(); 782 t.expect(categoriesBefore.length - 1).toEqual(categoriesAfter.length); 783 }); 784 }); 785 }); 786 787 t.describe('getBadgeCountAsync', () => { 788 t.it('resolves with an integer', async () => { 789 const badgeCount = await Notifications.getBadgeCountAsync(); 790 t.expect(typeof badgeCount).toBe('number'); 791 }); 792 }); 793 794 t.describe('setBadgeCountAsync', () => { 795 t.it('resolves with a boolean', async () => { 796 const randomCounter = Math.ceil(Math.random() * 9) + 1; 797 const result = await Notifications.setBadgeCountAsync(randomCounter); 798 t.expect(typeof result).toBe('boolean'); 799 }); 800 801 t.it('sets a retrievable counter (if set succeeds)', async () => { 802 const randomCounter = Math.ceil(Math.random() * 9) + 1; 803 if (await Notifications.setBadgeCountAsync(randomCounter)) { 804 const badgeCount = await Notifications.getBadgeCountAsync(); 805 t.expect(badgeCount).toBe(randomCounter); 806 } else { 807 // TODO: add t.pending() when it starts to work 808 } 809 }); 810 811 t.it('clears the counter', async () => { 812 const clearingCounter = 0; 813 await Notifications.setBadgeCountAsync(clearingCounter); 814 const badgeCount = await Notifications.getBadgeCountAsync(); 815 t.expect(badgeCount).toBe(clearingCounter); 816 }); 817 }); 818 819 t.describe('getPresentedNotificationsAsync()', () => { 820 const identifier = 'test-containing-id'; 821 const notificationStatuses = {}; 822 823 t.beforeAll(() => { 824 Notifications.setNotificationHandler({ 825 handleNotification: async () => ({ 826 shouldShowAlert: true, 827 }), 828 handleSuccess: notificationId => { 829 notificationStatuses[notificationId] = true; 830 }, 831 }); 832 }); 833 834 t.it('resolves with an array containing a displayed notification', async () => { 835 await Notifications.presentNotificationAsync( 836 { 837 title: 'Sample title', 838 subtitle: 'What an event!', 839 body: 'An interesting event has just happened', 840 badge: 1, 841 }, 842 identifier 843 ); 844 await waitFor(1000); 845 const displayedNotifications = await Notifications.getPresentedNotificationsAsync(); 846 t.expect(displayedNotifications).toContain( 847 t.jasmine.objectContaining({ 848 request: t.jasmine.objectContaining({ 849 identifier, 850 }), 851 }) 852 ); 853 }); 854 855 t.it('resolves with an array that does not contain a canceled notification', async () => { 856 await Notifications.dismissNotificationAsync(identifier); 857 await waitFor(1000); 858 const displayedNotifications = await Notifications.getPresentedNotificationsAsync(); 859 t.expect(displayedNotifications).not.toContain( 860 t.jasmine.objectContaining({ 861 request: t.jasmine.objectContaining({ 862 identifier, 863 }), 864 }) 865 ); 866 }); 867 868 // TODO: Limited this test to Android platform only as only there we have the "Exponent notification" 869 if (Constants.appOwnership === 'expo' && Platform.OS === 'android') { 870 t.it('includes the foreign persistent notification', async () => { 871 const displayedNotifications = await Notifications.getPresentedNotificationsAsync(); 872 t.expect(displayedNotifications).toContain( 873 t.jasmine.objectContaining({ 874 request: t.jasmine.objectContaining({ 875 identifier: t.jasmine.stringMatching( 876 /^expo-notifications:\/\/foreign_notifications/ 877 ), 878 }), 879 }) 880 ); 881 }); 882 } 883 }); 884 885 t.describe('dismissNotificationAsync()', () => { 886 t.it('resolves for a valid notification ID', async () => { 887 const identifier = 'test-id'; 888 await Notifications.presentNotificationAsync({ 889 identifier, 890 title: 'Sample title', 891 subtitle: 'What an event!', 892 body: 'An interesting event has just happened', 893 badge: 1, 894 }); 895 await Notifications.dismissNotificationAsync(identifier); 896 }); 897 898 t.it('resolves for an invalid notification ID', async () => { 899 await Notifications.dismissNotificationAsync('no-such-notification-id'); 900 }); 901 }); 902 903 t.describe('dismissAllNotificationsAsync()', () => { 904 t.it('resolves', async () => { 905 await Notifications.dismissAllNotificationsAsync(); 906 }); 907 }); 908 909 t.describe('getAllScheduledNotificationsAsync', () => { 910 const identifier = 'test-scheduled-notification'; 911 const notification = { title: 'Scheduled notification' }; 912 913 t.afterEach(async () => { 914 await Notifications.cancelScheduledNotificationAsync(identifier); 915 }); 916 917 t.it('resolves with an Array', async () => { 918 const notifications = await Notifications.getAllScheduledNotificationsAsync(); 919 t.expect(notifications).toEqual(t.jasmine.arrayContaining([])); 920 }); 921 922 t.it('contains a scheduled notification', async () => { 923 const trigger = { 924 seconds: 10, 925 }; 926 await Notifications.scheduleNotificationAsync({ 927 identifier, 928 content: notification, 929 trigger, 930 }); 931 const notifications = await Notifications.getAllScheduledNotificationsAsync(); 932 t.expect(notifications).toContain( 933 t.jasmine.objectContaining({ 934 identifier, 935 content: t.jasmine.objectContaining(notification), 936 trigger: t.jasmine.objectContaining({ 937 repeats: false, 938 seconds: trigger.seconds, 939 type: 'timeInterval', 940 }), 941 }) 942 ); 943 }); 944 945 t.it('does not contain a canceled notification', async () => { 946 const trigger = { 947 seconds: 10, 948 }; 949 await Notifications.scheduleNotificationAsync({ 950 identifier, 951 content: notification, 952 trigger, 953 }); 954 await Notifications.cancelScheduledNotificationAsync(identifier); 955 const notifications = await Notifications.getAllScheduledNotificationsAsync(); 956 t.expect(notifications).not.toContain(t.jasmine.objectContaining({ identifier })); 957 }); 958 }); 959 960 t.describe('scheduleNotificationAsync', () => { 961 const identifier = 'test-scheduled-notification'; 962 const notification = { 963 title: 'Scheduled notification', 964 data: { key: 'value' }, 965 badge: 2, 966 vibrate: [100, 100, 100, 100, 100, 100], 967 color: '#FF0000', 968 }; 969 970 t.afterEach(async () => { 971 await Notifications.cancelScheduledNotificationAsync(identifier); 972 }); 973 974 t.it( 975 'triggers a notification which emits an event', 976 async () => { 977 const notificationReceivedSpy = t.jasmine.createSpy('notificationReceived'); 978 const subscription = Notifications.addNotificationReceivedListener( 979 notificationReceivedSpy 980 ); 981 await Notifications.scheduleNotificationAsync({ 982 identifier, 983 content: notification, 984 trigger: { seconds: 5 }, 985 }); 986 await waitFor(6000); 987 t.expect(notificationReceivedSpy).toHaveBeenCalled(); 988 subscription.remove(); 989 }, 990 10000 991 ); 992 993 t.it( 994 'throws an error if a user defines an invalid trigger (no repeats)', 995 async () => { 996 let error = undefined; 997 try { 998 await Notifications.scheduleNotificationAsync({ 999 identifier, 1000 content: notification, 1001 trigger: { seconds: 5, hour: 2 }, 1002 }); 1003 } catch (err) { 1004 error = err; 1005 } 1006 t.expect(error).toBeDefined(); 1007 }, 1008 10000 1009 ); 1010 1011 t.it( 1012 'throws an error if a user defines an invalid trigger (with repeats)', 1013 async () => { 1014 let error = undefined; 1015 try { 1016 await Notifications.scheduleNotificationAsync({ 1017 identifier, 1018 content: notification, 1019 trigger: { seconds: 5, repeats: true, hour: 2 }, 1020 }); 1021 } catch (err) { 1022 error = err; 1023 } 1024 t.expect(error).toBeDefined(); 1025 }, 1026 10000 1027 ); 1028 1029 t.it( 1030 'triggers a notification which contains the custom color', 1031 async () => { 1032 const notificationReceivedSpy = t.jasmine.createSpy('notificationReceived'); 1033 const subscription = Notifications.addNotificationReceivedListener( 1034 notificationReceivedSpy 1035 ); 1036 await Notifications.scheduleNotificationAsync({ 1037 identifier, 1038 content: notification, 1039 trigger: { seconds: 5 }, 1040 }); 1041 await waitFor(6000); 1042 t.expect(notificationReceivedSpy).toHaveBeenCalled(); 1043 if (Platform.OS === 'android') { 1044 t.expect(notificationReceivedSpy).toHaveBeenCalledWith( 1045 t.jasmine.objectContaining({ 1046 request: t.jasmine.objectContaining({ 1047 content: t.jasmine.objectContaining({ 1048 // #AARRGGBB 1049 color: '#FFFF0000', 1050 }), 1051 }), 1052 }) 1053 ); 1054 } 1055 subscription.remove(); 1056 }, 1057 10000 1058 ); 1059 1060 t.it( 1061 'triggers a notification which triggers the handler (`seconds` trigger)', 1062 async () => { 1063 let notificationFromEvent = undefined; 1064 Notifications.setNotificationHandler({ 1065 handleNotification: async event => { 1066 notificationFromEvent = event; 1067 return { 1068 shouldShowAlert: true, 1069 }; 1070 }, 1071 }); 1072 await Notifications.scheduleNotificationAsync({ 1073 identifier, 1074 content: notification, 1075 trigger: { seconds: 5 }, 1076 }); 1077 await waitFor(6000); 1078 t.expect(notificationFromEvent).toBeDefined(); 1079 Notifications.setNotificationHandler(null); 1080 }, 1081 10000 1082 ); 1083 1084 t.it( 1085 'triggers a notification which triggers the handler (with custom sound)', 1086 async () => { 1087 let notificationFromEvent = undefined; 1088 Notifications.setNotificationHandler({ 1089 handleNotification: async event => { 1090 notificationFromEvent = event; 1091 return { 1092 shouldShowAlert: true, 1093 shouldPlaySound: true, 1094 }; 1095 }, 1096 }); 1097 await Notifications.scheduleNotificationAsync({ 1098 identifier, 1099 content: { 1100 ...notification, 1101 sound: 'notification.wav', 1102 }, 1103 trigger: { seconds: 5 }, 1104 }); 1105 await waitFor(6000); 1106 t.expect(notificationFromEvent).toBeDefined(); 1107 t.expect(notificationFromEvent).toEqual( 1108 t.jasmine.objectContaining({ 1109 request: t.jasmine.objectContaining({ 1110 content: t.jasmine.objectContaining({ 1111 sound: 'custom', 1112 }), 1113 }), 1114 }) 1115 ); 1116 Notifications.setNotificationHandler(null); 1117 }, 1118 10000 1119 ); 1120 1121 t.it( 1122 'triggers a notification which triggers the handler (with custom sound set, but not existent)', 1123 async () => { 1124 let notificationFromEvent = undefined; 1125 Notifications.setNotificationHandler({ 1126 handleNotification: async event => { 1127 notificationFromEvent = event; 1128 return { 1129 shouldShowAlert: true, 1130 shouldPlaySound: true, 1131 }; 1132 }, 1133 }); 1134 await Notifications.scheduleNotificationAsync({ 1135 identifier, 1136 content: { 1137 ...notification, 1138 sound: 'no-such-file.wav', 1139 }, 1140 trigger: { seconds: 5 }, 1141 }); 1142 await waitFor(6000); 1143 t.expect(notificationFromEvent).toBeDefined(); 1144 t.expect(notificationFromEvent).toEqual( 1145 t.jasmine.objectContaining({ 1146 request: t.jasmine.objectContaining({ 1147 content: t.jasmine.objectContaining({ 1148 sound: 'custom', 1149 }), 1150 }), 1151 }) 1152 ); 1153 Notifications.setNotificationHandler(null); 1154 }, 1155 10000 1156 ); 1157 1158 t.it( 1159 'triggers a notification which triggers the handler (`Date` trigger)', 1160 async () => { 1161 let notificationFromEvent = undefined; 1162 Notifications.setNotificationHandler({ 1163 handleNotification: async event => { 1164 notificationFromEvent = event; 1165 return { 1166 shouldShowAlert: true, 1167 }; 1168 }, 1169 }); 1170 const trigger = new Date(Date.now() + 5 * 1000); 1171 await Notifications.scheduleNotificationAsync({ 1172 identifier, 1173 content: notification, 1174 trigger, 1175 }); 1176 await waitFor(6000); 1177 t.expect(notificationFromEvent).toBeDefined(); 1178 Notifications.setNotificationHandler(null); 1179 }, 1180 10000 1181 ); 1182 1183 t.it( 1184 'schedules a repeating daily notification; only first scheduled event is verified.', 1185 async () => { 1186 const dateNow = new Date(); 1187 const trigger = { 1188 hour: dateNow.getHours(), 1189 minute: (dateNow.getMinutes() + 2) % 60, 1190 repeats: true, 1191 }; 1192 await Notifications.scheduleNotificationAsync({ 1193 identifier, 1194 content: notification, 1195 trigger, 1196 }); 1197 const result = await Notifications.getAllScheduledNotificationsAsync(); 1198 delete trigger.repeats; 1199 if (Platform.OS === 'android') { 1200 t.expect(result[0].trigger).toEqual({ 1201 type: 'daily', 1202 channelId: null, 1203 ...trigger, 1204 }); 1205 } else if (Platform.OS === 'ios') { 1206 t.expect(result[0].trigger).toEqual({ 1207 type: 'calendar', 1208 class: 'UNCalendarNotificationTrigger', 1209 repeats: true, 1210 dateComponents: { 1211 ...trigger, 1212 timeZone: null, 1213 isLeapMonth: false, 1214 calendar: null, 1215 }, 1216 }); 1217 } else { 1218 throw new Error('Test does not support platform'); 1219 } 1220 }, 1221 4000 1222 ); 1223 1224 t.it( 1225 'schedules a repeating weekly notification; only first scheduled event is verified.', 1226 async () => { 1227 const dateNow = new Date(); 1228 const trigger = { 1229 // JS weekday range equals 0 to 6, Sunday equals 0 1230 // Native weekday range equals 1 to 7, Sunday equals 1 1231 weekday: dateNow.getDay() + 1, 1232 hour: dateNow.getHours(), 1233 minute: (dateNow.getMinutes() + 2) % 60, 1234 repeats: true, 1235 }; 1236 await Notifications.scheduleNotificationAsync({ 1237 identifier, 1238 content: notification, 1239 trigger, 1240 }); 1241 const result = await Notifications.getAllScheduledNotificationsAsync(); 1242 delete trigger.repeats; 1243 if (Platform.OS === 'android') { 1244 t.expect(result[0].trigger).toEqual({ 1245 type: 'weekly', 1246 channelId: null, 1247 ...trigger, 1248 }); 1249 } else if (Platform.OS === 'ios') { 1250 t.expect(result[0].trigger).toEqual({ 1251 type: 'calendar', 1252 class: 'UNCalendarNotificationTrigger', 1253 repeats: true, 1254 dateComponents: { 1255 ...trigger, 1256 timeZone: null, 1257 isLeapMonth: false, 1258 calendar: null, 1259 }, 1260 }); 1261 } else { 1262 throw new Error('Test does not support platform'); 1263 } 1264 }, 1265 4000 1266 ); 1267 1268 t.it( 1269 'schedules a repeating yearly notification; only first scheduled event is verified.', 1270 async () => { 1271 const dateNow = new Date(); 1272 const trigger = { 1273 day: dateNow.getDate(), 1274 month: dateNow.getMonth(), 1275 hour: dateNow.getHours(), 1276 minute: (dateNow.getMinutes() + 2) % 60, 1277 repeats: true, 1278 }; 1279 await Notifications.scheduleNotificationAsync({ 1280 identifier, 1281 content: notification, 1282 trigger, 1283 }); 1284 const result = await Notifications.getAllScheduledNotificationsAsync(); 1285 delete trigger.repeats; 1286 if (Platform.OS === 'android') { 1287 t.expect(result[0].trigger).toEqual({ 1288 type: 'yearly', 1289 channelId: null, 1290 ...trigger, 1291 }); 1292 } else if (Platform.OS === 'ios') { 1293 t.expect(result[0].trigger).toEqual({ 1294 type: 'calendar', 1295 class: 'UNCalendarNotificationTrigger', 1296 repeats: true, 1297 dateComponents: { 1298 ...trigger, 1299 // iOS uses 1-12 based months 1300 month: trigger.month + 1, 1301 timeZone: null, 1302 isLeapMonth: false, 1303 calendar: null, 1304 }, 1305 }); 1306 } else { 1307 throw new Error('Test does not support platform'); 1308 } 1309 }, 1310 4000 1311 ); 1312 1313 // iOS rejects with "time interval must be at least 60 if repeating" 1314 // and having a test running for more than 60 seconds may be too 1315 // time-consuming to maintain 1316 if (Platform.OS !== 'ios') { 1317 t.it( 1318 'triggers a repeating notification which emits events', 1319 async () => { 1320 let timesSpyHasBeenCalled = 0; 1321 const subscription = Notifications.addNotificationReceivedListener(() => { 1322 timesSpyHasBeenCalled += 1; 1323 }); 1324 await Notifications.scheduleNotificationAsync({ 1325 identifier, 1326 content: notification, 1327 trigger: { 1328 seconds: 5, 1329 repeats: true, 1330 }, 1331 }); 1332 await waitFor(12000); 1333 t.expect(timesSpyHasBeenCalled).toBeGreaterThan(1); 1334 subscription.remove(); 1335 }, 1336 16000 1337 ); 1338 } 1339 1340 if (Platform.OS === 'ios') { 1341 t.it( 1342 'schedules a notification with calendar trigger', 1343 async () => { 1344 const notificationReceivedSpy = t.jasmine.createSpy('notificationReceived'); 1345 const subscription = Notifications.addNotificationReceivedListener( 1346 notificationReceivedSpy 1347 ); 1348 await Notifications.scheduleNotificationAsync({ 1349 identifier, 1350 content: notification, 1351 trigger: { 1352 second: (new Date().getSeconds() + 5) % 60, 1353 }, 1354 }); 1355 await waitFor(6000); 1356 t.expect(notificationReceivedSpy).toHaveBeenCalled(); 1357 subscription.remove(); 1358 }, 1359 16000 1360 ); 1361 } 1362 }); 1363 1364 t.describe('getNextTriggerDateAsync', () => { 1365 if (Platform.OS === 'ios') { 1366 t.it('generates trigger date for a calendar trigger', async () => { 1367 const nextDate = await Notifications.getNextTriggerDateAsync({ month: 1, hour: 9 }); 1368 t.expect(nextDate).not.toBeNull(); 1369 }); 1370 } else { 1371 t.it('fails to generate trigger date for a calendar trigger', async () => { 1372 let exception = null; 1373 try { 1374 await Notifications.getNextTriggerDateAsync({ month: 1, hour: 9, repeats: true }); 1375 } catch (e) { 1376 exception = e; 1377 } 1378 t.expect(exception).toBeDefined(); 1379 }); 1380 } 1381 1382 t.it('generates trigger date for a daily trigger', async () => { 1383 const nextDate = await Notifications.getNextTriggerDateAsync({ 1384 hour: 9, 1385 minute: 20, 1386 repeats: true, 1387 }); 1388 t.expect(nextDate).not.toBeNull(); 1389 t.expect(new Date(nextDate).getHours()).toBe(9); 1390 t.expect(new Date(nextDate).getMinutes()).toBe(20); 1391 }); 1392 1393 t.it('generates trigger date for a weekly trigger', async () => { 1394 const nextDateTimestamp = await Notifications.getNextTriggerDateAsync({ 1395 weekday: 2, 1396 hour: 9, 1397 minute: 20, 1398 repeats: true, 1399 }); 1400 t.expect(nextDateTimestamp).not.toBeNull(); 1401 const nextDate = new Date(nextDateTimestamp); 1402 // JS has 0 (Sunday) - 6 (Saturday) based week days 1403 t.expect(nextDate.getDay()).toBe(1); 1404 t.expect(nextDate.getHours()).toBe(9); 1405 t.expect(nextDate.getMinutes()).toBe(20); 1406 }); 1407 1408 t.it('generates trigger date for a yearly trigger', async () => { 1409 const nextDateTimestamp = await Notifications.getNextTriggerDateAsync({ 1410 day: 2, 1411 month: 6, 1412 hour: 9, 1413 minute: 20, 1414 repeats: true, 1415 }); 1416 t.expect(nextDateTimestamp).not.toBeNull(); 1417 const nextDate = new Date(nextDateTimestamp); 1418 t.expect(nextDate.getDate()).toBe(2); 1419 t.expect(nextDate.getMonth()).toBe(6); 1420 t.expect(nextDate.getHours()).toBe(9); 1421 t.expect(nextDate.getMinutes()).toBe(20); 1422 }); 1423 1424 t.it('fails to generate trigger date for the immediate trigger', async () => { 1425 let exception = null; 1426 try { 1427 await Notifications.getNextTriggerDateAsync({ channelId: 'test-channel-id' }); 1428 } catch (e) { 1429 exception = e; 1430 } 1431 t.expect(exception).toBeDefined(); 1432 }); 1433 }); 1434 1435 t.describe('cancelScheduledNotificationAsync', () => { 1436 const identifier = 'test-scheduled-canceled-notification'; 1437 const notification = { title: 'Scheduled, canceled notification' }; 1438 1439 t.it( 1440 'makes a scheduled notification not trigger', 1441 async () => { 1442 const notificationReceivedSpy = t.jasmine.createSpy('notificationReceived'); 1443 const subscription = Notifications.addNotificationReceivedListener( 1444 notificationReceivedSpy 1445 ); 1446 await Notifications.scheduleNotificationAsync({ 1447 identifier, 1448 content: notification, 1449 trigger: { seconds: 5 }, 1450 }); 1451 await Notifications.cancelScheduledNotificationAsync(identifier); 1452 await waitFor(6000); 1453 t.expect(notificationReceivedSpy).not.toHaveBeenCalled(); 1454 subscription.remove(); 1455 }, 1456 10000 1457 ); 1458 }); 1459 1460 t.describe('cancelAllScheduledNotificationsAsync', () => { 1461 const notification = { title: 'Scheduled, canceled notification' }; 1462 1463 t.it( 1464 'removes all scheduled notifications', 1465 async () => { 1466 const notificationReceivedSpy = t.jasmine.createSpy('notificationReceived'); 1467 const subscription = Notifications.addNotificationReceivedListener( 1468 notificationReceivedSpy 1469 ); 1470 for (let i = 0; i < 3; i += 1) { 1471 await Notifications.scheduleNotificationAsync({ 1472 identifier: `notification-${i}`, 1473 content: notification, 1474 trigger: { seconds: 5 }, 1475 }); 1476 } 1477 await Notifications.cancelAllScheduledNotificationsAsync(); 1478 await waitFor(6000); 1479 t.expect(notificationReceivedSpy).not.toHaveBeenCalled(); 1480 subscription.remove(); 1481 const scheduledNotifications = await Notifications.getAllScheduledNotificationsAsync(); 1482 t.expect(scheduledNotifications.length).toEqual(0); 1483 }, 1484 10000 1485 ); 1486 }); 1487 1488 const onlyInteractiveDescribe = isInteractive ? t.describe : t.xdescribe; 1489 onlyInteractiveDescribe('when the app is in background', () => { 1490 let subscription = null; 1491 let handleNotificationSpy = null; 1492 let handleSuccessSpy = null; 1493 let handleErrorSpy = null; 1494 let notificationReceivedSpy = null; 1495 1496 t.beforeEach(async () => { 1497 handleNotificationSpy = t.jasmine.createSpy('handleNotificationSpy'); 1498 handleSuccessSpy = t.jasmine.createSpy('handleSuccessSpy'); 1499 handleErrorSpy = t.jasmine.createSpy('handleErrorSpy').and.callFake((...args) => { 1500 console.log(args); 1501 }); 1502 notificationReceivedSpy = t.jasmine.createSpy('notificationReceivedSpy'); 1503 Notifications.setNotificationHandler({ 1504 handleNotification: handleNotificationSpy, 1505 handleSuccess: handleSuccessSpy, 1506 handleError: handleErrorSpy, 1507 }); 1508 subscription = Notifications.addNotificationReceivedListener(notificationReceivedSpy); 1509 }); 1510 1511 t.afterEach(() => { 1512 if (subscription) { 1513 subscription.remove(); 1514 subscription = null; 1515 } 1516 Notifications.setNotificationHandler(null); 1517 handleNotificationSpy = null; 1518 handleSuccessSpy = null; 1519 handleErrorSpy = null; 1520 notificationReceivedSpy = null; 1521 }); 1522 1523 t.it( 1524 'shows the notification', 1525 // without async-await the code is executed immediately after opening the screen 1526 async () => 1527 await new Promise((resolve, reject) => { 1528 const secondsToTimeout = 5; 1529 let notificationSent = false; 1530 Alert.alert(`Please move the app to the background and wait for 5 seconds`); 1531 let userInteractionTimeout = null; 1532 async function handleStateChange(state) { 1533 const identifier = 'test-interactive-notification'; 1534 if (state === 'background' && !notificationSent) { 1535 if (userInteractionTimeout) { 1536 clearInterval(userInteractionTimeout); 1537 userInteractionTimeout = null; 1538 } 1539 await Notifications.scheduleNotificationAsync({ 1540 identifier, 1541 content: { 1542 title: 'Hello from the application!', 1543 message: 1544 'You can now return to the app and let the test know the notification has been shown.', 1545 }, 1546 trigger: { seconds: 1 }, 1547 }); 1548 notificationSent = true; 1549 } else if (state === 'active' && notificationSent) { 1550 const notificationWasShown = await askUserYesOrNo('Was the notification shown?'); 1551 t.expect(notificationWasShown).toBeTruthy(); 1552 t.expect(handleNotificationSpy).not.toHaveBeenCalled(); 1553 t.expect(handleSuccessSpy).not.toHaveBeenCalled(); 1554 t.expect(handleErrorSpy).not.toHaveBeenCalledWith(identifier); 1555 t.expect(notificationReceivedSpy).not.toHaveBeenCalled(); 1556 AppState.removeEventListener('change', handleStateChange); 1557 resolve(); 1558 } 1559 } 1560 userInteractionTimeout = setTimeout(() => { 1561 console.warn( 1562 "Scheduled notification test was skipped and marked as successful. It required user interaction which hasn't occured in time." 1563 ); 1564 AppState.removeEventListener('change', handleStateChange); 1565 Alert.alert( 1566 'Scheduled notification test was skipped', 1567 `The test required user interaction which hasn't occurred in time (${secondsToTimeout} seconds). It has been marked as passing. Better luck next time!` 1568 ); 1569 resolve(); 1570 }, secondsToTimeout * 1000); 1571 AppState.addEventListener('change', handleStateChange); 1572 }), 1573 30000 1574 ); 1575 }); 1576 1577 onlyInteractiveDescribe('tapping on a notification', () => { 1578 let subscription = null; 1579 let event = null; 1580 1581 t.beforeEach(async () => { 1582 Notifications.setNotificationHandler({ 1583 handleNotification: async () => ({ 1584 shouldShowAlert: true, 1585 }), 1586 }); 1587 subscription = Notifications.addNotificationResponseReceivedListener(anEvent => { 1588 event = anEvent; 1589 }); 1590 }); 1591 1592 t.afterEach(() => { 1593 if (subscription) { 1594 subscription.remove(); 1595 subscription = null; 1596 } 1597 Notifications.setNotificationHandler(null); 1598 event = null; 1599 }); 1600 1601 t.it( 1602 'calls the “notification response received” listener with default action identifier', 1603 async () => { 1604 const secondsToTimeout = 5; 1605 const shouldRun = await Promise.race([ 1606 askUserYesOrNo('Could you tap on a notification when it shows?'), 1607 waitFor(secondsToTimeout * 1000), 1608 ]); 1609 if (!shouldRun) { 1610 console.warn( 1611 "Notification response test was skipped and marked as successful. It required user interaction which hasn't occured in time." 1612 ); 1613 Alert.alert( 1614 'Notification response test was skipped', 1615 `The test required user interaction which hasn't occurred in time (${secondsToTimeout} seconds). It has been marked as passing. Better luck next time!` 1616 ); 1617 return; 1618 } 1619 const notificationSpec = { 1620 title: 'Tap me!', 1621 body: 'Better be quick!', 1622 }; 1623 await Notifications.presentNotificationAsync(notificationSpec); 1624 let iterations = 0; 1625 while (iterations < 5) { 1626 iterations += 1; 1627 if (event) { 1628 break; 1629 } 1630 await waitFor(1000); 1631 } 1632 t.expect(event).not.toBeNull(); 1633 t.expect(event.actionIdentifier).toBe(Notifications.DEFAULT_ACTION_IDENTIFIER); 1634 t.expect(event.notification).toEqual( 1635 t.jasmine.objectContaining({ 1636 request: t.jasmine.objectContaining({ 1637 content: t.jasmine.objectContaining(notificationSpec), 1638 }), 1639 }) 1640 ); 1641 t.expect(event).toEqual(await Notifications.getLastNotificationResponseAsync()); 1642 }, 1643 10000 1644 ); 1645 }); 1646 1647 onlyInteractiveDescribe( 1648 'triggers a repeating daily notification. only first scheduled event is awaited and verified.', 1649 () => { 1650 let timesSpyHasBeenCalled = 0; 1651 const identifier = 'test-scheduled-notification'; 1652 const notification = { 1653 title: 'Scheduled notification', 1654 data: { key: 'value' }, 1655 badge: 2, 1656 vibrate: [100, 100, 100, 100, 100, 100], 1657 color: '#FF0000', 1658 }; 1659 1660 t.beforeEach(async () => { 1661 await Notifications.cancelAllScheduledNotificationsAsync(); 1662 Notifications.setNotificationHandler({ 1663 handleNotification: async () => { 1664 timesSpyHasBeenCalled += 1; 1665 return { 1666 shouldShowAlert: false, 1667 }; 1668 }, 1669 }); 1670 }); 1671 1672 t.afterEach(async () => { 1673 Notifications.setNotificationHandler(null); 1674 await Notifications.cancelAllScheduledNotificationsAsync(); 1675 }); 1676 1677 t.it( 1678 'triggers a repeating daily notification. only first event is verified.', 1679 async () => { 1680 // On iOS because we are using the calendar with repeat, it needs to be 1681 // greater than 60 seconds 1682 const triggerDate = new Date( 1683 new Date().getTime() + (Platform.OS === 'ios' ? 120000 : 60000) 1684 ); 1685 await Notifications.scheduleNotificationAsync({ 1686 identifier, 1687 content: notification, 1688 trigger: { 1689 hour: triggerDate.getHours(), 1690 minute: triggerDate.getMinutes(), 1691 repeats: true, 1692 }, 1693 }); 1694 const scheduledTime = new Date(triggerDate); 1695 scheduledTime.setSeconds(0); 1696 scheduledTime.setMilliseconds(0); 1697 const milliSecondsToWait = scheduledTime - new Date().getTime() + 2000; 1698 await waitFor(milliSecondsToWait); 1699 t.expect(timesSpyHasBeenCalled).toBe(1); 1700 }, 1701 140000 1702 ); 1703 } 1704 ); 1705 1706 onlyInteractiveDescribe( 1707 'triggers a repeating weekly notification. only first scheduled event is awaited and verified.', 1708 () => { 1709 let timesSpyHasBeenCalled = 0; 1710 const identifier = 'test-scheduled-notification'; 1711 const notification = { 1712 title: 'Scheduled notification', 1713 data: { key: 'value' }, 1714 badge: 2, 1715 vibrate: [100, 100, 100, 100, 100, 100], 1716 color: '#FF0000', 1717 }; 1718 1719 t.beforeEach(async () => { 1720 await Notifications.cancelAllScheduledNotificationsAsync(); 1721 Notifications.setNotificationHandler({ 1722 handleNotification: async () => { 1723 timesSpyHasBeenCalled += 1; 1724 return { 1725 shouldShowAlert: false, 1726 }; 1727 }, 1728 }); 1729 }); 1730 1731 t.afterEach(async () => { 1732 Notifications.setNotificationHandler(null); 1733 await Notifications.cancelAllScheduledNotificationsAsync(); 1734 }); 1735 1736 t.it( 1737 'triggers a repeating weekly notification. only first event is verified.', 1738 async () => { 1739 // On iOS because we are using the calendar with repeat, it needs to be 1740 // greater than 60 seconds 1741 const triggerDate = new Date( 1742 new Date().getTime() + (Platform.OS === 'ios' ? 120000 : 60000) 1743 ); 1744 await Notifications.scheduleNotificationAsync({ 1745 identifier, 1746 content: notification, 1747 trigger: { 1748 // JS weekday range equals 0 to 6, Sunday equals 0 1749 // Native weekday range equals 1 to 7, Sunday equals 1 1750 weekday: triggerDate.getDay() + 1, 1751 hour: triggerDate.getHours(), 1752 minute: triggerDate.getMinutes(), 1753 repeats: true, 1754 }, 1755 }); 1756 const scheduledTime = new Date(triggerDate); 1757 scheduledTime.setSeconds(0); 1758 scheduledTime.setMilliseconds(0); 1759 const milliSecondsToWait = scheduledTime - new Date().getTime() + 2000; 1760 await waitFor(milliSecondsToWait); 1761 t.expect(timesSpyHasBeenCalled).toBe(1); 1762 }, 1763 140000 1764 ); 1765 } 1766 ); 1767 1768 onlyInteractiveDescribe( 1769 'triggers a repeating yearly notification. only first scheduled event is awaited and verified.', 1770 () => { 1771 let timesSpyHasBeenCalled = 0; 1772 const identifier = 'test-scheduled-notification'; 1773 const notification = { 1774 title: 'Scheduled notification', 1775 data: { key: 'value' }, 1776 badge: 2, 1777 vibrate: [100, 100, 100, 100, 100, 100], 1778 color: '#FF0000', 1779 }; 1780 1781 t.beforeEach(async () => { 1782 await Notifications.cancelAllScheduledNotificationsAsync(); 1783 Notifications.setNotificationHandler({ 1784 handleNotification: async () => { 1785 timesSpyHasBeenCalled += 1; 1786 return { 1787 shouldShowAlert: false, 1788 }; 1789 }, 1790 }); 1791 }); 1792 1793 t.afterEach(async () => { 1794 Notifications.setNotificationHandler(null); 1795 await Notifications.cancelAllScheduledNotificationsAsync(); 1796 }); 1797 1798 t.it( 1799 'triggers a repeating yearly notification. only first event is verified.', 1800 async () => { 1801 // On iOS because we are using the calendar with repeat, it needs to be 1802 // greater than 60 seconds 1803 const triggerDate = new Date( 1804 new Date().getTime() + (Platform.OS === 'ios' ? 120000 : 60000) 1805 ); 1806 await Notifications.scheduleNotificationAsync({ 1807 identifier, 1808 content: notification, 1809 trigger: { 1810 day: triggerDate.getDate(), 1811 month: triggerDate.getMonth(), 1812 hour: triggerDate.getHours(), 1813 minute: triggerDate.getMinutes(), 1814 repeats: true, 1815 }, 1816 }); 1817 const scheduledTime = new Date(triggerDate); 1818 scheduledTime.setSeconds(0); 1819 scheduledTime.setMilliseconds(0); 1820 const milliSecondsToWait = scheduledTime - new Date().getTime() + 2000; 1821 await waitFor(milliSecondsToWait); 1822 t.expect(timesSpyHasBeenCalled).toBe(1); 1823 }, 1824 140000 1825 ); 1826 } 1827 ); 1828 }); 1829} 1830 1831// In this test app we contact the Expo push service directly. You *never* 1832// should do this in a real app. You should always store the push tokens on your 1833// own server or use the local notification API if you want to notify this user. 1834const PUSH_ENDPOINT = 'https://expo.io/--/api/v2/push/send'; 1835 1836async function sendTestPushNotification(expoPushToken, notificationOverrides) { 1837 // POST the token to the Expo push server 1838 const response = await fetch(PUSH_ENDPOINT, { 1839 method: 'POST', 1840 headers: { 1841 Accept: 'application/json', 1842 'Content-Type': 'application/json', 1843 }, 1844 body: JSON.stringify([ 1845 // No specific channel ID forces the package to create a fallback channel 1846 // to present the notification on newer Android devices. One of the tests 1847 // ensures that the fallback channel is created. 1848 { 1849 to: expoPushToken, 1850 title: 'Hello from Expo server!', 1851 data: { 1852 fieldTestedInDataContentsTest: 42, // <- it's true, do not remove it 1853 firstLevelString: 'value', 1854 firstLevelObject: { 1855 secondLevelInteger: 2137, 1856 secondLevelObject: { 1857 thirdLevelList: [21, 3, 1995, null, 4, 15], 1858 thirdLevelNull: null, 1859 }, 1860 }, 1861 }, 1862 ...notificationOverrides, 1863 }, 1864 ]), 1865 }); 1866 1867 const result = await response.json(); 1868 if (result.errors) { 1869 for (const error of result.errors) { 1870 console.warn(`API error sending push notification:`, error); 1871 } 1872 throw new Error('API error has occurred.'); 1873 } 1874 1875 const receipts = result.data; 1876 if (receipts) { 1877 const receipt = receipts[0]; 1878 if (receipt.status === 'error') { 1879 if (receipt.details) { 1880 console.warn( 1881 `Expo push service reported an error sending a notification: ${receipt.details.error}` 1882 ); 1883 } 1884 if (receipt.__debug) { 1885 console.warn(receipt.__debug); 1886 } 1887 throw new Error(`API error has occurred: ${receipt.details.error}`); 1888 } 1889 } 1890} 1891 1892function askUserYesOrNo(title, message = '') { 1893 return new Promise((resolve, reject) => { 1894 try { 1895 Alert.alert( 1896 title, 1897 message, 1898 [ 1899 { 1900 text: 'No', 1901 onPress: () => resolve(false), 1902 }, 1903 { 1904 text: 'Yes', 1905 onPress: () => resolve(true), 1906 }, 1907 ], 1908 { onDismiss: () => resolve(false) } 1909 ); 1910 } catch (e) { 1911 reject(e); 1912 } 1913 }); 1914} 1915