Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

test: first pass of ServiceWorker tests #1212

Draft
wants to merge 5 commits into
base: main
Choose a base branch
from
Draft
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
323 changes: 323 additions & 0 deletions __test__/unit/sw/serviceWorker.test.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,13 @@
import { ServiceWorker } from '../../../src/sw/serviceWorker/ServiceWorker';
import Database from '../../../src/shared/services/Database';

// suppress all internal logging
jest.mock('../../../src/shared/libraries/Log');

// mock dependencies
jest.mock('../../../src/shared/services/Database');
jest.mock('../../../src/shared/utils/AwaitableTimeout');

function chromeUserAgentDataBrands(): Array<{
brand: string;
version: string;
Expand All @@ -15,6 +20,27 @@ function chromeUserAgentDataBrands(): Array<{
}

describe('ServiceWorker', () => {
// Define the ServiceWorker global scope type
declare const self: ServiceWorkerGlobalScope;

// Create a mock self object
const mockSelf = {
registration: {
showNotification: jest.fn().mockResolvedValue(undefined),
},
clients: {
openWindow: jest.fn(),
}
} as unknown as ServiceWorkerGlobalScope;

beforeAll(() => {
// Set up the global ServiceWorker scope
Object.defineProperty(global, 'self', {
value: mockSelf,
writable: true
});
});

describe('requiresMacOS15ChromiumAfterDisplayWorkaround', () => {
test('navigator.userAgentData undefined', async () => {
delete (navigator as any).userAgentData;
Expand Down Expand Up @@ -50,4 +76,301 @@ describe('ServiceWorker', () => {
).toBe(true);
});
});

describe('API', () => {

afterEach(() => {
jest.clearAllMocks();
});

describe('onPushReceived', () => {
beforeEach(() => {
// Mock parseOrFetchNotifications to return a controlled payload
jest.spyOn(ServiceWorker, 'parseOrFetchNotifications').mockResolvedValue([
{
title: 'Test Title',
body: 'Test Body',
icon: 'test-icon.png',
custom: {
i: 'test-uuid'
}
}
]);

// Mock other required methods
jest.spyOn(ServiceWorker, 'getAppId').mockResolvedValue('test-app-id');
jest.spyOn(ServiceWorker, 'getPushSubscriptionId').mockResolvedValue('test-sub-id');

// Mock Database
(Database.putNotificationReceivedForOutcomes as jest.Mock).mockResolvedValue(undefined);
});

it('should not show notification for undefined payload', async () => {
const mockPushEvent = {
data: {
json: () => undefined
},
waitUntil: jest.fn()
};

ServiceWorker.onPushReceived(mockPushEvent);
expect(mockSelf.registration.showNotification).not.toHaveBeenCalled();
});

it('should not show notification for empty payload', async () => {
const mockPushEvent = {
data: {
json: () => ({})
},
waitUntil: jest.fn()
};

ServiceWorker.onPushReceived(mockPushEvent);
expect(mockSelf.registration.showNotification).not.toHaveBeenCalled();
});

it('should not show notification for non-OneSignal payload', async () => {
const mockPushEvent = {
data: {
json: () => ({ title: 'Test Title' })
},
waitUntil: jest.fn()
};

ServiceWorker.onPushReceived(mockPushEvent);
expect(mockSelf.registration.showNotification).not.toHaveBeenCalled();
});

it('should show notification with valid OneSignal payload', async () => {
// Track the promise passed to waitUntil
let waitUntilPromise: Promise<any>;

const mockPushEvent = {
data: {
json: () => ({
custom: {
i: 'test-uuid'
},
title: 'Test Title'
})
},
waitUntil: jest.fn((promise) => {
waitUntilPromise = promise;
return promise;
})
};

// Call onPushReceived
ServiceWorker.onPushReceived(mockPushEvent);

// Wait for the waitUntil promise to complete
await waitUntilPromise;

expect(mockSelf.registration.showNotification).toHaveBeenCalledWith(
'Test Title',
expect.objectContaining({
data: expect.objectContaining({
title: "Test Title",
notificationId: "test-uuid"
})
})
);
});
});

describe('displayNotification', () => {
beforeEach(() => {
// Reset mocks before each test
jest.clearAllMocks();

// Mock Database methods
(Database.get as jest.Mock).mockResolvedValue({ value: true });
(Database.put as jest.Mock).mockResolvedValue({ value: false });
(Database.getAppConfig as jest.Mock).mockResolvedValue({ appId: 'test-app-id' });
});

it('should set requireInteraction to true when persistNotification is true', async () => {
await ServiceWorker.displayNotification({
body: '',
title: 'Test Title',
confirmDelivery: false,
notificationId: 'test-id'
});

expect(mockSelf.registration.showNotification).toHaveBeenCalledWith(
'Test Title',
expect.objectContaining({
requireInteraction: true
})
);
});

it('should set requireInteraction to true when persistNotification is undefined', async () => {
(Database.get as jest.Mock).mockResolvedValue(undefined);

await ServiceWorker.displayNotification({
body: '',
title: 'Test Title',
confirmDelivery: false,
notificationId: 'test-id'
});

expect(mockSelf.registration.showNotification).toHaveBeenCalledWith(
'Test Title',
expect.objectContaining({
requireInteraction: true
})
);
});

it('should set requireInteraction to true when persistNotification is "force"', async () => {
(Database.get as jest.Mock).mockResolvedValue({ value: 'force' });

await ServiceWorker.displayNotification({
body: '',
title: 'Test Title',
confirmDelivery: false,
notificationId: 'test-id'
});

expect(mockSelf.registration.showNotification).toHaveBeenCalledWith(
'Test Title',
expect.objectContaining({
requireInteraction: true
})
);
});

it('should set requireInteraction to false when persistNotification is false', async () => {
await Database.put('Options', { key: 'persistNotification', value: false });
await ServiceWorker.displayNotification({
body: '',
confirmDelivery: false,
notificationId: ''
});

expect(mockSelf.registration.showNotification).toHaveBeenCalledWith(
'Test Title',
expect.objectContaining({
requireInteraction: false
})
);
});
});

describe('onNotificationClicked', () => {
beforeEach(() => {
// Mock fetch for API calls
global.fetch = jest.fn().mockResolvedValue({
ok: true,
json: () => Promise.resolve({ success: true })
});
});

it('should send notification PUT request when clicked', async () => {
const notificationId = 'test-notification-id';
const mockNotificationEvent = {
notification: {
data: { id: notificationId },
close: jest.fn()
}
};

await ServiceWorker.onNotificationClicked(mockNotificationEvent);

expect(global.fetch).toHaveBeenCalledWith(
expect.stringContaining(`/api/v1/notifications/${notificationId}`),
expect.objectContaining({
method: 'PUT',
body: expect.any(String)
})
);
});

it('should execute webhooks when notification is clicked', async () => {
const notificationId = 'test-notification-id';
const mockNotificationEvent = {
notification: {
data: { id: notificationId },
close: jest.fn()
}
};

const executeWebhooksSpy = jest.spyOn(ServiceWorker, 'executeWebhooks');
await ServiceWorker.onNotificationClicked(mockNotificationEvent);

expect(executeWebhooksSpy).toHaveBeenCalledWith(
'notification.clicked',
expect.objectContaining({ id: notificationId })
);
});

it('should open window when notification is clicked', async () => {
const notificationId = 'test-notification-id';
const mockNotificationEvent = {
notification: {
data: { id: notificationId },
close: jest.fn()
}
};

await ServiceWorker.onNotificationClicked(mockNotificationEvent);
expect(mockSelf.clients.openWindow).toHaveBeenCalled();
});
});

describe('sendConfirmedDelivery', () => {
beforeEach(() => {
// Mock fetch for API calls
global.fetch = jest.fn().mockResolvedValue({
ok: true,
json: () => Promise.resolve({ success: true })
});
});

it('should send confirmed delivery when feature flag is true', async () => {
const notificationId = 'test-notification-id';

await ServiceWorker.sendConfirmedDelivery({
notificationId,
confirmDelivery: true,
body: ''
});

expect(global.fetch).toHaveBeenCalledWith(
expect.stringContaining(`/api/v1/notifications/${notificationId}/report_received`),
expect.any(Object)
);
});

it('should not send confirmed delivery when feature flag is false', async () => {
const notificationId = 'test-notification-id';

await ServiceWorker.sendConfirmedDelivery({
notificationId,
confirmDelivery: false,
body: ''
});

expect(global.fetch).not.toHaveBeenCalled();
});

it('should include device_type in confirmed delivery request', async () => {
const notificationId = 'test-notification-id';

await ServiceWorker.sendConfirmedDelivery({
notificationId,
confirmDelivery: true,
body: ''
});

expect(global.fetch).toHaveBeenCalledWith(
expect.any(String),
expect.objectContaining({
body: expect.stringContaining('"device_type":5')
})
);
});
});
});
});
Loading