- Add webBluetooth.ts with browser detection and compatibility checks - Add WebBLEManager implementing IBLEManager for Web Bluetooth API - Add BrowserNotSupported component showing clear error for Safari/Firefox - Update services/ble/index.ts to use WebBLEManager on web platform - Add comprehensive tests for browser detection and WebBLEManager Works in Chrome/Edge/Opera, shows user-friendly error in unsupported browsers. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
338 lines
10 KiB
TypeScript
338 lines
10 KiB
TypeScript
// Tests for Web Bluetooth browser compatibility utilities
|
|
|
|
import {
|
|
detectBrowser,
|
|
checkWebBluetoothSupport,
|
|
getUnsupportedBrowserMessage,
|
|
isWebPlatform,
|
|
hasWebBluetooth,
|
|
SUPPORTED_BROWSERS,
|
|
BROWSER_HELP_URLS,
|
|
} from '../webBluetooth';
|
|
|
|
describe('webBluetooth utilities', () => {
|
|
// Store original navigator and window
|
|
const originalNavigator = global.navigator;
|
|
const originalWindow = global.window;
|
|
|
|
afterEach(() => {
|
|
// Restore originals
|
|
Object.defineProperty(global, 'navigator', {
|
|
value: originalNavigator,
|
|
writable: true,
|
|
configurable: true,
|
|
});
|
|
Object.defineProperty(global, 'window', {
|
|
value: originalWindow,
|
|
writable: true,
|
|
configurable: true,
|
|
});
|
|
});
|
|
|
|
describe('detectBrowser', () => {
|
|
it('should detect Chrome', () => {
|
|
Object.defineProperty(global, 'navigator', {
|
|
value: {
|
|
userAgent: 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',
|
|
},
|
|
writable: true,
|
|
configurable: true,
|
|
});
|
|
|
|
const browser = detectBrowser();
|
|
expect(browser.name).toBe('Chrome');
|
|
expect(browser.version).toBe('120');
|
|
});
|
|
|
|
it('should detect Safari', () => {
|
|
Object.defineProperty(global, 'navigator', {
|
|
value: {
|
|
userAgent: 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.2 Safari/605.1.15',
|
|
},
|
|
writable: true,
|
|
configurable: true,
|
|
});
|
|
|
|
const browser = detectBrowser();
|
|
expect(browser.name).toBe('Safari');
|
|
expect(browser.version).toBe('17');
|
|
});
|
|
|
|
it('should detect Firefox', () => {
|
|
Object.defineProperty(global, 'navigator', {
|
|
value: {
|
|
userAgent: 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:122.0) Gecko/20100101 Firefox/122.0',
|
|
},
|
|
writable: true,
|
|
configurable: true,
|
|
});
|
|
|
|
const browser = detectBrowser();
|
|
expect(browser.name).toBe('Firefox');
|
|
expect(browser.version).toBe('122');
|
|
});
|
|
|
|
it('should detect Edge', () => {
|
|
Object.defineProperty(global, 'navigator', {
|
|
value: {
|
|
userAgent: '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',
|
|
},
|
|
writable: true,
|
|
configurable: true,
|
|
});
|
|
|
|
const browser = detectBrowser();
|
|
expect(browser.name).toBe('Edge');
|
|
expect(browser.version).toBe('120');
|
|
});
|
|
|
|
it('should detect Opera', () => {
|
|
Object.defineProperty(global, 'navigator', {
|
|
value: {
|
|
userAgent: 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36 OPR/106.0.0.0',
|
|
},
|
|
writable: true,
|
|
configurable: true,
|
|
});
|
|
|
|
const browser = detectBrowser();
|
|
expect(browser.name).toBe('Opera');
|
|
expect(browser.version).toBe('106');
|
|
});
|
|
|
|
it('should detect Samsung Internet', () => {
|
|
Object.defineProperty(global, 'navigator', {
|
|
value: {
|
|
userAgent: 'Mozilla/5.0 (Linux; Android 13; SM-S918B) AppleWebKit/537.36 (KHTML, like Gecko) SamsungBrowser/23.0 Chrome/115.0.0.0 Mobile Safari/537.36',
|
|
},
|
|
writable: true,
|
|
configurable: true,
|
|
});
|
|
|
|
const browser = detectBrowser();
|
|
expect(browser.name).toBe('Samsung Internet');
|
|
expect(browser.version).toBe('23');
|
|
});
|
|
|
|
it('should return Unknown for undefined navigator', () => {
|
|
Object.defineProperty(global, 'navigator', {
|
|
value: undefined,
|
|
writable: true,
|
|
configurable: true,
|
|
});
|
|
|
|
const browser = detectBrowser();
|
|
expect(browser.name).toBe('Unknown');
|
|
expect(browser.version).toBe('0');
|
|
});
|
|
});
|
|
|
|
describe('checkWebBluetoothSupport', () => {
|
|
it('should return supported for Chrome with Web Bluetooth', () => {
|
|
Object.defineProperty(global, 'navigator', {
|
|
value: {
|
|
userAgent: 'Mozilla/5.0 Chrome/120.0.0.0',
|
|
bluetooth: {},
|
|
},
|
|
writable: true,
|
|
configurable: true,
|
|
});
|
|
Object.defineProperty(global, 'window', {
|
|
value: { isSecureContext: true },
|
|
writable: true,
|
|
configurable: true,
|
|
});
|
|
|
|
const support = checkWebBluetoothSupport();
|
|
expect(support.supported).toBe(true);
|
|
expect(support.browserName).toBe('Chrome');
|
|
});
|
|
|
|
it('should return unsupported for Safari', () => {
|
|
Object.defineProperty(global, 'navigator', {
|
|
value: {
|
|
userAgent: 'Mozilla/5.0 (Macintosh; Intel Mac OS X) Safari/605.1.15 Version/17.0',
|
|
},
|
|
writable: true,
|
|
configurable: true,
|
|
});
|
|
Object.defineProperty(global, 'window', {
|
|
value: { isSecureContext: true },
|
|
writable: true,
|
|
configurable: true,
|
|
});
|
|
|
|
const support = checkWebBluetoothSupport();
|
|
expect(support.supported).toBe(false);
|
|
expect(support.browserName).toBe('Safari');
|
|
expect(support.reason).toBe('unsupported_browser');
|
|
});
|
|
|
|
it('should return unsupported for Firefox', () => {
|
|
Object.defineProperty(global, 'navigator', {
|
|
value: {
|
|
userAgent: 'Mozilla/5.0 Firefox/122.0',
|
|
},
|
|
writable: true,
|
|
configurable: true,
|
|
});
|
|
Object.defineProperty(global, 'window', {
|
|
value: { isSecureContext: true },
|
|
writable: true,
|
|
configurable: true,
|
|
});
|
|
|
|
const support = checkWebBluetoothSupport();
|
|
expect(support.supported).toBe(false);
|
|
expect(support.browserName).toBe('Firefox');
|
|
expect(support.reason).toBe('unsupported_browser');
|
|
});
|
|
|
|
it('should return unsupported for insecure context', () => {
|
|
Object.defineProperty(global, 'navigator', {
|
|
value: {
|
|
userAgent: 'Mozilla/5.0 Chrome/120.0.0.0',
|
|
bluetooth: {},
|
|
},
|
|
writable: true,
|
|
configurable: true,
|
|
});
|
|
Object.defineProperty(global, 'window', {
|
|
value: { isSecureContext: false },
|
|
writable: true,
|
|
configurable: true,
|
|
});
|
|
|
|
const support = checkWebBluetoothSupport();
|
|
expect(support.supported).toBe(false);
|
|
expect(support.reason).toBe('insecure_context');
|
|
});
|
|
|
|
it('should return api_unavailable for Chrome without bluetooth API', () => {
|
|
Object.defineProperty(global, 'navigator', {
|
|
value: {
|
|
userAgent: 'Mozilla/5.0 Chrome/120.0.0.0',
|
|
// No bluetooth property
|
|
},
|
|
writable: true,
|
|
configurable: true,
|
|
});
|
|
Object.defineProperty(global, 'window', {
|
|
value: { isSecureContext: true },
|
|
writable: true,
|
|
configurable: true,
|
|
});
|
|
|
|
const support = checkWebBluetoothSupport();
|
|
expect(support.supported).toBe(false);
|
|
expect(support.reason).toBe('api_unavailable');
|
|
});
|
|
});
|
|
|
|
describe('getUnsupportedBrowserMessage', () => {
|
|
it('should return correct message for unsupported browser', () => {
|
|
const support = {
|
|
supported: false,
|
|
browserName: 'Safari',
|
|
browserVersion: '17',
|
|
reason: 'unsupported_browser' as const,
|
|
};
|
|
|
|
const message = getUnsupportedBrowserMessage(support);
|
|
expect(message.title).toBe('Browser Not Supported');
|
|
expect(message.message).toContain('Safari');
|
|
expect(message.suggestion).toContain('Chrome');
|
|
});
|
|
|
|
it('should return correct message for insecure context', () => {
|
|
const support = {
|
|
supported: false,
|
|
browserName: 'Chrome',
|
|
browserVersion: '120',
|
|
reason: 'insecure_context' as const,
|
|
};
|
|
|
|
const message = getUnsupportedBrowserMessage(support);
|
|
expect(message.title).toBe('Secure Connection Required');
|
|
expect(message.message).toContain('HTTPS');
|
|
});
|
|
|
|
it('should return correct message for api unavailable', () => {
|
|
const support = {
|
|
supported: false,
|
|
browserName: 'Chrome',
|
|
browserVersion: '120',
|
|
reason: 'api_unavailable' as const,
|
|
};
|
|
|
|
const message = getUnsupportedBrowserMessage(support);
|
|
expect(message.title).toBe('Bluetooth Not Available');
|
|
});
|
|
});
|
|
|
|
describe('isWebPlatform', () => {
|
|
it('should return true when window and document exist', () => {
|
|
Object.defineProperty(global, 'window', {
|
|
value: {},
|
|
writable: true,
|
|
configurable: true,
|
|
});
|
|
Object.defineProperty(global, 'document', {
|
|
value: {},
|
|
writable: true,
|
|
configurable: true,
|
|
});
|
|
|
|
expect(isWebPlatform()).toBe(true);
|
|
});
|
|
|
|
it('should return false when window is undefined', () => {
|
|
Object.defineProperty(global, 'window', {
|
|
value: undefined,
|
|
writable: true,
|
|
configurable: true,
|
|
});
|
|
|
|
expect(isWebPlatform()).toBe(false);
|
|
});
|
|
});
|
|
|
|
describe('hasWebBluetooth', () => {
|
|
it('should return true when navigator.bluetooth exists', () => {
|
|
Object.defineProperty(global, 'navigator', {
|
|
value: { bluetooth: {} },
|
|
writable: true,
|
|
configurable: true,
|
|
});
|
|
|
|
expect(hasWebBluetooth()).toBe(true);
|
|
});
|
|
|
|
it('should return false when navigator.bluetooth does not exist', () => {
|
|
Object.defineProperty(global, 'navigator', {
|
|
value: {},
|
|
writable: true,
|
|
configurable: true,
|
|
});
|
|
|
|
expect(hasWebBluetooth()).toBe(false);
|
|
});
|
|
});
|
|
|
|
describe('constants', () => {
|
|
it('should have supported browsers list', () => {
|
|
expect(SUPPORTED_BROWSERS).toContain('Chrome');
|
|
expect(SUPPORTED_BROWSERS).toContain('Edge');
|
|
expect(SUPPORTED_BROWSERS).toContain('Opera');
|
|
expect(SUPPORTED_BROWSERS).not.toContain('Safari');
|
|
expect(SUPPORTED_BROWSERS).not.toContain('Firefox');
|
|
});
|
|
|
|
it('should have browser help URLs', () => {
|
|
expect(BROWSER_HELP_URLS.Chrome).toContain('google.com/chrome');
|
|
expect(BROWSER_HELP_URLS.Edge).toContain('microsoft.com');
|
|
expect(BROWSER_HELP_URLS.Opera).toContain('opera.com');
|
|
});
|
|
});
|
|
});
|