diff --git a/__mocks__/react-native.ts b/__mocks__/react-native.ts index 3c6670b6..517f1d10 100644 --- a/__mocks__/react-native.ts +++ b/__mocks__/react-native.ts @@ -85,6 +85,7 @@ const mockRNOneSignal = { addUniqueOutcome: vi.fn(), addOutcomeWithValue: vi.fn(), displayNotification: vi.fn(), + preventDefault: vi.fn(), }; const mockPlatform = { diff --git a/src/events/NotificationWillDisplayEvent.test.ts b/src/events/NotificationWillDisplayEvent.test.ts new file mode 100644 index 00000000..217ffb83 --- /dev/null +++ b/src/events/NotificationWillDisplayEvent.test.ts @@ -0,0 +1,107 @@ +import { NativeModules } from 'react-native'; +import OSNotification, { type BaseNotificationData } from '../OSNotification'; +import NotificationWillDisplayEvent from './NotificationWillDisplayEvent'; + +const mockRNOneSignal = NativeModules.OneSignal; + +describe('NotificationWillDisplayEvent', () => { + const notificationId = 'test-notification-id'; + const baseNotificationData: BaseNotificationData = { + body: 'Test notification body', + sound: 'default', + title: 'Test Title', + launchURL: 'https://example.com', + rawPayload: { key: 'value' }, + actionButtons: [{ id: 'btn1', text: 'Button 1' }], + additionalData: { custom: 'data' }, + notificationId, + }; + + describe('constructor', () => { + test('should initialize with OSNotification instance', () => { + const notification = new OSNotification(baseNotificationData); + const event = new NotificationWillDisplayEvent(notification); + + expect(event.notification).toBeInstanceOf(OSNotification); + expect(event.notification.notificationId).toBe(notificationId); + expect(event.notification.body).toBe('Test notification body'); + expect(event.notification.title).toBe('Test Title'); + }); + + test('should create a new OSNotification instance from the provided notification', () => { + const notification = new OSNotification(baseNotificationData); + const event = new NotificationWillDisplayEvent(notification); + + expect(event.notification).not.toBe(notification); + expect(event.notification.notificationId).toBe(notificationId); + expect(event.notification.body).toBe(notification.body); + }); + + test('should initialize with notification containing all optional fields', () => { + const fullData = { + ...baseNotificationData, + sound: 'custom-sound', + launchURL: 'https://example.com/launch', + actionButtons: [ + { id: 'btn1', text: 'Button 1' }, + { id: 'btn2', text: 'Button 2' }, + ], + additionalData: { key1: 'value1', key2: 'value2' }, + }; + const notification = new OSNotification(fullData); + const event = new NotificationWillDisplayEvent(notification); + + expect(event.notification.sound).toBe('custom-sound'); + expect(event.notification.launchURL).toBe('https://example.com/launch'); + expect(event.notification.actionButtons).toHaveLength(2); + expect(event.notification.additionalData).toEqual({ + key1: 'value1', + key2: 'value2', + }); + }); + }); + + describe('preventDefault', () => { + test('should call native preventDefault with notificationId', () => { + const notification = new OSNotification(baseNotificationData); + const event = new NotificationWillDisplayEvent(notification); + const result = event.preventDefault(); + + expect(mockRNOneSignal.preventDefault).toHaveBeenCalledWith( + notificationId, + ); + expect(result).toBeUndefined(); + }); + + test('should allow multiple calls to preventDefault', () => { + const notification = new OSNotification(baseNotificationData); + const event = new NotificationWillDisplayEvent(notification); + + event.preventDefault(); + event.preventDefault(); + event.preventDefault(); + + expect(mockRNOneSignal.preventDefault).toHaveBeenCalledTimes(3); + expect(mockRNOneSignal.preventDefault).toHaveBeenCalledWith( + 'test-notification-id', + ); + }); + }); + + describe('getNotification', () => { + test('should return the notification instance', () => { + const notification = new OSNotification(baseNotificationData); + const event = new NotificationWillDisplayEvent(notification); + const returnedNotification = event.getNotification(); + + expect(returnedNotification).toBe(event.notification); + expect(returnedNotification).toBeInstanceOf(OSNotification); + + expect(returnedNotification.notificationId).toBe('test-notification-id'); + expect(returnedNotification.body).toBe('Test notification body'); + expect(returnedNotification.title).toBe('Test Title'); + expect(returnedNotification.sound).toBe('default'); + expect(returnedNotification.rawPayload).toEqual({ key: 'value' }); + }); + }); +}); diff --git a/src/events/NotificationWillDisplayEvent.ts b/src/events/NotificationWillDisplayEvent.ts index e647aba9..c042b5df 100644 --- a/src/events/NotificationWillDisplayEvent.ts +++ b/src/events/NotificationWillDisplayEvent.ts @@ -11,7 +11,6 @@ export default class NotificationWillDisplayEvent { preventDefault(): void { RNOneSignal.preventDefault(this.notification.notificationId); - return; } getNotification(): OSNotification { diff --git a/src/helpers.test.ts b/src/helpers.test.ts new file mode 100644 index 00000000..b3e5fb6c --- /dev/null +++ b/src/helpers.test.ts @@ -0,0 +1,61 @@ +import type { NativeModule } from 'react-native'; +import type { MockInstance } from 'vitest'; +import { isNativeModuleLoaded, isValidCallback } from './helpers'; + +describe('helpers', () => { + let errorSpy: MockInstance; + + beforeEach(() => { + errorSpy = vi.spyOn(console, 'error').mockImplementation(() => {}); + }); + + describe('isValidCallback', () => { + test('should not throw when handler is a function', () => { + const handler = () => {}; + expect(() => isValidCallback(handler)).not.toThrow(); + }); + + test.each([ + { description: 'null', value: null }, + { description: 'undefined', value: undefined }, + { description: 'a string', value: 'not a function' }, + { description: 'a number', value: 123 }, + { description: 'an object', value: {} }, + { description: 'an array', value: [] }, + { description: 'a boolean', value: true }, + ])( + 'should throw invariant error when handler is $description', + ({ value }) => { + expect(() => isValidCallback(value as unknown as Function)).toThrow( + 'Must provide a valid callback', + ); + }, + ); + }); + + describe('isNativeModuleLoaded', () => { + test.each([ + { description: 'null', value: null as unknown as NativeModule }, + { + description: 'undefined', + value: undefined as unknown as NativeModule, + }, + ])( + 'should return false and log error when module is $description', + ({ value }) => { + const result = isNativeModuleLoaded(value); + + expect(result).toBe(false); + expect(errorSpy).toHaveBeenCalledTimes(1); + expect(errorSpy).toHaveBeenCalledWith( + 'Could not load RNOneSignal native module. Make sure native dependencies are properly linked.', + ); + }, + ); + + test('should return true when module is loaded', () => { + const result = isNativeModuleLoaded({} as NativeModule); + expect(result).toBe(true); + }); + }); +}); diff --git a/vitest.config.ts b/vitest.config.ts index 26641578..097c3aa8 100644 --- a/vitest.config.ts +++ b/vitest.config.ts @@ -13,6 +13,12 @@ export default defineConfig({ reporter: ['text-summary', 'lcov'], reportOnFailure: true, reportsDirectory: 'coverage', + thresholds: { + statements: 95, + branches: 95, + functions: 95, + lines: 95, + }, }, }, resolve: {