Skip to content

Commit d4e8c7c

Browse files
authored
test: add event manager tests (#1856)
1 parent f8314f1 commit d4e8c7c

File tree

9 files changed

+972
-140
lines changed

9 files changed

+972
-140
lines changed

__mocks__/react-native.ts

Lines changed: 26 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,28 @@
11
import { vi } from 'vitest';
22

3+
export const createEmitterSubscriptionMock = (
4+
eventName: string,
5+
callback: (payload: unknown) => void,
6+
) => ({
7+
remove: vi.fn(),
8+
emitter: {
9+
addListener: vi.fn(),
10+
removeAllListeners: vi.fn(),
11+
listenerCount: vi.fn(() => 1),
12+
emit: vi.fn(),
13+
},
14+
listener: () => callback,
15+
context: undefined,
16+
eventType: eventName,
17+
key: 0,
18+
subscriber: {
19+
addSubscription: vi.fn(),
20+
removeSubscription: vi.fn(),
21+
removeAllSubscriptions: vi.fn(),
22+
getSubscriptionsForType: vi.fn(),
23+
},
24+
});
25+
326
const mockRNOneSignal = {
427
initialize: vi.fn(),
528
login: vi.fn(),
@@ -61,6 +84,7 @@ const mockRNOneSignal = {
6184
addOutcome: vi.fn(),
6285
addUniqueOutcome: vi.fn(),
6386
addOutcomeWithValue: vi.fn(),
87+
displayNotification: vi.fn(),
6488
};
6589

6690
const mockPlatform = {
@@ -78,10 +102,8 @@ export { mockPlatform, mockRNOneSignal };
78102
export class NativeEventEmitter {
79103
constructor(_nativeModule: typeof mockRNOneSignal) {}
80104

81-
addListener(_eventName: string, _callback: (payload: unknown) => void) {
82-
return {
83-
remove: vi.fn(),
84-
};
105+
addListener(eventName: string, callback: (payload: unknown) => void) {
106+
return createEmitterSubscriptionMock(eventName, callback);
85107
}
86108

87109
removeListener(_eventName: string, _callback: (payload: unknown) => void) {

src/OSNotification.test.ts

Lines changed: 242 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,242 @@
1+
import { NativeModules, Platform } from 'react-native';
2+
import OSNotification, { type BaseNotificationData } from './OSNotification';
3+
4+
const mockRNOneSignal = NativeModules.OneSignal;
5+
const mockPlatform = Platform;
6+
7+
describe('OSNotification', () => {
8+
const baseNotificationData: BaseNotificationData = {
9+
body: 'Test notification body',
10+
sound: 'default',
11+
title: 'Test Title',
12+
launchURL: 'https://example.com',
13+
rawPayload: { key: 'value' },
14+
actionButtons: [{ id: 'btn1', text: 'Button 1' }],
15+
additionalData: { custom: 'data' },
16+
notificationId: 'test-notification-id',
17+
};
18+
19+
beforeEach(() => {
20+
mockPlatform.OS = 'ios';
21+
mockRNOneSignal.displayNotification.mockClear();
22+
});
23+
24+
describe('constructor', () => {
25+
test('should initialize common properties', () => {
26+
const notification = new OSNotification(baseNotificationData);
27+
28+
expect(notification.body).toBe('Test notification body');
29+
expect(notification.sound).toBe('default');
30+
expect(notification.title).toBe('Test Title');
31+
expect(notification.launchURL).toBe('https://example.com');
32+
expect(notification.rawPayload).toEqual({ key: 'value' });
33+
expect(notification.actionButtons).toEqual([
34+
{ id: 'btn1', text: 'Button 1' },
35+
]);
36+
expect(notification.additionalData).toEqual({ custom: 'data' });
37+
expect(notification.notificationId).toBe('test-notification-id');
38+
});
39+
40+
test('should initialize with optional common properties as undefined', () => {
41+
const notificationData = {
42+
body: 'Test body',
43+
rawPayload: '',
44+
notificationId: 'id-123',
45+
};
46+
const notification = new OSNotification(notificationData);
47+
48+
expect(notification.body).toBe('Test body');
49+
expect(notification.sound).toBeUndefined();
50+
expect(notification.title).toBeUndefined();
51+
expect(notification.launchURL).toBeUndefined();
52+
expect(notification.actionButtons).toBeUndefined();
53+
expect(notification.additionalData).toBeUndefined();
54+
});
55+
56+
describe('Android-specific properties', () => {
57+
beforeEach(() => {
58+
mockPlatform.OS = 'android';
59+
});
60+
61+
test('should initialize Android-specific properties on Android platform', () => {
62+
const androidData = {
63+
...baseNotificationData,
64+
groupKey: 'group-1',
65+
groupMessage: 'group message',
66+
ledColor: 'FFFF0000',
67+
priority: 2,
68+
smallIcon: 'icon_small',
69+
largeIcon: 'icon_large',
70+
bigPicture: 'image_url',
71+
collapseId: 'collapse-1',
72+
fromProjectNumber: '123456789',
73+
smallIconAccentColor: 'FFFF0000',
74+
lockScreenVisibility: '1',
75+
androidNotificationId: 456,
76+
};
77+
const notification = new OSNotification(androidData);
78+
79+
expect(notification.groupKey).toBe('group-1');
80+
expect(notification.groupMessage).toBe('group message');
81+
expect(notification.ledColor).toBe('FFFF0000');
82+
expect(notification.priority).toBe(2);
83+
expect(notification.smallIcon).toBe('icon_small');
84+
expect(notification.largeIcon).toBe('icon_large');
85+
expect(notification.bigPicture).toBe('image_url');
86+
expect(notification.collapseId).toBe('collapse-1');
87+
expect(notification.fromProjectNumber).toBe('123456789');
88+
expect(notification.smallIconAccentColor).toBe('FFFF0000');
89+
expect(notification.lockScreenVisibility).toBe('1');
90+
expect(notification.androidNotificationId).toBe(456);
91+
});
92+
93+
test('should not set iOS-specific properties on Android', () => {
94+
const notification = new OSNotification(baseNotificationData);
95+
96+
expect(notification.badge).toBeUndefined();
97+
expect(notification.badgeIncrement).toBeUndefined();
98+
expect(notification.category).toBeUndefined();
99+
expect(notification.threadId).toBeUndefined();
100+
expect(notification.subtitle).toBeUndefined();
101+
expect(notification.templateId).toBeUndefined();
102+
expect(notification.templateName).toBeUndefined();
103+
expect(notification.attachments).toBeUndefined();
104+
expect(notification.mutableContent).toBeUndefined();
105+
expect(notification.contentAvailable).toBeUndefined();
106+
expect(notification.relevanceScore).toBeUndefined();
107+
expect(notification.interruptionLevel).toBeUndefined();
108+
});
109+
});
110+
111+
describe('iOS-specific properties', () => {
112+
beforeEach(() => {
113+
mockPlatform.OS = 'ios';
114+
});
115+
116+
test('should initialize iOS-specific properties on iOS platform', () => {
117+
const iosData = {
118+
...baseNotificationData,
119+
badge: '1',
120+
badgeIncrement: '5',
121+
category: 'NOTIFICATION_CATEGORY',
122+
threadId: 'thread-123',
123+
subtitle: 'Test Subtitle',
124+
templateId: 'template-1',
125+
templateName: 'template-name',
126+
attachments: { key: 'attachment-value' },
127+
mutableContent: true,
128+
contentAvailable: '1',
129+
relevanceScore: 0.9,
130+
interruptionLevel: 'timeSensitive',
131+
};
132+
const notification = new OSNotification(iosData);
133+
134+
expect(notification.badge).toBe('1');
135+
expect(notification.badgeIncrement).toBe('5');
136+
expect(notification.category).toBe('NOTIFICATION_CATEGORY');
137+
expect(notification.threadId).toBe('thread-123');
138+
expect(notification.subtitle).toBe('Test Subtitle');
139+
expect(notification.templateId).toBe('template-1');
140+
expect(notification.templateName).toBe('template-name');
141+
expect(notification.attachments).toEqual({ key: 'attachment-value' });
142+
expect(notification.mutableContent).toBe(true);
143+
expect(notification.contentAvailable).toBe('1');
144+
expect(notification.relevanceScore).toBe(0.9);
145+
expect(notification.interruptionLevel).toBe('timeSensitive');
146+
});
147+
148+
test('should not set Android-specific properties on iOS', () => {
149+
const notification = new OSNotification(baseNotificationData);
150+
151+
expect(notification.groupKey).toBeUndefined();
152+
expect(notification.groupMessage).toBeUndefined();
153+
expect(notification.ledColor).toBeUndefined();
154+
expect(notification.priority).toBeUndefined();
155+
expect(notification.smallIcon).toBeUndefined();
156+
expect(notification.largeIcon).toBeUndefined();
157+
expect(notification.bigPicture).toBeUndefined();
158+
expect(notification.collapseId).toBeUndefined();
159+
expect(notification.fromProjectNumber).toBeUndefined();
160+
expect(notification.smallIconAccentColor).toBeUndefined();
161+
expect(notification.lockScreenVisibility).toBeUndefined();
162+
expect(notification.androidNotificationId).toBeUndefined();
163+
});
164+
});
165+
});
166+
167+
describe('display', () => {
168+
test('should call native displayNotification with notificationId', () => {
169+
const notification = new OSNotification(baseNotificationData);
170+
notification.display();
171+
172+
expect(mockRNOneSignal.displayNotification).toHaveBeenCalledWith(
173+
'test-notification-id',
174+
);
175+
});
176+
177+
test('should display notification with different notificationId', () => {
178+
const notificationID = 'custom-id-789';
179+
const notification = new OSNotification({
180+
...baseNotificationData,
181+
notificationId: notificationID,
182+
});
183+
notification.display();
184+
185+
expect(mockRNOneSignal.displayNotification).toHaveBeenCalledWith(
186+
notificationID,
187+
);
188+
});
189+
190+
test('should return undefined', () => {
191+
const notification = new OSNotification(baseNotificationData);
192+
const result = notification.display();
193+
194+
expect(result).toBeUndefined();
195+
});
196+
});
197+
198+
describe('rawPayload types', () => {
199+
test('should accept object as rawPayload', () => {
200+
const notificationData = {
201+
...baseNotificationData,
202+
rawPayload: { key: 'value', nested: { key: 'value' } },
203+
};
204+
const notification = new OSNotification(notificationData);
205+
206+
expect(notification.rawPayload).toEqual({
207+
key: 'value',
208+
nested: { key: 'value' },
209+
});
210+
});
211+
212+
test('should accept string as rawPayload', () => {
213+
const notificationData = {
214+
...baseNotificationData,
215+
rawPayload: '{"key":"value"}',
216+
};
217+
const notification = new OSNotification(notificationData);
218+
219+
expect(notification.rawPayload).toBe('{"key":"value"}');
220+
});
221+
222+
test('should accept empty object as rawPayload', () => {
223+
const notificationData = {
224+
...baseNotificationData,
225+
rawPayload: {},
226+
};
227+
const notification = new OSNotification(notificationData);
228+
229+
expect(notification.rawPayload).toEqual({});
230+
});
231+
232+
test('should accept empty string as rawPayload', () => {
233+
const notificationData = {
234+
...baseNotificationData,
235+
rawPayload: '',
236+
};
237+
const notification = new OSNotification(notificationData);
238+
239+
expect(notification.rawPayload).toBe('');
240+
});
241+
});
242+
});

src/OSNotification.ts

Lines changed: 59 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,49 @@
11
import { NativeModules, Platform } from 'react-native';
22
const RNOneSignal = NativeModules.OneSignal;
33

4+
export interface BaseNotificationData {
5+
body: string;
6+
sound?: string;
7+
title?: string;
8+
launchURL?: string;
9+
rawPayload: object | string; // platform bridges return different types
10+
actionButtons?: object[];
11+
additionalData?: object;
12+
notificationId: string;
13+
}
14+
15+
interface AndroidNotificationData extends BaseNotificationData {
16+
groupKey?: string;
17+
groupMessage?: string;
18+
ledColor?: string;
19+
priority?: number;
20+
smallIcon?: string;
21+
largeIcon?: string;
22+
bigPicture?: string;
23+
collapseId?: string;
24+
fromProjectNumber?: string;
25+
smallIconAccentColor?: string;
26+
lockScreenVisibility?: string;
27+
androidNotificationId?: number;
28+
}
29+
30+
interface iOSNotificationData extends BaseNotificationData {
31+
badge?: string;
32+
badgeIncrement?: string;
33+
category?: string;
34+
threadId?: string;
35+
subtitle?: string;
36+
templateId?: string;
37+
templateName?: string;
38+
attachments?: object;
39+
mutableContent?: boolean;
40+
contentAvailable?: string;
41+
relevanceScore?: number;
42+
interruptionLevel?: string;
43+
}
44+
45+
export type OSNotificationData = AndroidNotificationData | iOSNotificationData;
46+
447
export default class OSNotification {
548
body: string;
649
sound?: string;
@@ -37,7 +80,7 @@ export default class OSNotification {
3780
relevanceScore?: number;
3881
interruptionLevel?: string;
3982

40-
constructor(receivedEvent: OSNotification) {
83+
constructor(receivedEvent: OSNotificationData) {
4184
this.body = receivedEvent.body;
4285
this.sound = receivedEvent.sound;
4386
this.title = receivedEvent.title;
@@ -47,7 +90,8 @@ export default class OSNotification {
4790
this.additionalData = receivedEvent.additionalData;
4891
this.notificationId = receivedEvent.notificationId;
4992

50-
if (Platform.OS === 'android') {
93+
/* v8 ignore else -- @preserve */
94+
if (isAndroidNotificationData(receivedEvent)) {
5195
this.groupKey = receivedEvent.groupKey;
5296
this.ledColor = receivedEvent.ledColor;
5397
this.priority = receivedEvent.priority;
@@ -60,9 +104,7 @@ export default class OSNotification {
60104
this.smallIconAccentColor = receivedEvent.smallIconAccentColor;
61105
this.lockScreenVisibility = receivedEvent.lockScreenVisibility;
62106
this.androidNotificationId = receivedEvent.androidNotificationId;
63-
}
64-
65-
if (Platform.OS === 'ios') {
107+
} else if (isiOSNotificationData(receivedEvent)) {
66108
this.badge = receivedEvent.badge;
67109
this.category = receivedEvent.category;
68110
this.threadId = receivedEvent.threadId;
@@ -83,3 +125,15 @@ export default class OSNotification {
83125
return;
84126
}
85127
}
128+
129+
const isAndroidNotificationData = (
130+
_data: AndroidNotificationData | iOSNotificationData,
131+
): _data is AndroidNotificationData => {
132+
return Platform.OS === 'android';
133+
};
134+
135+
const isiOSNotificationData = (
136+
_data: AndroidNotificationData | iOSNotificationData,
137+
): _data is iOSNotificationData => {
138+
return Platform.OS === 'ios';
139+
};

0 commit comments

Comments
 (0)