xref: /expo/apps/test-suite/tests/Notifications.js (revision 4bf00a55)
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