Mock implementations and testing utilities for unit testing Angular service worker functionality in development and test environments.
Mock implementation of the browser's ServiceWorkerContainer for testing service worker interactions.
/**
* Mock implementation of ServiceWorkerContainer for testing
* Simulates service worker registration and message handling
*/
class MockServiceWorkerContainer {
/**
* Current active service worker instance
* Set by setupSw() method
*/
controller: MockServiceWorker | null;
/**
* Subject for push messages received by the service worker
* Use to simulate incoming push notifications
*/
messages: Subject<any>;
/**
* Subject for notification click events
* Use to simulate user interactions with notifications
*/
notificationClicks: Subject<{}>;
/**
* Mock service worker registration object
* Created by setupSw() method
*/
mockRegistration: MockServiceWorkerRegistration | null;
/**
* Add event listener for service worker events
* @param event - Event type to listen for
* @param handler - Event handler function
*/
addEventListener(event: 'controllerchange' | 'message', handler: Function): void;
/**
* Remove event listener for service worker events
* @param event - Event type to remove listener for
* @param handler - Event handler function to remove
*/
removeEventListener(event: 'controllerchange' | 'message', handler: Function): void;
/**
* Mock service worker registration
* @param url - Service worker script URL (ignored in mock)
* @returns Promise that resolves immediately
*/
register(url: string): Promise<void>;
/**
* Get service worker registration
* @returns Promise resolving to mock registration object
*/
getRegistration(): Promise<ServiceWorkerRegistration>;
/**
* Set up mock service worker with controller
* @param url - Service worker script URL (default: '/ngsw-worker.js')
*/
setupSw(url?: string): void;
/**
* Send message to service worker event listeners
* @param value - Message data to send
*/
sendMessage(value: Object): void;
}Usage Examples:
import { TestBed } from '@angular/core/testing';
import { SwPush, SwUpdate } from '@angular/service-worker';
import { MockServiceWorkerContainer } from '@angular/service-worker/testing';
describe('Service Worker Tests', () => {
let mockSwContainer: MockServiceWorkerContainer;
let swPush: SwPush;
let swUpdate: SwUpdate;
beforeEach(() => {
mockSwContainer = new MockServiceWorkerContainer();
TestBed.configureTestingModule({
providers: [
SwPush,
SwUpdate,
{
provide: 'serviceWorker',
useValue: mockSwContainer
}
]
});
swPush = TestBed.inject(SwPush);
swUpdate = TestBed.inject(SwUpdate);
// Set up mock service worker
mockSwContainer.setupSw();
});
it('should handle push messages', () => {
const receivedMessages: any[] = [];
swPush.messages.subscribe(message => {
receivedMessages.push(message);
});
// Simulate push message
mockSwContainer.sendMessage({
type: 'PUSH',
data: { title: 'Test Message', body: 'Test Body' }
});
expect(receivedMessages).toHaveLength(1);
expect(receivedMessages[0]).toEqual({
title: 'Test Message',
body: 'Test Body'
});
});
it('should handle version updates', () => {
const versionEvents: any[] = [];
swUpdate.versionUpdates.subscribe(event => {
versionEvents.push(event);
});
// Simulate version ready event
mockSwContainer.sendMessage({
type: 'VERSION_READY',
currentVersion: { hash: 'abc123' },
latestVersion: { hash: 'def456' }
});
expect(versionEvents).toHaveLength(1);
expect(versionEvents[0].type).toBe('VERSION_READY');
});
});Mock implementation of a service worker instance for testing message posting.
/**
* Mock service worker instance for testing
* Simulates postMessage functionality
*/
class MockServiceWorker {
/**
* URL of the service worker script
*/
readonly scriptURL: string;
/**
* Create mock service worker
* @param mock - Parent mock container (private)
* @param scriptURL - Service worker script URL (readonly)
*/
constructor(private mock: MockServiceWorkerContainer, readonly scriptURL: string);
/**
* Post message to service worker
* @param value - Message data to post
*/
postMessage(value: Object): void;
}Mock implementation of ServiceWorkerRegistration for testing push subscriptions.
/**
* Mock service worker registration for testing
* Provides mock push manager
*/
class MockServiceWorkerRegistration {
/**
* Mock push manager instance
* Handles push subscription operations
*/
pushManager: PushManager;
}Mock implementation of PushManager for testing push notification subscriptions.
/**
* Mock push manager for testing push subscriptions
* Simulates browser push notification API
*/
class MockPushManager {
/**
* Get current push subscription
* @returns Promise resolving to current subscription or null
*/
getSubscription(): Promise<PushSubscription | null>;
/**
* Subscribe to push notifications
* @param options - Push subscription options (ignored in mock)
* @returns Promise resolving to mock push subscription
*/
subscribe(options?: PushSubscriptionOptionsInit): Promise<PushSubscription>;
}Mock implementation of PushSubscription for testing unsubscribe operations.
/**
* Mock push subscription for testing
* Simulates browser PushSubscription API
*/
class MockPushSubscription {
/**
* Unsubscribe from push notifications
* @returns Promise resolving to true (always succeeds in mock)
*/
unsubscribe(): Promise<boolean>;
}Helper functions for testing service worker functionality.
/**
* Patch base64 decoding for Node.js testing environments
* @param proto - Object with decodeBase64 method to patch
* @returns Function to unpatch the method
*/
function patchDecodeBase64(proto: {decodeBase64: typeof atob}): () => void;Usage Examples:
import { SwPush } from '@angular/service-worker';
import { patchDecodeBase64 } from '@angular/service-worker/testing';
describe('SwPush', () => {
let swPush: SwPush;
let unpatch: () => void;
beforeEach(() => {
// Patch for Node.js environment
unpatch = patchDecodeBase64(SwPush.prototype);
// Set up component/service
swPush = TestBed.inject(SwPush);
});
afterEach(() => {
// Restore original method
unpatch();
});
it('should decode VAPID keys', async () => {
const subscription = await swPush.requestSubscription({
serverPublicKey: 'BEl62iUYgUivxIkv69yViEuiBIa-Ib9-SkvMeAtA3LFgDzkrxZJjSgSnfckjBJuBkr3qBUYIHBQFLXYp5Nksh8U'
});
expect(subscription).toBeDefined();
});
});import { TestBed } from '@angular/core/testing';
import { SwPush, SwUpdate } from '@angular/service-worker';
import {
MockServiceWorkerContainer,
patchDecodeBase64
} from '@angular/service-worker/testing';
describe('Service Worker Integration', () => {
let mockSwContainer: MockServiceWorkerContainer;
let swPush: SwPush;
let swUpdate: SwUpdate;
let unpatchDecodeBase64: () => void;
beforeEach(() => {
mockSwContainer = new MockServiceWorkerContainer();
unpatchDecodeBase64 = patchDecodeBase64(SwPush.prototype);
TestBed.configureTestingModule({
providers: [
SwPush,
SwUpdate,
{
provide: 'serviceWorker',
useValue: mockSwContainer
}
]
});
swPush = TestBed.inject(SwPush);
swUpdate = TestBed.inject(SwUpdate);
mockSwContainer.setupSw();
});
afterEach(() => {
unpatchDecodeBase64();
});
// Test cases here...
});it('should subscribe to push notifications', async () => {
const subscription = await swPush.requestSubscription({
serverPublicKey: 'test-key'
});
expect(subscription).toBeDefined();
expect(swPush.isEnabled).toBe(true);
});
it('should receive push messages', () => {
const messages: any[] = [];
swPush.messages.subscribe(msg => messages.push(msg));
mockSwContainer.sendMessage({
type: 'PUSH',
data: { title: 'Test', body: 'Message' }
});
expect(messages).toHaveLength(1);
expect(messages[0]).toEqual({ title: 'Test', body: 'Message' });
});
it('should handle notification clicks', () => {
const clicks: any[] = [];
swPush.notificationClicks.subscribe(click => clicks.push(click));
mockSwContainer.sendMessage({
type: 'NOTIFICATION_CLICK',
data: {
action: 'open',
notification: { title: 'Test', body: 'Click' }
}
});
expect(clicks).toHaveLength(1);
expect(clicks[0].action).toBe('open');
});it('should handle version updates', () => {
const events: any[] = [];
swUpdate.versionUpdates.subscribe(event => events.push(event));
// Simulate version detected
mockSwContainer.sendMessage({
type: 'VERSION_DETECTED',
version: { hash: 'new-version' }
});
// Simulate version ready
mockSwContainer.sendMessage({
type: 'VERSION_READY',
currentVersion: { hash: 'old-version' },
latestVersion: { hash: 'new-version' }
});
expect(events).toHaveLength(2);
expect(events[0].type).toBe('VERSION_DETECTED');
expect(events[1].type).toBe('VERSION_READY');
});
it('should handle unrecoverable states', () => {
const states: any[] = [];
swUpdate.unrecoverable.subscribe(state => states.push(state));
mockSwContainer.sendMessage({
type: 'UNRECOVERABLE_STATE',
reason: 'Cache corruption'
});
expect(states).toHaveLength(1);
expect(states[0].reason).toBe('Cache corruption');
});
it('should check for updates', async () => {
const promise = swUpdate.checkForUpdate();
// Simulate response
mockSwContainer.sendMessage({
type: 'OPERATION_COMPLETED',
nonce: jasmine.any(Number),
result: true
});
const result = await promise;
expect(result).toBe(true);
});it('should detect service worker support', () => {
expect(swPush.isEnabled).toBe(true);
expect(swUpdate.isEnabled).toBe(true);
});
it('should handle disabled service worker', () => {
// Test with no service worker
TestBed.overrideProvider('serviceWorker', { useValue: undefined });
const swPushDisabled = TestBed.inject(SwPush);
const swUpdateDisabled = TestBed.inject(SwUpdate);
expect(swPushDisabled.isEnabled).toBe(false);
expect(swUpdateDisabled.isEnabled).toBe(false);
});When running tests in Node.js (e.g., with Jest or Karma), you may need to polyfill browser APIs:
// test-setup.ts
global.atob = global.atob || ((str: string) => Buffer.from(str, 'base64').toString('binary'));
global.btoa = global.btoa || ((str: string) => Buffer.from(str, 'binary').toString('base64'));
// Mock navigator.serviceWorker if needed
if (typeof navigator === 'undefined') {
(global as any).navigator = {
serviceWorker: new MockServiceWorkerContainer()
};
}patchDecodeBase64 for Node.js environmentsafterEachMockPushManager for push notification tests