/** * Tests for EnvironmentDetector */ import { EnvironmentDetector } from '../EnvironmentDetector'; describe('EnvironmentDetector', () => { let detector: EnvironmentDetector; let originalUserAgent: string; beforeEach(() => { // Save original userAgent originalUserAgent = navigator.userAgent; // Set default userAgent for tests (can be overridden in individual tests) Object.defineProperty(navigator, 'userAgent', { writable: true, configurable: true, value: 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 Chrome/120.0.0.0' }); // Mock screen dimensions (required for device detection) Object.defineProperty(screen, 'width', { writable: true, configurable: true, value: 1920 }); Object.defineProperty(screen, 'height', { writable: true, configurable: true, value: 1080 }); // Reset navigator and window mocks jest.clearAllMocks(); }); afterEach(() => { // Restore original userAgent Object.defineProperty(navigator, 'userAgent', { writable: true, value: originalUserAgent }); }); describe('Browser Detection', () => { it('should detect Chrome browser', () => { Object.defineProperty(navigator, 'userAgent', { writable: true, value: 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36' }); detector = new EnvironmentDetector(); const browserInfo = detector.getBrowserInfo(); expect(browserInfo.name).toBe('chrome'); expect(browserInfo.engine).toBe('blink'); }); it('should detect Firefox browser', () => { Object.defineProperty(navigator, 'userAgent', { writable: true, value: 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:109.0) Gecko/20100101 Firefox/120.0' }); detector = new EnvironmentDetector(); const browserInfo = detector.getBrowserInfo(); expect(browserInfo.name).toBe('firefox'); expect(browserInfo.engine).toBe('gecko'); }); it('should detect Safari browser', () => { Object.defineProperty(navigator, 'userAgent', { writable: true, value: 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.0 Safari/605.1.15' }); detector = new EnvironmentDetector(); const browserInfo = detector.getBrowserInfo(); expect(browserInfo.name).toBe('safari'); expect(browserInfo.engine).toBe('webkit'); }); it('should detect Edge browser', () => { Object.defineProperty(navigator, 'userAgent', { writable: true, value: 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36 Edg/120.0.0.0' }); detector = new EnvironmentDetector(); const browserInfo = detector.getBrowserInfo(); expect(browserInfo.name).toBe('edge'); expect(browserInfo.engine).toBe('blink'); }); }); describe('Device Detection', () => { it('should detect desktop device', () => { Object.defineProperty(navigator, 'userAgent', { writable: true, value: 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 Chrome/120.0.0.0' }); detector = new EnvironmentDetector(); expect(detector.isDesktop()).toBe(true); expect(detector.isMobile()).toBe(false); expect(detector.isTablet()).toBe(false); }); it('should detect mobile device', () => { Object.defineProperty(navigator, 'userAgent', { writable: true, value: 'Mozilla/5.0 (iPhone; CPU iPhone OS 17_0 like Mac OS X) AppleWebKit/605.1.15 Mobile/15E148' }); detector = new EnvironmentDetector(); expect(detector.isMobile()).toBe(true); expect(detector.isTablet()).toBe(false); expect(detector.isDesktop()).toBe(false); }); it('should detect tablet device', () => { Object.defineProperty(navigator, 'userAgent', { writable: true, value: 'Mozilla/5.0 (iPad; CPU OS 17_0 like Mac OS X) AppleWebKit/605.1.15' }); detector = new EnvironmentDetector(); expect(detector.isTablet()).toBe(true); expect(detector.isMobile()).toBe(false); expect(detector.isDesktop()).toBe(false); }); it('should detect iOS', () => { Object.defineProperty(navigator, 'userAgent', { writable: true, value: 'Mozilla/5.0 (iPhone; CPU iPhone OS 17_0 like Mac OS X)' }); detector = new EnvironmentDetector(); const deviceInfo = detector.getDeviceInfo(); expect(deviceInfo.os).toBe('ios'); }); it('should detect Android', () => { Object.defineProperty(navigator, 'userAgent', { writable: true, value: 'Mozilla/5.0 (Linux; Android 14) AppleWebKit/537.36 Mobile Safari/537.36' }); detector = new EnvironmentDetector(); const deviceInfo = detector.getDeviceInfo(); expect(deviceInfo.os).toBe('android'); }); it('should detect Windows', () => { Object.defineProperty(navigator, 'userAgent', { writable: true, value: 'Mozilla/5.0 (Windows NT 10.0; Win64; x64)' }); detector = new EnvironmentDetector(); const deviceInfo = detector.getDeviceInfo(); expect(deviceInfo.os).toBe('windows'); }); it('should detect macOS', () => { Object.defineProperty(navigator, 'userAgent', { writable: true, value: 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7)' }); detector = new EnvironmentDetector(); const deviceInfo = detector.getDeviceInfo(); expect(deviceInfo.os).toBe('macos'); }); }); describe('Framework Detection', () => { it('should detect React', () => { (window as any).React = {}; detector = new EnvironmentDetector(); expect(detector.getFramework()).toBe('react'); delete (window as any).React; }); it('should detect Vue', () => { (window as any).Vue = {}; detector = new EnvironmentDetector(); expect(detector.getFramework()).toBe('vue'); delete (window as any).Vue; }); it('should detect Angular', () => { (window as any).ng = {}; detector = new EnvironmentDetector(); expect(detector.getFramework()).toBe('angular'); delete (window as any).ng; }); it('should default to vanilla when no framework detected', () => { detector = new EnvironmentDetector(); expect(detector.getFramework()).toBe('vanilla'); }); }); describe('Platform Detection', () => { it('should detect web platform for desktop', () => { Object.defineProperty(navigator, 'userAgent', { writable: true, value: 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) Chrome/120.0.0.0' }); detector = new EnvironmentDetector(); expect(detector.getPlatform()).toBe('web'); }); it('should detect mobile-web platform', () => { Object.defineProperty(navigator, 'userAgent', { writable: true, value: 'Mozilla/5.0 (iPhone; CPU iPhone OS 17_0) Mobile Safari' }); detector = new EnvironmentDetector(); expect(detector.getPlatform()).toBe('mobile-web'); }); }); describe('Feature Support', () => { it('should detect localStorage support', () => { detector = new EnvironmentDetector(); expect(detector.supportsLocalStorage()).toBe(true); }); it('should detect service worker support', () => { Object.defineProperty(navigator, 'serviceWorker', { writable: true, value: {} }); detector = new EnvironmentDetector(); expect(detector.supportsServiceWorkers()).toBe(true); }); it('should detect modern features support', () => { (window as any).fetch = () => {}; (window as any).Promise = Promise; (window as any).IntersectionObserver = class {}; (window as any).requestIdleCallback = () => {}; detector = new EnvironmentDetector(); expect(detector.supportsModernFeatures()).toBe(true); }); }); describe('Touch Support', () => { it('should detect touch support via ontouchstart', () => { (window as any).ontouchstart = {}; detector = new EnvironmentDetector(); expect(detector.hasTouchSupport()).toBe(true); delete (window as any).ontouchstart; }); it('should detect touch support via maxTouchPoints', () => { Object.defineProperty(navigator, 'maxTouchPoints', { writable: true, value: 5 }); detector = new EnvironmentDetector(); expect(detector.hasTouchSupport()).toBe(true); }); it('should return false when no touch support', () => { Object.defineProperty(navigator, 'maxTouchPoints', { writable: true, value: 0 }); detector = new EnvironmentDetector(); expect(detector.hasTouchSupport()).toBe(false); }); }); describe('WebView Detection', () => { it('should detect Android WebView', () => { Object.defineProperty(navigator, 'userAgent', { writable: true, value: 'Mozilla/5.0 (Linux; Android 14; wv) AppleWebKit/537.36' }); detector = new EnvironmentDetector(); expect(detector.isWebView()).toBe(true); }); it('should detect Facebook app', () => { Object.defineProperty(navigator, 'userAgent', { writable: true, value: 'Mozilla/5.0 (iPhone; CPU iPhone OS 17_0) FBAN/FBIOS' }); detector = new EnvironmentDetector(); expect(detector.isWebView()).toBe(true); }); it('should return false for regular browser', () => { Object.defineProperty(navigator, 'userAgent', { writable: true, value: 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) Chrome/120.0.0.0' }); detector = new EnvironmentDetector(); expect(detector.isWebView()).toBe(false); }); }); describe('Dimensions and Density', () => { it('should get viewport dimensions', () => { Object.defineProperty(window, 'innerWidth', { writable: true, value: 1920 }); Object.defineProperty(window, 'innerHeight', { writable: true, value: 1080 }); detector = new EnvironmentDetector(); const viewport = detector.getViewportDimensions(); expect(viewport.width).toBe(1920); expect(viewport.height).toBe(1080); }); it('should get screen dimensions', () => { Object.defineProperty(screen, 'width', { writable: true, value: 1920 }); Object.defineProperty(screen, 'height', { writable: true, value: 1080 }); detector = new EnvironmentDetector(); const screenDimensions = detector.getScreenDimensions(); expect(screenDimensions.width).toBe(1920); expect(screenDimensions.height).toBe(1080); }); it('should get pixel density', () => { Object.defineProperty(window, 'devicePixelRatio', { writable: true, value: 2 }); detector = new EnvironmentDetector(); expect(detector.getPixelDensity()).toBe(2); }); it('should default pixel density to 1', () => { Object.defineProperty(window, 'devicePixelRatio', { writable: true, value: undefined }); detector = new EnvironmentDetector(); expect(detector.getPixelDensity()).toBe(1); }); }); describe('Environment Info', () => { it('should get complete environment info', () => { detector = new EnvironmentDetector(); const envInfo = detector.getEnvironmentInfo(); expect(envInfo).toHaveProperty('framework'); expect(envInfo).toHaveProperty('isSPA'); expect(envInfo).toHaveProperty('isSSR'); expect(envInfo).toHaveProperty('platform'); expect(envInfo).toHaveProperty('browser'); expect(envInfo).toHaveProperty('device'); }); it('should return a copy of environment info', () => { detector = new EnvironmentDetector(); const envInfo1 = detector.getEnvironmentInfo(); const envInfo2 = detector.getEnvironmentInfo(); expect(envInfo1).not.toBe(envInfo2); expect(envInfo1).toEqual(envInfo2); }); it('should update environment info', () => { detector = new EnvironmentDetector(); const oldInfo = detector.getEnvironmentInfo(); // Change something (window as any).React = {}; detector.updateEnvironmentInfo(); const newInfo = detector.getEnvironmentInfo(); // Framework should now be detected as React expect(newInfo.framework).toBe('react'); delete (window as any).React; }); }); describe('SPA and SSR Detection', () => { it('should detect SPA', () => { // Use delete and redefine to work around jsdom's location restrictions delete (window as any).location; (window as any).location = { hash: '#/app', href: 'http://localhost/#/app', search: '', pathname: '/' }; detector = new EnvironmentDetector(); expect(detector.isSPA()).toBe(true); }); it('should detect SSR', () => { const meta = document.createElement('meta'); meta.name = 'generator'; meta.content = 'Next.js'; document.head.appendChild(meta); detector = new EnvironmentDetector(); expect(detector.isSSR()).toBe(true); document.head.removeChild(meta); }); }); describe('Network Information', () => { it('should get network info when available', () => { (navigator as any).connection = { effectiveType: '4g', downlink: 10, rtt: 50 }; detector = new EnvironmentDetector(); const networkInfo = detector.getNetworkInfo(); expect(networkInfo).toEqual({ effectiveType: '4g', downlink: 10, rtt: 50 }); delete (navigator as any).connection; }); it('should return undefined when network info not available', () => { delete (navigator as any).connection; detector = new EnvironmentDetector(); const networkInfo = detector.getNetworkInfo(); expect(networkInfo).toBeUndefined(); }); }); describe('Private Mode Detection', () => { it('should detect private mode using localStorage test', async () => { // Mock localStorage to throw error const originalSetItem = Storage.prototype.setItem; Storage.prototype.setItem = jest.fn(() => { throw new Error('QuotaExceededError'); }); detector = new EnvironmentDetector(); const isPrivate = await detector.isPrivateMode(); expect(isPrivate).toBe(true); // Restore Storage.prototype.setItem = originalSetItem; }); it('should return false when not in private mode', async () => { detector = new EnvironmentDetector(); const isPrivate = await detector.isPrivateMode(); // In jsdom, private mode detection should return false expect(isPrivate).toBe(false); }); }); describe('Performance Timing', () => { it('should get performance timing when available', () => { const mockTiming = { navigationStart: 1000, domContentLoadedEventEnd: 2000, loadEventEnd: 3000, domInteractive: 1500 }; Object.defineProperty(window, 'performance', { writable: true, value: { timing: mockTiming, getEntriesByType: () => [] } }); detector = new EnvironmentDetector(); const timing = detector.getPerformanceTiming(); expect(timing).not.toBeNull(); expect(timing?.navigationStart).toBe(1000); expect(timing?.domContentLoaded).toBe(1000); expect(timing?.loadComplete).toBe(2000); }); it('should return null when performance timing not available', () => { Object.defineProperty(window, 'performance', { writable: true, value: undefined }); detector = new EnvironmentDetector(); const timing = detector.getPerformanceTiming(); expect(timing).toBeNull(); }); }); describe('Caching', () => { it('should cache detection results', () => { detector = new EnvironmentDetector(); // Call twice const framework1 = detector.getFramework(); const framework2 = detector.getFramework(); // Should return same result expect(framework1).toBe(framework2); }); it('should clear cache on update', () => { detector = new EnvironmentDetector(); const oldFramework = detector.getFramework(); // Add React (window as any).React = {}; // Update should clear cache and re-detect detector.updateEnvironmentInfo(); const newFramework = detector.getFramework(); expect(oldFramework).toBe('vanilla'); expect(newFramework).toBe('react'); delete (window as any).React; }); }); });