Skip to content

fix: expose isSupportedBrowser() utility #1859

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 11 commits into
base: main
Choose a base branch
from
163 changes: 163 additions & 0 deletions packages/client/src/helpers/__tests__/browsers.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,163 @@
import { beforeEach, describe, expect, it, vi } from 'vitest';
import { isChrome, isFirefox, isSafari, isSupportedBrowser } from '../browsers';
import { getClientDetails } from '../client-details';
import { ClientDetails } from '../../gen/video/sfu/models/models';

describe('browsers', () => {
beforeEach(() => {
Object.defineProperty(globalThis, 'navigator', {
value: { userAgent: '' },
writable: true,
});
});

describe('isSafari', () => {
it('should return false if navigator is undefined', () => {
expect(isSafari()).toBe(false);
});

it('should return true for Safari user agent', () => {
// @ts-expect-error - mocking navigator
globalThis.navigator.userAgent =
'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/15.0 Safari/605.1.15';
expect(isSafari()).toBe(true);
});

it('should return false for Chrome user agent', () => {
// @ts-expect-error - mocking navigator
globalThis.navigator.userAgent =
'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36';
expect(isSafari()).toBe(false);
});
});

describe('isFirefox', () => {
it('should return false if navigator is undefined', () => {
expect(isFirefox()).toBe(false);
});

it('should return true for Firefox user agent', () => {
// @ts-expect-error - mocking navigator
globalThis.navigator.userAgent =
'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:89.0) Gecko/20100101 Firefox/89.0';
expect(isFirefox()).toBe(true);
});

it('should return false for Chrome user agent', () => {
// @ts-expect-error - mocking navigator
globalThis.navigator.userAgent =
'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36';
expect(isFirefox()).toBe(false);
});
});

describe('isChrome', () => {
it('should return false if navigator is undefined', () => {
expect(isChrome()).toBe(false);
});

it('should return true for Chrome user agent', () => {
// @ts-expect-error - mocking navigator
globalThis.navigator.userAgent =
'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36';
expect(isChrome()).toBe(true);
});

it('should return false for Firefox user agent', () => {
// @ts-expect-error - mocking navigator
globalThis.navigator.userAgent =
'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:89.0) Gecko/20100101 Firefox/89.0';
expect(isChrome()).toBe(false);
});
});

describe('isSupportedBrowser', () => {
vi.mock('../client-details', () => ({
getClientDetails: vi.fn(),
}));

it('should return false if browser is undefined', async () => {
vi.mocked(getClientDetails).mockResolvedValue({
browser: undefined,
} as ClientDetails);
expect(await isSupportedBrowser()).toBe(false);
});

it('should return true for supported Chrome version', async () => {
vi.mocked(getClientDetails).mockResolvedValue({
browser: { name: 'Chrome', version: '91' },
} as ClientDetails);
expect(await isSupportedBrowser()).toBe(true);
});

it('should return true for supported Chrome detailed version', async () => {
vi.mocked(getClientDetails).mockResolvedValue({
browser: { name: 'Chrome', version: '138.0.7204.158' },
} as ClientDetails);
expect(await isSupportedBrowser()).toBe(true);
});

it('should return false for unsupported Chrome version', async () => {
vi.mocked(getClientDetails).mockResolvedValue({
browser: { name: 'Chrome', version: '90' },
} as ClientDetails);
expect(await isSupportedBrowser()).toBe(false);
});

it('should return false for unsupported Chrome detailed version', async () => {
vi.mocked(getClientDetails).mockResolvedValue({
browser: { name: 'Chrome', version: '90.0.1234.99' },
} as ClientDetails);
expect(await isSupportedBrowser()).toBe(false);
});

it('should return true for supported Edge version', async () => {
vi.mocked(getClientDetails).mockResolvedValue({
browser: { name: 'Edge', version: '91' },
} as ClientDetails);
expect(await isSupportedBrowser()).toBe(true);
});

it('should return false for unsupported Edge version', async () => {
vi.mocked(getClientDetails).mockResolvedValue({
browser: { name: 'Edge', version: '90' },
} as ClientDetails);
expect(await isSupportedBrowser()).toBe(false);
});

it('should return true for supported Firefox version', async () => {
vi.mocked(getClientDetails).mockResolvedValue({
browser: { name: 'Firefox', version: '89' },
} as ClientDetails);
expect(await isSupportedBrowser()).toBe(true);
});

it('should return false for unsupported Firefox version', async () => {
vi.mocked(getClientDetails).mockResolvedValue({
browser: { name: 'Firefox', version: '88' },
} as ClientDetails);
expect(await isSupportedBrowser()).toBe(false);
});

it('should return true for supported Safari version', async () => {
vi.mocked(getClientDetails).mockResolvedValue({
browser: { name: 'Safari', version: '15' },
} as ClientDetails);
expect(await isSupportedBrowser()).toBe(true);
});

it('should return false for unsupported Safari version', async () => {
vi.mocked(getClientDetails).mockResolvedValue({
browser: { name: 'Safari', version: '14' },
} as ClientDetails);
expect(await isSupportedBrowser()).toBe(false);
});

it('should return false for unsupported browser', async () => {
vi.mocked(getClientDetails).mockResolvedValue({
browser: { name: 'Opera', version: '78' },
} as ClientDetails);
expect(await isSupportedBrowser()).toBe(false);
});
});
});
23 changes: 23 additions & 0 deletions packages/client/src/helpers/browsers.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import { getClientDetails } from './client-details';

/**
* Checks whether the current browser is Safari.
*/
Expand All @@ -21,3 +23,24 @@ export const isChrome = () => {
if (typeof navigator === 'undefined') return false;
return navigator.userAgent?.includes('Chrome');
};

/**
* Checks whether the current browser is among the list of first-class supported browsers.
* This includes Chrome, Edge, Firefox, and Safari.
*
* Although the Stream Video SDK may work in other browsers, these are the ones we officially support.
*/
export const isSupportedBrowser = async (): Promise<boolean> => {
const { browser } = await getClientDetails();
if (!browser) return false; // we aren't running in a browser

const name = browser.name.toLowerCase();
const [major] = browser.version.split('.');
const version = parseInt(major, 10);
return (
(name.includes('chrome') && version >= 91) ||
(name.includes('edge') && version >= 91) ||
(name.includes('firefox') && version >= 89) ||
(name.includes('safari') && version >= 15)
);
};
15 changes: 12 additions & 3 deletions packages/client/src/helpers/client-details.ts
Original file line number Diff line number Diff line change
Expand Up @@ -143,13 +143,18 @@ export const getClientDetails = async (): Promise<ClientDetails> => {
// @ts-expect-error - userAgentData is not yet in the TS types
const userAgentDataApi = navigator.userAgentData;
let userAgentData:
| { platform?: string; platformVersion?: string }
| {
platform?: string;
platformVersion?: string;
fullVersionList?: Array<{ brand: string; version: string }>;
}
| undefined;
if (userAgentDataApi && userAgentDataApi.getHighEntropyValues) {
try {
userAgentData = await userAgentDataApi.getHighEntropyValues([
'platform',
'platformVersion',
'fullVersionList',
]);
} catch {
// Ignore the error
Expand All @@ -158,11 +163,15 @@ export const getClientDetails = async (): Promise<ClientDetails> => {

const userAgent = new UAParser(navigator.userAgent);
const { browser, os, device, cpu } = userAgent.getResult();
const browserName = browser.name || navigator.userAgent;
const browserVersion =
userAgentData?.fullVersionList?.find((v) => v.brand.includes(browserName))
?.version ?? browser.version;
return {
sdk: sdkInfo,
browser: {
name: browser.name || navigator.userAgent,
version: browser.version || '',
name: browserName,
version: browserVersion || '',
},
os: {
name: userAgentData?.platform || os.name || '',
Expand Down
Loading