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