xref: /expo/apps/test-suite/tests/Notifications.js (revision 8ef929c3)
1'use strict';
2
3import { isMatch } from 'lodash';
4import { Platform } from 'react-native';
5import { Notifications, Permissions } from 'expo';
6
7import { waitFor } from './helpers';
8import * as TestUtils from '../TestUtils';
9
10export const name = 'Notifications';
11
12const localNotificationFactory = overrides => ({
13  title: 'Notification title',
14  body: 'Body text of the notification',
15  ...overrides,
16});
17
18// We need to accumulate notifications, because listener of current test
19// was being notified of notifications scheduled by previous tests.
20const waitForCallOfListener = (notificationOverrides, options) =>
21  new Promise(async (resolve, reject) => {
22    let timeout = options ? options.timeout : null;
23    let notificationsCount = (options ? options.notificationsCount : null) || (timeout ? null : 1);
24    let executorFunction =
25      (options ? options.executor : null) || Notifications.presentLocalNotificationAsync;
26    let subscription = null;
27    let notifications = [];
28
29    const finish = () => {
30      if (subscription) {
31        subscription.remove();
32      }
33      resolve(notifications);
34    };
35
36    const listener = notification => {
37      notifications.push(notification);
38      if (notificationsCount === notifications.length) {
39        finish();
40      }
41    };
42    subscription = Notifications.addListener(listener);
43    executorFunction(localNotificationFactory(notificationOverrides)).catch(reject);
44    if (timeout) {
45      await waitFor(timeout);
46      finish();
47    }
48  });
49
50export async function test(t) {
51  const shouldSkipTestsRequiringPermissions = await TestUtils.shouldSkipTestsRequiringPermissionsAsync();
52  const describeWithPermissions = shouldSkipTestsRequiringPermissions ? t.xdescribe : t.describe;
53
54  describeWithPermissions('Notifications', () => {
55    t.beforeAll(async () => {
56      await Permissions.askAsync(Permissions.NOTIFICATIONS);
57    });
58
59    t.afterEach(async () => {
60      if (Platform.OS === 'android') {
61        await Notifications.dismissAllNotificationsAsync();
62      }
63    });
64
65    t.describe('getExpoPushTokenAsync', () => {
66      t.it('resolves with a string', async () => {
67        const expoPushToken = await Notifications.getExpoPushTokenAsync();
68        t.expect(typeof expoPushToken === 'string').toBe(true);
69      });
70    });
71
72    t.describe('presentLocalNotificationAsync', () => {
73      t.it('resolves with notificationId', async () => {
74        let error = null;
75        try {
76          const notificationId = await Notifications.presentLocalNotificationAsync(
77            localNotificationFactory()
78          );
79          t.expect(notificationId).toBeDefined();
80        } catch (e) {
81          error = e;
82        }
83        t.expect(error).toBeNull();
84      });
85
86      // It turns out iOS rejects such notifications, while Android does not.
87      if (Platform.OS === 'ios') {
88        t.it('rejects notification with empty body', async () => {
89          let error = null;
90          try {
91            await Notifications.presentLocalNotificationAsync(
92              localNotificationFactory({ body: null })
93            );
94          } catch (e) {
95            error = e;
96          }
97          t.expect(error).not.toBeNull();
98        });
99      }
100
101      t.it('rejects notification with empty title', async () => {
102        let error = null;
103        try {
104          await Notifications.presentLocalNotificationAsync(
105            localNotificationFactory({ title: null })
106          );
107        } catch (e) {
108          error = e;
109        }
110        t.expect(error).not.toBeNull();
111      });
112    });
113
114    t.describe('addListener', () => {
115      t.it('is notified of new notifications', async () => {
116        const notificationsListener = t.jasmine.createSpy('notificationsListener');
117        const subscription = Notifications.addListener(notificationsListener);
118        await Notifications.presentLocalNotificationAsync(localNotificationFactory());
119        await waitFor(500);
120        t.expect(notificationsListener).toHaveBeenCalled();
121        subscription.remove();
122      });
123
124      t.it('reported notifications have origin=received', async () => {
125        const notifications = await waitForCallOfListener();
126        t.expect(isMatch(notifications[0], { origin: 'received' })).toBe(true);
127      });
128
129      t.it('reported notifications have proper data attached', async () => {
130        const data = { scheduledAt: new Date().getTime() };
131        const notifications = await waitForCallOfListener({ data }, { timeout: 1000 });
132        let hasMatched = false;
133        notifications.forEach(notification => {
134          hasMatched = hasMatched || isMatch(notification, { data });
135        });
136        t.expect(hasMatched).toBe(true);
137      });
138    });
139
140    t.describe('scheduleLocalNotificationAsync', () => {
141      // Android schedules notifications in a too unpredictable manner for it to be testable.
142      if (Platform.OS === 'ios') {
143        t.it('schedules local notifications', async () => {
144          const notificationsListener = t.jasmine.createSpy('notificationsListener');
145          const subscription = Notifications.addListener(notificationsListener);
146          Notifications.scheduleLocalNotificationAsync(localNotificationFactory(), {
147            time: new Date().getTime() + 1000,
148          });
149          await waitFor(800);
150          t.expect(notificationsListener).not.toHaveBeenCalled();
151          await waitFor(500);
152          t.expect(notificationsListener).toHaveBeenCalled();
153          subscription.remove();
154        });
155
156        t.it('data is properly set', async () => {
157          const data = { scheduledAt: new Date().getTime() };
158          const notifications = await waitForCallOfListener(
159            { data },
160            {
161              timeout: 3000,
162              executor: notification =>
163                Notifications.scheduleLocalNotificationAsync(notification, {
164                  time: new Date().getTime() + 1000,
165                }),
166            }
167          );
168          let hasMatched = false;
169          notifications.forEach(notification => {
170            hasMatched = hasMatched || isMatch(notification, { data });
171          });
172          t.expect(hasMatched).toBe(true);
173        });
174      }
175    });
176
177    if (Platform.OS === 'android') {
178      t.describe('cancelScheduledNotificationAsync', () => {
179        t.it('cancels a scheduled notification', async () => {
180          let error = null;
181          try {
182            const data = { scheduledAt: new Date().getTime() };
183            const notificationId = await Notifications.scheduleLocalNotificationAsync(
184              localNotificationFactory(),
185              {
186                time: new Date().getTime() + 1000,
187              }
188            );
189            await Notifications.cancelScheduledNotificationAsync(notificationId);
190            const notifications = await waitForCallOfListener(null, {
191              timeout: 3000,
192              executor: async () => {},
193            });
194            let hasMatched = false;
195            notifications.forEach(notification => {
196              hasMatched = hasMatched || isMatch(notification, { data });
197            });
198            t.expect(hasMatched).toBe(false);
199          } catch (e) {
200            error = e;
201          }
202          t.expect(error).toBeNull();
203        });
204      });
205
206      t.describe('cancelAllScheduledNotificationsAsync', () => {
207        t.it('cancels a scheduled notification', async () => {
208          let error = null;
209          try {
210            const data = { scheduledAt: new Date().getTime() };
211            await Notifications.scheduleLocalNotificationAsync(localNotificationFactory(), {
212              time: new Date().getTime() + 1000,
213            });
214            await Notifications.cancelAllScheduledNotificationsAsync();
215            const notifications = await waitForCallOfListener(null, {
216              timeout: 3000,
217              executor: async () => {},
218            });
219            let hasMatched = false;
220            notifications.forEach(notification => {
221              hasMatched = hasMatched || isMatch(notification, { data });
222            });
223            t.expect(hasMatched).toBe(false);
224          } catch (e) {
225            error = e;
226          }
227          t.expect(error).toBeNull();
228        });
229      });
230    }
231
232    if (Platform.OS === 'ios') {
233      t.describe('getBadgeNumberAsync', () => {
234        t.it('resolves with a number', async () => {
235          const badgeNumber = await Notifications.getBadgeNumberAsync();
236          t.expect(typeof badgeNumber === 'number').toBe(true);
237        });
238      });
239
240      t.describe('setBadgeNumberAsync', () => {
241        t.afterEach(async () => await Notifications.setBadgeNumberAsync(0));
242
243        t.it('sets the badge number', async () => {
244          await Notifications.setBadgeNumberAsync(10);
245          const badgeNumber = await Notifications.getBadgeNumberAsync();
246          t.expect(badgeNumber).toEqual(10);
247        });
248      });
249    }
250  });
251}
252