diff --git a/web/__tests__/browserCheck.test.ts b/web/__tests__/browserCheck.test.ts new file mode 100644 index 0000000..e82db02 --- /dev/null +++ b/web/__tests__/browserCheck.test.ts @@ -0,0 +1,366 @@ +/** + * Unit tests for browserCheck module + */ + +import { + isBrowserSupported, + getBrowserInfo, + getRecommendedBrowsers, + getUnsupportedMessage, +} from '../lib/browserCheck'; + +describe('browserCheck', () => { + // Store original navigator + const originalNavigator = global.navigator; + + beforeEach(() => { + // Reset navigator before each test + Object.defineProperty(global, 'navigator', { + writable: true, + configurable: true, + value: { + userAgent: '', + platform: '', + }, + }); + }); + + afterEach(() => { + // Restore original navigator + Object.defineProperty(global, 'navigator', { + writable: true, + configurable: true, + value: originalNavigator, + }); + }); + + describe('isBrowserSupported', () => { + it('should return true for Chrome 70+', () => { + Object.defineProperty(global.navigator, 'userAgent', { + writable: true, + value: + 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/100.0.0.0 Safari/537.36', + }); + Object.defineProperty(global.navigator, 'platform', { + writable: true, + value: 'Win32', + }); + Object.defineProperty(global.navigator, 'bluetooth', { + writable: true, + value: {}, + }); + + expect(isBrowserSupported()).toBe(true); + }); + + it('should return false for Chrome below version 70', () => { + Object.defineProperty(global.navigator, 'userAgent', { + writable: true, + value: + 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/69.0.0.0 Safari/537.36', + }); + Object.defineProperty(global.navigator, 'platform', { + writable: true, + value: 'Win32', + }); + + expect(isBrowserSupported()).toBe(false); + }); + + it('should return true for Edge 79+', () => { + Object.defineProperty(global.navigator, 'userAgent', { + writable: true, + value: + 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/100.0.0.0 Safari/537.36 Edg/100.0.0.0', + }); + Object.defineProperty(global.navigator, 'platform', { + writable: true, + value: 'Win32', + }); + Object.defineProperty(global.navigator, 'bluetooth', { + writable: true, + value: {}, + }); + + expect(isBrowserSupported()).toBe(true); + }); + + it('should return false for Edge below version 79', () => { + Object.defineProperty(global.navigator, 'userAgent', { + writable: true, + value: + 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/78.0.0.0 Safari/537.36 Edg/78.0.0.0', + }); + Object.defineProperty(global.navigator, 'platform', { + writable: true, + value: 'Win32', + }); + + expect(isBrowserSupported()).toBe(false); + }); + + it('should return true for Opera 57+', () => { + Object.defineProperty(global.navigator, 'userAgent', { + writable: true, + value: + 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/100.0.0.0 Safari/537.36 OPR/86.0.0.0', + }); + Object.defineProperty(global.navigator, 'platform', { + writable: true, + value: 'Win32', + }); + Object.defineProperty(global.navigator, 'bluetooth', { + writable: true, + value: {}, + }); + + expect(isBrowserSupported()).toBe(true); + }); + + it('should return false for Opera below version 57', () => { + Object.defineProperty(global.navigator, 'userAgent', { + writable: true, + value: + 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.0.0 Safari/537.36 OPR/56.0.0.0', + }); + Object.defineProperty(global.navigator, 'platform', { + writable: true, + value: 'Win32', + }); + + expect(isBrowserSupported()).toBe(false); + }); + + it('should return false for Safari', () => { + Object.defineProperty(global.navigator, 'userAgent', { + writable: true, + value: + 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/16.0 Safari/605.1.15', + }); + Object.defineProperty(global.navigator, 'platform', { + writable: true, + value: 'MacIntel', + }); + + expect(isBrowserSupported()).toBe(false); + }); + + it('should return false for Firefox', () => { + Object.defineProperty(global.navigator, 'userAgent', { + writable: true, + value: + 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:109.0) Gecko/20100101 Firefox/115.0', + }); + Object.defineProperty(global.navigator, 'platform', { + writable: true, + value: 'Win32', + }); + + expect(isBrowserSupported()).toBe(false); + }); + + it('should return false for iOS browsers', () => { + Object.defineProperty(global.navigator, 'userAgent', { + writable: true, + value: + 'Mozilla/5.0 (iPhone; CPU iPhone OS 16_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/16.0 Mobile/15E148 Safari/604.1', + }); + Object.defineProperty(global.navigator, 'platform', { + writable: true, + value: 'iPhone', + }); + + expect(isBrowserSupported()).toBe(false); + }); + }); + + describe('getBrowserInfo', () => { + it('should return correct info for Chrome on Windows', () => { + Object.defineProperty(global.navigator, 'userAgent', { + writable: true, + value: + 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/100.0.0.0 Safari/537.36', + }); + Object.defineProperty(global.navigator, 'platform', { + writable: true, + value: 'Win32', + }); + Object.defineProperty(global.navigator, 'bluetooth', { + writable: true, + value: {}, + }); + + const info = getBrowserInfo(); + expect(info.name).toBe('Chrome'); + expect(info.version).toBe('100'); + expect(info.platform).toBe('Windows'); + expect(info.hasWebBluetooth).toBe(true); + expect(info.isSupported).toBe(true); + }); + + it('should return correct info for Edge on macOS', () => { + Object.defineProperty(global.navigator, 'userAgent', { + writable: true, + value: + 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/100.0.0.0 Safari/537.36 Edg/100.0.0.0', + }); + Object.defineProperty(global.navigator, 'platform', { + writable: true, + value: 'MacIntel', + }); + Object.defineProperty(global.navigator, 'bluetooth', { + writable: true, + value: {}, + }); + + const info = getBrowserInfo(); + expect(info.name).toBe('Edge'); + expect(info.version).toBe('100'); + expect(info.platform).toBe('macOS'); + expect(info.hasWebBluetooth).toBe(true); + expect(info.isSupported).toBe(true); + }); + + it('should detect when Web Bluetooth is not available', () => { + Object.defineProperty(global.navigator, 'userAgent', { + writable: true, + value: + 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/100.0.0.0 Safari/537.36', + }); + Object.defineProperty(global.navigator, 'platform', { + writable: true, + value: 'Win32', + }); + // Explicitly delete bluetooth property + delete (global.navigator as any).bluetooth; + + const info = getBrowserInfo(); + expect(info.hasWebBluetooth).toBe(false); + }); + + it('should handle Safari correctly', () => { + Object.defineProperty(global.navigator, 'userAgent', { + writable: true, + value: + 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/16.0 Safari/605.1.15', + }); + Object.defineProperty(global.navigator, 'platform', { + writable: true, + value: 'MacIntel', + }); + + const info = getBrowserInfo(); + expect(info.name).toBe('Safari'); + expect(info.isSupported).toBe(false); + }); + }); + + describe('getRecommendedBrowsers', () => { + it('should return an array of recommended browsers', () => { + const browsers = getRecommendedBrowsers(); + expect(Array.isArray(browsers)).toBe(true); + expect(browsers.length).toBeGreaterThan(0); + }); + + it('should include Chrome, Edge, and Opera', () => { + const browsers = getRecommendedBrowsers(); + const browserNames = browsers.map((b) => b.name); + expect(browserNames).toContain('Google Chrome'); + expect(browserNames).toContain('Microsoft Edge'); + expect(browserNames).toContain('Opera'); + }); + + it('should have correct minimum versions', () => { + const browsers = getRecommendedBrowsers(); + const chrome = browsers.find((b) => b.name === 'Google Chrome'); + const edge = browsers.find((b) => b.name === 'Microsoft Edge'); + const opera = browsers.find((b) => b.name === 'Opera'); + + expect(chrome?.minVersion).toBe('70'); + expect(edge?.minVersion).toBe('79'); + expect(opera?.minVersion).toBe('57'); + }); + + it('should include download URLs', () => { + const browsers = getRecommendedBrowsers(); + browsers.forEach((browser) => { + expect(browser.downloadUrl).toBeTruthy(); + expect(browser.downloadUrl.startsWith('https://')).toBe(true); + }); + }); + + it('should include platform information', () => { + const browsers = getRecommendedBrowsers(); + browsers.forEach((browser) => { + expect(Array.isArray(browser.platforms)).toBe(true); + expect(browser.platforms.length).toBeGreaterThan(0); + }); + }); + }); + + describe('getUnsupportedMessage', () => { + it('should return iOS-specific message for iOS platform', () => { + const browserInfo = { + name: 'Safari', + version: '16', + isSupported: false, + hasWebBluetooth: false, + platform: 'iOS', + }; + const message = getUnsupportedMessage(browserInfo); + expect(message).toContain('iOS'); + expect(message).toContain('system limitations'); + }); + + it('should return Safari-specific message', () => { + const browserInfo = { + name: 'Safari', + version: '16', + isSupported: false, + hasWebBluetooth: false, + platform: 'macOS', + }; + const message = getUnsupportedMessage(browserInfo); + expect(message).toContain('Safari'); + expect(message).toContain('does not support'); + }); + + it('should return Firefox-specific message', () => { + const browserInfo = { + name: 'Firefox', + version: '115', + isSupported: false, + hasWebBluetooth: false, + platform: 'Windows', + }; + const message = getUnsupportedMessage(browserInfo); + expect(message).toContain('Firefox'); + expect(message).toContain('privacy concerns'); + }); + + it('should return generic message for browsers without Web Bluetooth', () => { + const browserInfo = { + name: 'Chrome', + version: '50', + isSupported: false, + hasWebBluetooth: false, + platform: 'Windows', + }; + const message = getUnsupportedMessage(browserInfo); + expect(message).toContain('does not support'); + expect(message).toContain('Web Bluetooth API'); + }); + + it('should return version requirement message for supported browsers with old versions', () => { + const browserInfo = { + name: 'Chrome', + version: '69', + isSupported: false, + hasWebBluetooth: true, + platform: 'Windows', + }; + const message = getUnsupportedMessage(browserInfo); + expect(message).toContain('Chrome 70+'); + }); + }); +}); diff --git a/web/lib/browserCheck.ts b/web/lib/browserCheck.ts new file mode 100644 index 0000000..77b52f1 --- /dev/null +++ b/web/lib/browserCheck.ts @@ -0,0 +1,197 @@ +/** + * Browser Compatibility Check Module + * Detects browser support for Web Bluetooth API and provides compatibility information + */ + +export interface BrowserInfo { + name: string; + version: string; + isSupported: boolean; + hasWebBluetooth: boolean; + platform: string; +} + +export interface RecommendedBrowser { + name: string; + minVersion: string; + downloadUrl: string; + platforms: string[]; +} + +/** + * Detects the current browser name and version from user agent + */ +function detectBrowser(): { name: string; version: string } { + if (typeof window === 'undefined' || typeof navigator === 'undefined') { + return { name: 'Unknown', version: '0' }; + } + + const userAgent = navigator.userAgent; + let name = 'Unknown'; + let version = '0'; + + // Chrome (must check before Safari as it contains both, exclude Edge and Opera) + if (/Chrome\/(\d+)/.test(userAgent) && !/Edg/.test(userAgent) && !/OPR/.test(userAgent)) { + name = 'Chrome'; + const match = userAgent.match(/Chrome\/(\d+)/); + version = match ? match[1] : '0'; + } + // Edge (Chromium-based) + else if (/Edg\/(\d+)/.test(userAgent)) { + name = 'Edge'; + const match = userAgent.match(/Edg\/(\d+)/); + version = match ? match[1] : '0'; + } + // Opera + else if (/OPR\/(\d+)/.test(userAgent)) { + name = 'Opera'; + const match = userAgent.match(/OPR\/(\d+)/); + version = match ? match[1] : '0'; + } + // Safari (check after Chrome) + else if (/Safari\/(\d+)/.test(userAgent) && !/Chrome/.test(userAgent)) { + name = 'Safari'; + const match = userAgent.match(/Version\/(\d+)/); + version = match ? match[1] : '0'; + } + // Firefox + else if (/Firefox\/(\d+)/.test(userAgent)) { + name = 'Firefox'; + const match = userAgent.match(/Firefox\/(\d+)/); + version = match ? match[1] : '0'; + } + + return { name, version }; +} + +/** + * Detects the platform/OS + */ +function detectPlatform(): string { + if (typeof window === 'undefined' || typeof navigator === 'undefined') { + return 'Unknown'; + } + + const userAgent = navigator.userAgent; + const platform = navigator.platform; + + if (/Win/.test(platform)) return 'Windows'; + if (/Mac/.test(platform)) return 'macOS'; + if (/Linux/.test(platform)) return 'Linux'; + if (/iPhone|iPad|iPod/.test(userAgent)) return 'iOS'; + if (/Android/.test(userAgent)) return 'Android'; + + return 'Unknown'; +} + +/** + * Checks if Web Bluetooth API is available + */ +function hasWebBluetoothAPI(): boolean { + if (typeof navigator === 'undefined') { + return false; + } + + return 'bluetooth' in navigator; +} + +/** + * Checks if the current browser is supported for WellNuo Web + * Supported browsers: Chrome 70+, Edge 79+, Opera 57+ (Windows 10+, macOS) + */ +export function isBrowserSupported(): boolean { + const { name, version } = detectBrowser(); + const versionNum = parseInt(version, 10); + const platform = detectPlatform(); + + // iOS is not supported (system limitations) + if (platform === 'iOS') { + return false; + } + + // Safari and Firefox don't support Web Bluetooth + if (name === 'Safari' || name === 'Firefox') { + return false; + } + + // Check version requirements + switch (name) { + case 'Chrome': + return versionNum >= 70; + case 'Edge': + return versionNum >= 79; + case 'Opera': + return versionNum >= 57; + default: + return false; + } +} + +/** + * Gets detailed information about the current browser + */ +export function getBrowserInfo(): BrowserInfo { + const { name, version } = detectBrowser(); + const platform = detectPlatform(); + const hasWebBluetooth = hasWebBluetoothAPI(); + const isSupported = isBrowserSupported(); + + return { + name, + version, + isSupported, + hasWebBluetooth, + platform, + }; +} + +/** + * Returns list of recommended browsers with download links + */ +export function getRecommendedBrowsers(): RecommendedBrowser[] { + return [ + { + name: 'Google Chrome', + minVersion: '70', + downloadUrl: 'https://www.google.com/chrome/', + platforms: ['Windows 10+', 'macOS', 'Linux'], + }, + { + name: 'Microsoft Edge', + minVersion: '79', + downloadUrl: 'https://www.microsoft.com/edge', + platforms: ['Windows 10+', 'macOS'], + }, + { + name: 'Opera', + minVersion: '57', + downloadUrl: 'https://www.opera.com/', + platforms: ['Windows', 'macOS', 'Linux'], + }, + ]; +} + +/** + * Gets a user-friendly message explaining why the browser is not supported + */ +export function getUnsupportedMessage(browserInfo: BrowserInfo): string { + const { name, platform } = browserInfo; + + if (platform === 'iOS') { + return 'Web Bluetooth is not supported on iOS due to system limitations. Please use a desktop browser or download our mobile app.'; + } + + if (name === 'Safari') { + return 'Safari does not support Web Bluetooth API. Please use Chrome, Edge, or Opera instead.'; + } + + if (name === 'Firefox') { + return 'Firefox does not support Web Bluetooth API due to privacy concerns. Please use Chrome, Edge, or Opera instead.'; + } + + if (!browserInfo.hasWebBluetooth) { + return 'Your browser does not support Web Bluetooth API. Please update to a newer version or try Chrome, Edge, or Opera.'; + } + + return 'Your browser is not supported. Please use Chrome 70+, Edge 79+, or Opera 57+.'; +}