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