357 lines
10 KiB
JavaScript
357 lines
10 KiB
JavaScript
|
|
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
|
||
|
|
|
||
|
|
describe('Service Worker Registration Tests', () => {
|
||
|
|
let originalNavigator;
|
||
|
|
let mockServiceWorkerContainer;
|
||
|
|
let mockRegistration;
|
||
|
|
|
||
|
|
beforeEach(() => {
|
||
|
|
// Save original navigator
|
||
|
|
originalNavigator = global.navigator;
|
||
|
|
|
||
|
|
// Create mock service worker registration
|
||
|
|
mockRegistration = {
|
||
|
|
installing: null,
|
||
|
|
waiting: null,
|
||
|
|
active: {
|
||
|
|
state: 'activated',
|
||
|
|
postMessage: vi.fn(),
|
||
|
|
},
|
||
|
|
scope: '/',
|
||
|
|
update: vi.fn(),
|
||
|
|
unregister: vi.fn(),
|
||
|
|
addEventListener: vi.fn(),
|
||
|
|
removeEventListener: vi.fn(),
|
||
|
|
};
|
||
|
|
|
||
|
|
// Create mock service worker container
|
||
|
|
mockServiceWorkerContainer = {
|
||
|
|
register: vi.fn().mockResolvedValue(mockRegistration),
|
||
|
|
getRegistration: vi.fn().mockResolvedValue(mockRegistration),
|
||
|
|
getRegistrations: vi.fn().mockResolvedValue([mockRegistration]),
|
||
|
|
ready: Promise.resolve(mockRegistration),
|
||
|
|
controller: null,
|
||
|
|
addEventListener: vi.fn(),
|
||
|
|
removeEventListener: vi.fn(),
|
||
|
|
};
|
||
|
|
|
||
|
|
// Mock navigator.serviceWorker
|
||
|
|
Object.defineProperty(global.navigator, 'serviceWorker', {
|
||
|
|
value: mockServiceWorkerContainer,
|
||
|
|
configurable: true,
|
||
|
|
writable: true,
|
||
|
|
});
|
||
|
|
});
|
||
|
|
|
||
|
|
afterEach(() => {
|
||
|
|
global.navigator = originalNavigator;
|
||
|
|
});
|
||
|
|
|
||
|
|
describe('Service Worker Support Detection', () => {
|
||
|
|
it('should detect service worker support in browser', () => {
|
||
|
|
expect('serviceWorker' in navigator).toBe(true);
|
||
|
|
});
|
||
|
|
|
||
|
|
it('should not have service worker in browsers that do not support it', () => {
|
||
|
|
// Remove serviceWorker
|
||
|
|
const navigatorWithoutSW = { ...global.navigator };
|
||
|
|
delete navigatorWithoutSW.serviceWorker;
|
||
|
|
global.navigator = navigatorWithoutSW;
|
||
|
|
|
||
|
|
expect('serviceWorker' in navigator).toBe(false);
|
||
|
|
});
|
||
|
|
});
|
||
|
|
|
||
|
|
describe('Service Worker Registration', () => {
|
||
|
|
it('should register service worker with correct scope', async () => {
|
||
|
|
if ('serviceWorker' in navigator) {
|
||
|
|
await navigator.serviceWorker.register('/sw.js', { scope: '/' });
|
||
|
|
|
||
|
|
expect(mockServiceWorkerContainer.register).toHaveBeenCalledWith(
|
||
|
|
'/sw.js',
|
||
|
|
expect.objectContaining({ scope: '/' })
|
||
|
|
);
|
||
|
|
}
|
||
|
|
});
|
||
|
|
|
||
|
|
it('should return registration object on successful registration', async () => {
|
||
|
|
if ('serviceWorker' in navigator) {
|
||
|
|
const registration = await navigator.serviceWorker.register('/sw.js');
|
||
|
|
|
||
|
|
expect(registration).toBeDefined();
|
||
|
|
expect(registration.scope).toBe('/');
|
||
|
|
expect(registration.active).toBeDefined();
|
||
|
|
}
|
||
|
|
});
|
||
|
|
|
||
|
|
it('should handle registration failure gracefully', async () => {
|
||
|
|
// Mock registration failure
|
||
|
|
mockServiceWorkerContainer.register = vi.fn().mockRejectedValue(
|
||
|
|
new Error('Registration failed')
|
||
|
|
);
|
||
|
|
|
||
|
|
if ('serviceWorker' in navigator) {
|
||
|
|
await expect(
|
||
|
|
navigator.serviceWorker.register('/sw.js')
|
||
|
|
).rejects.toThrow('Registration failed');
|
||
|
|
}
|
||
|
|
});
|
||
|
|
|
||
|
|
it('should get existing service worker registration', async () => {
|
||
|
|
if ('serviceWorker' in navigator) {
|
||
|
|
const registration = await navigator.serviceWorker.getRegistration('/');
|
||
|
|
|
||
|
|
expect(registration).toBeDefined();
|
||
|
|
expect(mockServiceWorkerContainer.getRegistration).toHaveBeenCalledWith('/');
|
||
|
|
}
|
||
|
|
});
|
||
|
|
|
||
|
|
it('should get all service worker registrations', async () => {
|
||
|
|
if ('serviceWorker' in navigator) {
|
||
|
|
const registrations = await navigator.serviceWorker.getRegistrations();
|
||
|
|
|
||
|
|
expect(Array.isArray(registrations)).toBe(true);
|
||
|
|
expect(registrations.length).toBeGreaterThan(0);
|
||
|
|
}
|
||
|
|
});
|
||
|
|
});
|
||
|
|
|
||
|
|
describe('Service Worker Lifecycle', () => {
|
||
|
|
it('should have active service worker after registration', async () => {
|
||
|
|
if ('serviceWorker' in navigator) {
|
||
|
|
const registration = await navigator.serviceWorker.register('/sw.js');
|
||
|
|
|
||
|
|
expect(registration.active).toBeDefined();
|
||
|
|
expect(registration.active.state).toBe('activated');
|
||
|
|
}
|
||
|
|
});
|
||
|
|
|
||
|
|
it('should update service worker', async () => {
|
||
|
|
if ('serviceWorker' in navigator) {
|
||
|
|
const registration = await navigator.serviceWorker.register('/sw.js');
|
||
|
|
await registration.update();
|
||
|
|
|
||
|
|
expect(mockRegistration.update).toHaveBeenCalled();
|
||
|
|
}
|
||
|
|
});
|
||
|
|
|
||
|
|
it('should unregister service worker', async () => {
|
||
|
|
if ('serviceWorker' in navigator) {
|
||
|
|
const registration = await navigator.serviceWorker.register('/sw.js');
|
||
|
|
await registration.unregister();
|
||
|
|
|
||
|
|
expect(mockRegistration.unregister).toHaveBeenCalled();
|
||
|
|
}
|
||
|
|
});
|
||
|
|
|
||
|
|
it('should wait for service worker to be ready', async () => {
|
||
|
|
if ('serviceWorker' in navigator) {
|
||
|
|
const registration = await navigator.serviceWorker.ready;
|
||
|
|
|
||
|
|
expect(registration).toBeDefined();
|
||
|
|
expect(registration.active).toBeDefined();
|
||
|
|
}
|
||
|
|
});
|
||
|
|
});
|
||
|
|
|
||
|
|
describe('Service Worker Communication', () => {
|
||
|
|
it('should post message to service worker', async () => {
|
||
|
|
if ('serviceWorker' in navigator) {
|
||
|
|
const registration = await navigator.serviceWorker.register('/sw.js');
|
||
|
|
const message = { type: 'SKIP_WAITING' };
|
||
|
|
|
||
|
|
registration.active.postMessage(message);
|
||
|
|
|
||
|
|
expect(mockRegistration.active.postMessage).toHaveBeenCalledWith(message);
|
||
|
|
}
|
||
|
|
});
|
||
|
|
|
||
|
|
it('should listen for service worker messages', () => {
|
||
|
|
if ('serviceWorker' in navigator) {
|
||
|
|
const messageHandler = vi.fn();
|
||
|
|
|
||
|
|
navigator.serviceWorker.addEventListener('message', messageHandler);
|
||
|
|
|
||
|
|
expect(mockServiceWorkerContainer.addEventListener).toHaveBeenCalledWith(
|
||
|
|
'message',
|
||
|
|
messageHandler
|
||
|
|
);
|
||
|
|
}
|
||
|
|
});
|
||
|
|
|
||
|
|
it('should remove message event listener', () => {
|
||
|
|
if ('serviceWorker' in navigator) {
|
||
|
|
const messageHandler = vi.fn();
|
||
|
|
|
||
|
|
navigator.serviceWorker.addEventListener('message', messageHandler);
|
||
|
|
navigator.serviceWorker.removeEventListener('message', messageHandler);
|
||
|
|
|
||
|
|
expect(mockServiceWorkerContainer.removeEventListener).toHaveBeenCalledWith(
|
||
|
|
'message',
|
||
|
|
messageHandler
|
||
|
|
);
|
||
|
|
}
|
||
|
|
});
|
||
|
|
});
|
||
|
|
|
||
|
|
describe('Workbox Integration', () => {
|
||
|
|
it('should have workbox available in service worker scope', () => {
|
||
|
|
// Mock workbox global object
|
||
|
|
const mockWorkbox = {
|
||
|
|
core: {
|
||
|
|
clientsClaim: vi.fn(),
|
||
|
|
skipWaiting: vi.fn(),
|
||
|
|
},
|
||
|
|
precaching: {
|
||
|
|
precacheAndRoute: vi.fn(),
|
||
|
|
cleanupOutdatedCaches: vi.fn(),
|
||
|
|
},
|
||
|
|
routing: {
|
||
|
|
registerRoute: vi.fn(),
|
||
|
|
},
|
||
|
|
strategies: {
|
||
|
|
CacheFirst: vi.fn(),
|
||
|
|
NetworkFirst: vi.fn(),
|
||
|
|
StaleWhileRevalidate: vi.fn(),
|
||
|
|
},
|
||
|
|
};
|
||
|
|
|
||
|
|
global.workbox = mockWorkbox;
|
||
|
|
|
||
|
|
expect(global.workbox).toBeDefined();
|
||
|
|
expect(global.workbox.core).toBeDefined();
|
||
|
|
expect(global.workbox.precaching).toBeDefined();
|
||
|
|
expect(global.workbox.routing).toBeDefined();
|
||
|
|
expect(global.workbox.strategies).toBeDefined();
|
||
|
|
|
||
|
|
// Cleanup
|
||
|
|
delete global.workbox;
|
||
|
|
});
|
||
|
|
|
||
|
|
it('should call precacheAndRoute in service worker', () => {
|
||
|
|
const mockWorkbox = {
|
||
|
|
precaching: {
|
||
|
|
precacheAndRoute: vi.fn(),
|
||
|
|
},
|
||
|
|
};
|
||
|
|
|
||
|
|
global.workbox = mockWorkbox;
|
||
|
|
|
||
|
|
// Simulate precaching
|
||
|
|
const manifest = [
|
||
|
|
{ url: '/index.html', revision: 'abc123' },
|
||
|
|
{ url: '/assets/main.js', revision: 'def456' },
|
||
|
|
];
|
||
|
|
|
||
|
|
global.workbox.precaching.precacheAndRoute(manifest);
|
||
|
|
|
||
|
|
expect(mockWorkbox.precaching.precacheAndRoute).toHaveBeenCalledWith(manifest);
|
||
|
|
|
||
|
|
// Cleanup
|
||
|
|
delete global.workbox;
|
||
|
|
});
|
||
|
|
|
||
|
|
it('should cleanup outdated caches', () => {
|
||
|
|
const mockWorkbox = {
|
||
|
|
precaching: {
|
||
|
|
cleanupOutdatedCaches: vi.fn(),
|
||
|
|
},
|
||
|
|
};
|
||
|
|
|
||
|
|
global.workbox = mockWorkbox;
|
||
|
|
|
||
|
|
global.workbox.precaching.cleanupOutdatedCaches();
|
||
|
|
|
||
|
|
expect(mockWorkbox.precaching.cleanupOutdatedCaches).toHaveBeenCalled();
|
||
|
|
|
||
|
|
// Cleanup
|
||
|
|
delete global.workbox;
|
||
|
|
});
|
||
|
|
});
|
||
|
|
|
||
|
|
describe('Cache Storage API', () => {
|
||
|
|
let mockCache;
|
||
|
|
let mockCacheStorage;
|
||
|
|
|
||
|
|
beforeEach(() => {
|
||
|
|
mockCache = {
|
||
|
|
match: vi.fn(),
|
||
|
|
matchAll: vi.fn(),
|
||
|
|
add: vi.fn(),
|
||
|
|
addAll: vi.fn(),
|
||
|
|
put: vi.fn(),
|
||
|
|
delete: vi.fn(),
|
||
|
|
keys: vi.fn(),
|
||
|
|
};
|
||
|
|
|
||
|
|
mockCacheStorage = {
|
||
|
|
open: vi.fn().mockResolvedValue(mockCache),
|
||
|
|
has: vi.fn().mockResolvedValue(true),
|
||
|
|
delete: vi.fn().mockResolvedValue(true),
|
||
|
|
keys: vi.fn().mockResolvedValue(['v1-static', 'v1-images']),
|
||
|
|
match: vi.fn(),
|
||
|
|
};
|
||
|
|
|
||
|
|
global.caches = mockCacheStorage;
|
||
|
|
});
|
||
|
|
|
||
|
|
it('should open cache storage', async () => {
|
||
|
|
const cache = await caches.open('v1-static');
|
||
|
|
|
||
|
|
expect(cache).toBeDefined();
|
||
|
|
expect(mockCacheStorage.open).toHaveBeenCalledWith('v1-static');
|
||
|
|
});
|
||
|
|
|
||
|
|
it('should check if cache exists', async () => {
|
||
|
|
const exists = await caches.has('v1-static');
|
||
|
|
|
||
|
|
expect(exists).toBe(true);
|
||
|
|
expect(mockCacheStorage.has).toHaveBeenCalledWith('v1-static');
|
||
|
|
});
|
||
|
|
|
||
|
|
it('should delete cache', async () => {
|
||
|
|
const deleted = await caches.delete('old-cache');
|
||
|
|
|
||
|
|
expect(deleted).toBe(true);
|
||
|
|
expect(mockCacheStorage.delete).toHaveBeenCalledWith('old-cache');
|
||
|
|
});
|
||
|
|
|
||
|
|
it('should list all caches', async () => {
|
||
|
|
const cacheNames = await caches.keys();
|
||
|
|
|
||
|
|
expect(Array.isArray(cacheNames)).toBe(true);
|
||
|
|
expect(cacheNames).toContain('v1-static');
|
||
|
|
expect(cacheNames).toContain('v1-images');
|
||
|
|
});
|
||
|
|
|
||
|
|
it('should add file to cache', async () => {
|
||
|
|
const cache = await caches.open('v1-static');
|
||
|
|
await cache.add('/index.html');
|
||
|
|
|
||
|
|
expect(mockCache.add).toHaveBeenCalledWith('/index.html');
|
||
|
|
});
|
||
|
|
|
||
|
|
it('should add multiple files to cache', async () => {
|
||
|
|
const cache = await caches.open('v1-static');
|
||
|
|
const urls = ['/index.html', '/main.js', '/style.css'];
|
||
|
|
|
||
|
|
await cache.addAll(urls);
|
||
|
|
|
||
|
|
expect(mockCache.addAll).toHaveBeenCalledWith(urls);
|
||
|
|
});
|
||
|
|
|
||
|
|
it('should match cached request', async () => {
|
||
|
|
mockCache.match = vi.fn().mockResolvedValue(
|
||
|
|
new Response('Cached content', { status: 200 })
|
||
|
|
);
|
||
|
|
|
||
|
|
const cache = await caches.open('v1-static');
|
||
|
|
const response = await cache.match('/index.html');
|
||
|
|
|
||
|
|
expect(response).toBeDefined();
|
||
|
|
expect(response.status).toBe(200);
|
||
|
|
expect(mockCache.match).toHaveBeenCalledWith('/index.html');
|
||
|
|
});
|
||
|
|
});
|
||
|
|
});
|