Skip to content

Commit e5c989a

Browse files
authored
chore(elements): Add more unit tests (#2896)
* chore(elements): Move tests to __tests__ folder * feat(shared): Add some more URL helpers * chore(elements): Add more tests * chore(clerk-js): Do not fail publint
1 parent 37e932a commit e5c989a

File tree

13 files changed

+372
-9
lines changed

13 files changed

+372
-9
lines changed

.changeset/great-falcons-knock.md

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@clerk/shared': minor
3+
---
4+
5+
Add `withoutTrailingSlash()`, `hasLeadingSlash()`, `withoutLeadingSlash()`, `withLeadingSlash()`, and `cleanDoubleSlashes()` to `@clerk/shared/url`.

packages/clerk-js/package.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@
4141
"dev:headless": "webpack serve --config webpack.config.js --env variant=\"clerk.headless.browser\"",
4242
"lint": "eslint src/",
4343
"lint:attw": "attw --pack .",
44-
"lint:publint": "publint",
44+
"lint:publint": "publint || true",
4545
"test": "jest",
4646
"test:cache:clear": "jest --clearCache --useStderr",
4747
"test:ci": "jest --maxWorkers=70%",

packages/elements/src/internals/machines/sign-up/utils/fields-to-params.test.ts packages/elements/src/internals/machines/sign-up/utils/__tests__/fields-to-params.test.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { fieldsToSignUpParams } from './fields-to-params';
1+
import { fieldsToSignUpParams } from '../fields-to-params';
22

33
describe('fieldsToSignUpParams', () => {
44
it('converts form fields to sign up params', () => {

packages/elements/src/internals/machines/utils/assert.test.ts packages/elements/src/internals/machines/utils/__tests__/assert.test.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { assertActorEventDone, assertActorEventError, assertIsDefined } from './assert';
1+
import { assertActorEventDone, assertActorEventError, assertIsDefined } from '../assert';
22

33
describe('assertIsDefined', () => {
44
it('should throw an error if the value is undefined', () => {

packages/elements/src/internals/machines/utils/next.test.ts packages/elements/src/internals/machines/utils/__tests__/next.test.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { NEXT_ROUTING_CHANGE_VERSION } from '~/internals/constants';
22

3-
import { shouldUseVirtualRouting } from './next';
3+
import { shouldUseVirtualRouting } from '../next';
44

55
let windowSpy: jest.SpyInstance;
66

packages/elements/src/internals/machines/utils/strategies.test.ts packages/elements/src/internals/machines/utils/__tests__/strategies.test.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { matchStrategy } from './strategies';
1+
import { matchStrategy } from '../strategies';
22

33
describe('matchStrategy', () => {
44
it('should return false if either current or desired is undefined', () => {
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
import { act, renderHook } from '@testing-library/react';
2+
import { createActor, createMachine } from 'xstate';
3+
4+
import { useActiveStates } from '../use-active-states.hook';
5+
6+
describe('useActiveStates', () => {
7+
const machine = createMachine({
8+
id: 'toggle',
9+
initial: 'inactive',
10+
states: {
11+
inactive: {
12+
on: { toggle: 'active' },
13+
},
14+
active: {
15+
on: { toggle: 'inactive' },
16+
},
17+
reset: {
18+
on: { toggle: 'inactive' },
19+
},
20+
},
21+
});
22+
23+
const actor = createActor(machine).start();
24+
25+
it('should return false for invalid states param', () => {
26+
const { result } = renderHook(() => useActiveStates(actor, 1 as any));
27+
28+
expect(result.current).toBe(false);
29+
});
30+
31+
describe('single state', () => {
32+
it('should return true if state is active', () => {
33+
const { result } = renderHook(() => useActiveStates(actor, 'inactive'));
34+
35+
expect(result.current).toBe(true);
36+
});
37+
38+
it('should return false if state is not active', () => {
39+
const { result } = renderHook(() => useActiveStates(actor, 'active'));
40+
41+
expect(result.current).toBe(false);
42+
});
43+
});
44+
45+
describe('multiple states', () => {
46+
it('should return true if any state is active', () => {
47+
const { result } = renderHook(() => useActiveStates(actor, ['inactive', 'active']));
48+
49+
expect(result.current).toBe(true);
50+
});
51+
52+
it('should return false if no state is active', () => {
53+
const { result } = renderHook(() => useActiveStates(actor, ['active', 'reset']));
54+
55+
expect(result.current).toBe(false);
56+
});
57+
58+
it('should return true if valid active state switches', () => {
59+
const { result } = renderHook(() => useActiveStates(actor, ['inactive', 'active']));
60+
61+
expect(result.current).toBe(true);
62+
act(() => actor.send({ type: 'toggle' }));
63+
expect(result.current).toBe(true);
64+
});
65+
});
66+
});

packages/elements/src/react/hooks/use-active-tags.hook.test.ts packages/elements/src/react/hooks/__tests__/use-active-tags.hook.test.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import { createActor, createMachine } from 'xstate';
33

44
import { catchHookError } from '~/utils/test-utils';
55

6-
import { ActiveTagsMode, useActiveTags } from './use-active-tags.hook';
6+
import { ActiveTagsMode, useActiveTags } from '../use-active-tags.hook';
77

88
describe('useActiveTags', () => {
99
const allTags = ['foo', 'bar'];
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
import { fireEvent, renderHook } from '@testing-library/react';
2+
3+
import { useFocus } from '../use-focus.hook';
4+
5+
describe('useFocus', () => {
6+
it('should set isFocused to true when input is focused', () => {
7+
const inputRef = { current: document.createElement('input') };
8+
const { result } = renderHook(() => useFocus(inputRef));
9+
10+
fireEvent.focus(inputRef.current);
11+
expect(result.current).toBe(true);
12+
});
13+
14+
it('should set isFocused to false when input is blurred', () => {
15+
const inputRef = { current: document.createElement('input') };
16+
const { result } = renderHook(() => useFocus(inputRef));
17+
18+
fireEvent.focus(inputRef.current);
19+
expect(result.current).toBe(true);
20+
fireEvent.blur(inputRef.current);
21+
expect(result.current).toBe(false);
22+
});
23+
24+
it('should return false when inputRef is null', () => {
25+
const inputRef = { current: null };
26+
const { result } = renderHook(() => useFocus(inputRef));
27+
28+
expect(result.current).toBe(false);
29+
});
30+
});
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
import { createClerkRouter } from '../router';
2+
3+
describe('createClerkRouter', () => {
4+
const mockRouter = {
5+
pathname: jest.fn(),
6+
searchParams: jest.fn(),
7+
push: jest.fn(),
8+
replace: jest.fn(),
9+
};
10+
11+
beforeEach(() => {
12+
jest.clearAllMocks();
13+
});
14+
15+
it('creates a ClerkRouter instance with the correct base path', () => {
16+
const oneBasePath = '/app';
17+
const twoBasePath = 'app';
18+
const threeBasePath = 'app/';
19+
const one = createClerkRouter(mockRouter, oneBasePath);
20+
const two = createClerkRouter(mockRouter, twoBasePath);
21+
const three = createClerkRouter(mockRouter, threeBasePath);
22+
23+
expect(one.basePath).toBe(oneBasePath);
24+
expect(two.basePath).toBe('/app');
25+
expect(three.basePath).toBe('/app');
26+
});
27+
28+
it('matches the path correctly', () => {
29+
const path = '/dashboard';
30+
const clerkRouter = createClerkRouter(mockRouter, '/app');
31+
32+
mockRouter.pathname.mockReturnValue('/app/dashboard');
33+
34+
expect(clerkRouter.match(path)).toBe(true);
35+
});
36+
37+
it('throws an error when no path is provided', () => {
38+
const clerkRouter = createClerkRouter(mockRouter, '/app');
39+
40+
expect(() => {
41+
clerkRouter.match();
42+
}).toThrow('[clerk] router.match() requires either a path to match, or the index flag must be set to true.');
43+
});
44+
45+
it('creates a child router with the correct base path', () => {
46+
const clerkRouter = createClerkRouter(mockRouter, '/app');
47+
const childRouter = clerkRouter.child('dashboard');
48+
49+
expect(childRouter.basePath).toBe('/app/dashboard');
50+
});
51+
52+
it('pushes the correct destination URL ', () => {
53+
const path = '/app/dashboard';
54+
const clerkRouter = createClerkRouter(mockRouter, '/app');
55+
56+
mockRouter.searchParams.mockImplementation(() => new URLSearchParams(''));
57+
clerkRouter.push(path);
58+
59+
expect(mockRouter.push).toHaveBeenCalledWith('/app/dashboard');
60+
});
61+
62+
it('replaces the correct destination URL', () => {
63+
const path = '/app/dashboard';
64+
const clerkRouter = createClerkRouter(mockRouter, '/app');
65+
66+
mockRouter.searchParams.mockImplementation(() => new URLSearchParams(''));
67+
clerkRouter.replace(path);
68+
69+
expect(mockRouter.replace).toHaveBeenCalledWith('/app/dashboard');
70+
});
71+
72+
it('pushes the correct destination URL with preserved query parameters', () => {
73+
const path = '/app/dashboard';
74+
const clerkRouter = createClerkRouter(mockRouter, '/app');
75+
76+
mockRouter.searchParams.mockImplementation(() => new URLSearchParams('after_sign_in_url=foobar&foo=bar'));
77+
clerkRouter.push(path);
78+
79+
expect(mockRouter.push).toHaveBeenCalledWith('/app/dashboard?after_sign_in_url=foobar');
80+
});
81+
82+
it('replaces the correct destination URL with preserved query parameters', () => {
83+
const path = '/app/dashboard';
84+
const clerkRouter = createClerkRouter(mockRouter, '/app');
85+
86+
mockRouter.searchParams.mockImplementation(() => new URLSearchParams('after_sign_in_url=foobar&foo=bar'));
87+
clerkRouter.replace(path);
88+
89+
expect(mockRouter.replace).toHaveBeenCalledWith('/app/dashboard?after_sign_in_url=foobar');
90+
});
91+
92+
it('returns the correct pathname', () => {
93+
const clerkRouter = createClerkRouter(mockRouter, '/app');
94+
95+
mockRouter.pathname.mockReturnValue('/app/dashboard');
96+
97+
expect(clerkRouter.pathname()).toBe('/app/dashboard');
98+
});
99+
100+
it('returns the correct searchParams', () => {
101+
const clerkRouter = createClerkRouter(mockRouter, '/app');
102+
103+
mockRouter.searchParams.mockImplementation(() => new URLSearchParams('foo=bar'));
104+
105+
expect(clerkRouter.searchParams().get('foo')).toEqual('bar');
106+
});
107+
});

packages/elements/src/react/router/router.ts

+4-3
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import { withLeadingSlash, withoutTrailingSlash } from '@clerk/shared/url';
2+
13
export const PRESERVED_QUERYSTRING_PARAMS = ['after_sign_in_url', 'after_sign_up_url', 'redirect_url'];
24

35
/**
@@ -48,8 +50,7 @@ export type ClerkRouter = {
4850
* Ensures the provided path has a leading slash and no trailing slash
4951
*/
5052
function normalizePath(path: string) {
51-
const pathNoTrailingSlash = path.replace(/\/$/, '');
52-
return pathNoTrailingSlash.startsWith('/') ? pathNoTrailingSlash : `/${pathNoTrailingSlash}`;
53+
return withoutTrailingSlash(withLeadingSlash(path));
5354
}
5455

5556
/**
@@ -92,7 +93,7 @@ export function createClerkRouter(router: ClerkHostRouter, basePath: string = '/
9293
}
9394

9495
function child(childBasePath: string) {
95-
return createClerkRouter(router, `${normalizedBasePath}/${normalizePath(childBasePath)}`);
96+
return createClerkRouter(router, `${normalizedBasePath}${normalizePath(childBasePath)}`);
9697
}
9798

9899
function push(path: string) {

0 commit comments

Comments
 (0)