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