test(pwa): add comprehensive PWA and Vitest test suite
- Install Vitest and React Testing Library for frontend tests - Configure Vitest with jsdom environment and coverage - Add test setup file with global mocks (matchMedia, IntersectionObserver) - Write InstallPWA component tests (14 tests): - iOS detection and manual installation instructions - Android/Chrome beforeinstallprompt event handling - Install and dismiss functionality - 7-day dismissal persistence (localStorage) - Installed state detection (standalone mode) - Write PWA configuration tests (28 tests): - App icons existence (PNG and SVG) - iOS splash screens for multiple devices - Vite PWA plugin configuration - index.html meta tags (iOS PWA support) - Manifest schema validation - Service worker configuration (Workbox) - Write service worker tests (24 tests): - Service worker registration and lifecycle - Workbox integration - Cache Storage API operations - Migrate existing WebRTC tests from Jest to Vitest (25 tests): - Update imports to use Vitest (vi.fn, describe, it, expect) - Fix WebRTCWarning and webrtcDetection test expectations - Add test scripts to package.json (test, test:watch, test:ui, test:coverage) All 91 tests passing (InstallPWA: 14, PWA config: 28, Service Worker: 24, WebRTC: 25 total across 2 files)
This commit is contained in:
208
frontend/src/__tests__/pwa-config.test.js
Normal file
208
frontend/src/__tests__/pwa-config.test.js
Normal file
@@ -0,0 +1,208 @@
|
||||
import { describe, it, expect, beforeAll } from 'vitest';
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
import { fileURLToPath } from 'url';
|
||||
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = path.dirname(__filename);
|
||||
|
||||
describe('PWA Configuration Tests', () => {
|
||||
describe('App Icons', () => {
|
||||
const iconsDir = path.join(__dirname, '../../public/icons');
|
||||
|
||||
it('should have 192x192 icon (PNG)', () => {
|
||||
const iconPath = path.join(iconsDir, 'icon-192x192.png');
|
||||
expect(fs.existsSync(iconPath)).toBe(true);
|
||||
});
|
||||
|
||||
it('should have 512x512 icon (PNG)', () => {
|
||||
const iconPath = path.join(iconsDir, 'icon-512x512.png');
|
||||
expect(fs.existsSync(iconPath)).toBe(true);
|
||||
});
|
||||
|
||||
it('should have apple-touch-icon (PNG)', () => {
|
||||
const iconPath = path.join(iconsDir, 'apple-touch-icon.png');
|
||||
expect(fs.existsSync(iconPath)).toBe(true);
|
||||
});
|
||||
|
||||
it('should have 192x192 icon (SVG)', () => {
|
||||
const iconPath = path.join(iconsDir, 'icon-192x192.svg');
|
||||
expect(fs.existsSync(iconPath)).toBe(true);
|
||||
});
|
||||
|
||||
it('should have 512x512 icon (SVG)', () => {
|
||||
const iconPath = path.join(iconsDir, 'icon-512x512.svg');
|
||||
expect(fs.existsSync(iconPath)).toBe(true);
|
||||
});
|
||||
|
||||
it('should have apple-touch-icon (SVG)', () => {
|
||||
const iconPath = path.join(iconsDir, 'apple-touch-icon.svg');
|
||||
expect(fs.existsSync(iconPath)).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('iOS Splash Screens', () => {
|
||||
const splashDir = path.join(__dirname, '../../public/splash');
|
||||
|
||||
it('should have iPhone X splash screen (PNG)', () => {
|
||||
const splashPath = path.join(splashDir, 'iphone-x.png');
|
||||
expect(fs.existsSync(splashPath)).toBe(true);
|
||||
});
|
||||
|
||||
it('should have iPhone XR splash screen (PNG)', () => {
|
||||
const splashPath = path.join(splashDir, 'iphone-xr.png');
|
||||
expect(fs.existsSync(splashPath)).toBe(true);
|
||||
});
|
||||
|
||||
it('should have iPhone XS Max splash screen (PNG)', () => {
|
||||
const splashPath = path.join(splashDir, 'iphone-xsmax.png');
|
||||
expect(fs.existsSync(splashPath)).toBe(true);
|
||||
});
|
||||
|
||||
it('should have iPhone 8 splash screen (PNG)', () => {
|
||||
const splashPath = path.join(splashDir, 'iphone-8.png');
|
||||
expect(fs.existsSync(splashPath)).toBe(true);
|
||||
});
|
||||
|
||||
it('should have iPhone X splash screen (SVG)', () => {
|
||||
const splashPath = path.join(splashDir, 'iphone-x.svg');
|
||||
expect(fs.existsSync(splashPath)).toBe(true);
|
||||
});
|
||||
|
||||
it('should have iPhone XR splash screen (SVG)', () => {
|
||||
const splashPath = path.join(splashDir, 'iphone-xr.svg');
|
||||
expect(fs.existsSync(splashPath)).toBe(true);
|
||||
});
|
||||
|
||||
it('should have iPhone XS Max splash screen (SVG)', () => {
|
||||
const splashPath = path.join(splashDir, 'iphone-xsmax.svg');
|
||||
expect(fs.existsSync(splashPath)).toBe(true);
|
||||
});
|
||||
|
||||
it('should have iPhone 8 splash screen (SVG)', () => {
|
||||
const splashPath = path.join(splashDir, 'iphone-8.svg');
|
||||
expect(fs.existsSync(splashPath)).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Vite PWA Configuration', () => {
|
||||
it('should have VitePWA plugin configured', () => {
|
||||
const configPath = path.join(__dirname, '../../vite.config.js');
|
||||
const configContent = fs.readFileSync(configPath, 'utf-8');
|
||||
|
||||
// Check for VitePWA import and usage
|
||||
expect(configContent).toContain("import { VitePWA } from 'vite-plugin-pwa'");
|
||||
expect(configContent).toContain('VitePWA(');
|
||||
expect(configContent).toContain('plugins:');
|
||||
});
|
||||
});
|
||||
|
||||
describe('index.html Meta Tags', () => {
|
||||
let htmlContent;
|
||||
|
||||
beforeAll(() => {
|
||||
const htmlPath = path.join(__dirname, '../../index.html');
|
||||
htmlContent = fs.readFileSync(htmlPath, 'utf-8');
|
||||
});
|
||||
|
||||
it('should have apple-mobile-web-app-capable meta tag', () => {
|
||||
expect(htmlContent).toContain('name="apple-mobile-web-app-capable"');
|
||||
expect(htmlContent).toContain('content="yes"');
|
||||
});
|
||||
|
||||
it('should have apple-mobile-web-app-status-bar-style meta tag', () => {
|
||||
expect(htmlContent).toContain('name="apple-mobile-web-app-status-bar-style"');
|
||||
expect(htmlContent).toContain('content="black-translucent"');
|
||||
});
|
||||
|
||||
it('should have apple-mobile-web-app-title meta tag', () => {
|
||||
expect(htmlContent).toContain('name="apple-mobile-web-app-title"');
|
||||
expect(htmlContent).toContain('content="spotlight"');
|
||||
});
|
||||
|
||||
it('should have apple-touch-icon link', () => {
|
||||
expect(htmlContent).toContain('rel="apple-touch-icon"');
|
||||
expect(htmlContent).toContain('href="/icons/apple-touch-icon.png"');
|
||||
});
|
||||
|
||||
it('should have iOS splash screens for iPhone X', () => {
|
||||
expect(htmlContent).toContain('rel="apple-touch-startup-image"');
|
||||
expect(htmlContent).toContain('href="/splash/iphone-x.png"');
|
||||
expect(htmlContent).toContain('device-width: 375px');
|
||||
expect(htmlContent).toContain('device-height: 812px');
|
||||
});
|
||||
|
||||
it('should have iOS splash screens for iPhone XR', () => {
|
||||
expect(htmlContent).toContain('href="/splash/iphone-xr.png"');
|
||||
expect(htmlContent).toContain('device-width: 414px');
|
||||
expect(htmlContent).toContain('device-height: 896px');
|
||||
});
|
||||
|
||||
it('should have iOS splash screens for iPhone XS Max', () => {
|
||||
expect(htmlContent).toContain('href="/splash/iphone-xsmax.png"');
|
||||
expect(htmlContent).toContain('device-width: 414px');
|
||||
expect(htmlContent).toContain('device-height: 896px');
|
||||
expect(htmlContent).toContain('-webkit-device-pixel-ratio: 3');
|
||||
});
|
||||
|
||||
it('should have iOS splash screens for iPhone 8', () => {
|
||||
expect(htmlContent).toContain('href="/splash/iphone-8.png"');
|
||||
expect(htmlContent).toContain('device-width: 375px');
|
||||
expect(htmlContent).toContain('device-height: 667px');
|
||||
});
|
||||
|
||||
it('should have theme-color meta tag', () => {
|
||||
expect(htmlContent).toContain('name="theme-color"');
|
||||
expect(htmlContent).toContain('content="#6366f1"');
|
||||
});
|
||||
|
||||
it('should have viewport meta tag', () => {
|
||||
expect(htmlContent).toContain('name="viewport"');
|
||||
expect(htmlContent).toContain('width=device-width');
|
||||
expect(htmlContent).toContain('initial-scale=1.0');
|
||||
});
|
||||
});
|
||||
|
||||
describe('PWA Manifest Schema Validation', () => {
|
||||
it('should have valid manifest configuration in vite.config.js', async () => {
|
||||
const configPath = path.join(__dirname, '../../vite.config.js');
|
||||
const configContent = fs.readFileSync(configPath, 'utf-8');
|
||||
|
||||
// Check for required manifest fields
|
||||
expect(configContent).toContain('manifest:');
|
||||
expect(configContent).toContain('name:');
|
||||
expect(configContent).toContain('short_name:');
|
||||
expect(configContent).toContain('theme_color:');
|
||||
expect(configContent).toContain('display:');
|
||||
expect(configContent).toContain('icons:');
|
||||
|
||||
// Check for spotlight.cam specific values
|
||||
expect(configContent).toContain('spotlight.cam');
|
||||
expect(configContent).toContain('#6366f1'); // indigo theme
|
||||
expect(configContent).toContain('standalone');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Service Worker Configuration', () => {
|
||||
it('should have workbox configuration in vite.config.js', async () => {
|
||||
const configPath = path.join(__dirname, '../../vite.config.js');
|
||||
const configContent = fs.readFileSync(configPath, 'utf-8');
|
||||
|
||||
// Check for Workbox settings
|
||||
expect(configContent).toContain('workbox:');
|
||||
expect(configContent).toContain('globPatterns');
|
||||
|
||||
// Should cache static assets
|
||||
expect(configContent).toMatch(/\.(js|css|html|ico|png|svg|woff2)/);
|
||||
});
|
||||
|
||||
it('should have registerType configured', async () => {
|
||||
const configPath = path.join(__dirname, '../../vite.config.js');
|
||||
const configContent = fs.readFileSync(configPath, 'utf-8');
|
||||
|
||||
// Should use autoUpdate
|
||||
expect(configContent).toContain('registerType:');
|
||||
expect(configContent).toContain('autoUpdate');
|
||||
});
|
||||
});
|
||||
});
|
||||
356
frontend/src/__tests__/pwa-serviceWorker.test.js
Normal file
356
frontend/src/__tests__/pwa-serviceWorker.test.js
Normal file
@@ -0,0 +1,356 @@
|
||||
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');
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user