Skip to content

Commit 52f8553

Browse files
authored
chore(shared): Improve test coverage (#1925)
* chore(shared): Improve test coverage * Create grumpy-swans-taste.md * chore(shared): Improve test coverage
1 parent 92727ee commit 52f8553

10 files changed

+240
-3
lines changed

.changeset/grumpy-swans-taste.md

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@clerk/shared": patch
3+
---
4+
5+
Improve internal test coverage and fix small bug inside `callWithRetry`

packages/shared/src/__tests__/browser.test.ts

+58-1
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,66 @@
1-
import { inBrowser, isValidBrowserOnline, userAgentIsRobot } from '../browser';
1+
import { inBrowser, isValidBrowser, isValidBrowserOnline, userAgentIsRobot } from '../browser';
22

33
describe('inBrowser()', () => {
4+
afterEach(() => {
5+
jest.restoreAllMocks();
6+
});
7+
48
it('returns true if window is defined', () => {
59
expect(inBrowser()).toBe(true);
610
});
11+
it('returns false if window is undefined', () => {
12+
const windowSpy = jest.spyOn(global, 'window', 'get');
13+
// @ts-ignore - Test
14+
windowSpy.mockReturnValue(undefined);
15+
expect(inBrowser()).toBe(false);
16+
});
17+
});
18+
19+
describe('isValidBrowser', () => {
20+
let userAgentGetter: any;
21+
let webdriverGetter: any;
22+
23+
beforeEach(() => {
24+
userAgentGetter = jest.spyOn(window.navigator, 'userAgent', 'get');
25+
webdriverGetter = jest.spyOn(window.navigator, 'webdriver', 'get');
26+
});
27+
28+
afterEach(() => {
29+
jest.restoreAllMocks();
30+
});
31+
32+
it('returns false if not in browser', () => {
33+
const windowSpy = jest.spyOn(global, 'window', 'get');
34+
// @ts-ignore - Test
35+
windowSpy.mockReturnValue(undefined);
36+
37+
expect(isValidBrowser()).toBe(false);
38+
});
39+
40+
it('returns true if in browser, navigator is not a bot, and webdriver is not enabled', () => {
41+
userAgentGetter.mockReturnValue(
42+
'Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:109.0) Gecko/20100101 Firefox/109.0',
43+
);
44+
webdriverGetter.mockReturnValue(false);
45+
46+
expect(isValidBrowser()).toBe(true);
47+
});
48+
49+
it('returns false if navigator is a bot', () => {
50+
userAgentGetter.mockReturnValue('msnbot-NewsBlogs/2.0b (+http://search.msn.com/msnbot.htm)');
51+
webdriverGetter.mockReturnValue(false);
52+
53+
expect(isValidBrowser()).toBe(false);
54+
});
55+
56+
it('returns false if webdriver is enabled', () => {
57+
userAgentGetter.mockReturnValue(
58+
'Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:109.0) Gecko/20100101 Firefox/109.0',
59+
);
60+
webdriverGetter.mockReturnValue(true);
61+
62+
expect(isValidBrowser()).toBe(false);
63+
});
764
});
865

966
describe('detectUserAgentRobot', () => {
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
import { callWithRetry } from '../callWithRetry';
2+
3+
describe('callWithRetry', () => {
4+
test('should return the result of the function if it succeeds', async () => {
5+
const fn = jest.fn().mockResolvedValue('result');
6+
const result = await callWithRetry(fn);
7+
expect(result).toBe('result');
8+
expect(fn).toHaveBeenCalledTimes(1);
9+
});
10+
11+
test('should retry the function if it fails', async () => {
12+
const fn = jest.fn().mockRejectedValueOnce(new Error('error')).mockResolvedValueOnce('result');
13+
const result = await callWithRetry(fn, 1, 2);
14+
expect(result).toBe('result');
15+
expect(fn).toHaveBeenCalledTimes(2);
16+
});
17+
18+
test('should throw an error if the function fails too many times', async () => {
19+
const fn = jest.fn().mockRejectedValue(new Error('error'));
20+
await expect(callWithRetry(fn, 1, 2)).rejects.toThrow('error');
21+
expect(fn).toHaveBeenCalledTimes(2);
22+
});
23+
});

packages/shared/src/__tests__/keys.test.ts

+30
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
import {
22
buildPublishableKey,
33
createDevOrStagingUrlCache,
4+
isDevelopmentFromApiKey,
45
isLegacyFrontendApiKey,
6+
isProductionFromApiKey,
57
isPublishableKey,
68
parsePublishableKey,
79
} from '../keys';
@@ -90,3 +92,31 @@ describe('isDevOrStagingUrl(url)', () => {
9092
expect(isDevOrStagingUrl(a)).toBe(expected);
9193
});
9294
});
95+
96+
describe('isDevelopmentFromApiKey(key)', () => {
97+
const cases: Array<[string, boolean]> = [
98+
['sk_live_Y2xlcmsuY2xlcmsuZGV2JA==', false],
99+
['sk_test_Y2xlcmsuY2xlcmsuZGV2JA==', true],
100+
['live_Y2xlcmsuY2xlcmsuZGV2JA==', false],
101+
['test_Y2xlcmsuY2xlcmsuZGV2JA==', true],
102+
];
103+
104+
test.each(cases)('given %p as a publishable key string, returns %p', (publishableKeyStr, expected) => {
105+
const result = isDevelopmentFromApiKey(publishableKeyStr);
106+
expect(result).toEqual(expected);
107+
});
108+
});
109+
110+
describe('isProductionFromApiKey(key)', () => {
111+
const cases: Array<[string, boolean]> = [
112+
['sk_live_Y2xlcmsuY2xlcmsuZGV2JA==', true],
113+
['sk_test_Y2xlcmsuY2xlcmsuZGV2JA==', false],
114+
['live_Y2xlcmsuY2xlcmsuZGV2JA==', true],
115+
['test_Y2xlcmsuY2xlcmsuZGV2JA==', false],
116+
];
117+
118+
test.each(cases)('given %p as a publishable key string, returns %p', (publishableKeyStr, expected) => {
119+
const result = isProductionFromApiKey(publishableKeyStr);
120+
expect(result).toEqual(expected);
121+
});
122+
});

packages/shared/src/__tests__/url.test.ts

+57-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { addClerkPrefix, parseSearchParams, stripScheme } from '../url';
1+
import { addClerkPrefix, getClerkJsMajorVersionOrTag, getScriptUrl, parseSearchParams, stripScheme } from '../url';
22

33
describe('parseSearchParams(queryString)', () => {
44
it('parses query string and returns a URLSearchParams object', () => {
@@ -56,3 +56,59 @@ describe('addClerkPrefix(str)', () => {
5656
expect(addClerkPrefix(urlInput)).toBe(urlOutput);
5757
});
5858
});
59+
60+
describe('getClerkJsMajorVersionOrTag', () => {
61+
const stagingFrontendApi = 'foobar.lclstage.dev';
62+
63+
it('returns staging if pkgVersion is not provided and frontendApi is staging', () => {
64+
expect(getClerkJsMajorVersionOrTag(stagingFrontendApi)).toBe('staging');
65+
});
66+
67+
it('returns latest if pkgVersion is not provided and frontendApi is not staging', () => {
68+
expect(getClerkJsMajorVersionOrTag('foobar.dev')).toBe('latest');
69+
});
70+
71+
it('returns next if pkgVersion contains next', () => {
72+
expect(getClerkJsMajorVersionOrTag('foobar.dev', '1.2.3-next.4')).toBe('next');
73+
});
74+
75+
it('returns the major version if pkgVersion is provided', () => {
76+
expect(getClerkJsMajorVersionOrTag('foobar.dev', '1.2.3')).toBe('1');
77+
});
78+
79+
it('returns latest if pkgVersion is empty string', () => {
80+
expect(getClerkJsMajorVersionOrTag('foobar.dev', '')).toBe('latest');
81+
});
82+
});
83+
84+
describe('getScriptUrl', () => {
85+
const frontendApi = 'https://foobar.dev';
86+
87+
it('returns URL using the clerkJSVersion if provided', () => {
88+
expect(getScriptUrl(frontendApi, { clerkJSVersion: '1.2.3' })).toBe(
89+
'https://foobar.dev/npm/@clerk/clerk-js@1.2.3/dist/clerk.browser.js',
90+
);
91+
});
92+
93+
it('returns URL using the latest version if clerkJSVersion & pkgVersion is not provided + frontendApi is not staging', () => {
94+
expect(getScriptUrl(frontendApi, {})).toBe('https://foobar.dev/npm/@clerk/clerk-js@latest/dist/clerk.browser.js');
95+
});
96+
97+
it('returns URL using the major version if only pkgVersion is provided', () => {
98+
expect(getScriptUrl(frontendApi, { pkgVersion: '1.2.3' })).toBe(
99+
'https://foobar.dev/npm/@clerk/clerk-js@1/dist/clerk.browser.js',
100+
);
101+
});
102+
103+
it('returns URL using the major version if only pkgVersion contains next', () => {
104+
expect(getScriptUrl(frontendApi, { pkgVersion: '1.2.3-next.4' })).toBe(
105+
'https://foobar.dev/npm/@clerk/clerk-js@next/dist/clerk.browser.js',
106+
);
107+
});
108+
109+
it('returns URL using the staging tag if frontendApi is staging', () => {
110+
expect(getScriptUrl('https://foobar.lclstage.dev', {})).toBe(
111+
'https://foobar.lclstage.dev/npm/@clerk/clerk-js@staging/dist/clerk.browser.js',
112+
);
113+
});
114+
});

packages/shared/src/callWithRetry.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,6 @@ export async function callWithRetry<T>(
2323
}
2424
await wait(2 ** attempt * 100);
2525

26-
return callWithRetry(fn, attempt + 1);
26+
return callWithRetry(fn, attempt + 1, maxAttempts);
2727
}
2828
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
import { createDeferredPromise } from '../createDeferredPromise';
2+
3+
describe('createDeferredPromise', () => {
4+
test('resolves with correct value', async () => {
5+
const { promise, resolve } = createDeferredPromise();
6+
const expectedValue = 'hello world';
7+
resolve(expectedValue);
8+
const result = await promise;
9+
expect(result).toBe(expectedValue);
10+
});
11+
12+
test('rejects with correct error', async () => {
13+
const { promise, reject } = createDeferredPromise();
14+
const expectedError = new Error('something went wrong');
15+
reject(expectedError);
16+
try {
17+
await promise;
18+
} catch (error) {
19+
expect(error).toBe(expectedError);
20+
}
21+
});
22+
});
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
import { isStaging } from '../instance';
2+
3+
describe('isStaging', () => {
4+
it.each([
5+
['clerk', false],
6+
['clerk.com', false],
7+
['whatever.com', false],
8+
['clerk.abcef', false],
9+
['clerk.abcef.12345', false],
10+
['clerk.abcef.12345.lcl', false],
11+
['clerk.abcef.12345.lcl.dev', false],
12+
['clerk.abcef.12345.stg.dev', false],
13+
['clerk.abcef.12345.lclstage.dev', true],
14+
['clerk.abcef.12345.stgstage.dev', true],
15+
['clerk.abcef.12345.clerkstage.dev', true],
16+
['clerk.abcef.12345.accountsstage.dev', true],
17+
])('validates the frontendApi format', (str, expected) => {
18+
expect(isStaging(str)).toBe(expected);
19+
});
20+
});
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
import { runWithExponentialBackOff } from '../runWithExponentialBackOff';
2+
3+
describe('runWithExponentialBackOff', () => {
4+
test('resolves with the result of the callback', async () => {
5+
const result = await runWithExponentialBackOff(() => Promise.resolve('success'));
6+
expect(result).toBe('success');
7+
});
8+
9+
test('retries the callback until it succeeds', async () => {
10+
let attempts = 0;
11+
const result = await runWithExponentialBackOff(() => {
12+
attempts++;
13+
if (attempts < 3) {
14+
throw new Error('failed');
15+
}
16+
return Promise.resolve('success');
17+
});
18+
expect(result).toBe('success');
19+
expect(attempts).toBe(3);
20+
});
21+
});

packages/shared/src/utils/instance.ts

+3
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,6 @@
1+
/**
2+
* Check if the frontendApi ends with a staging domain
3+
*/
14
export function isStaging(frontendApi: string): boolean {
25
return (
36
frontendApi.endsWith('.lclstage.dev') ||

0 commit comments

Comments
 (0)