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