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