Skip to content

feat(clerk-js): Additional vitest specs #5716

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

Merged
merged 14 commits into from
Jun 11, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .changeset/chatty-wombats-rest.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
---
---
8 changes: 8 additions & 0 deletions eslint.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -369,6 +369,14 @@ export default tseslint.config([
'custom-rules/no-navigate-useClerk': 'error',
},
},
{
name: 'packages/clerk-js - vitest',
files: ['packages/clerk-js/src/**/*.spec.{ts,tsx}'],
rules: {
'jest/unbound-method': 'off',
'@typescript-eslint/unbound-method': 'off',
},
},
{
name: 'packages/expo-passkeys',
files: ['packages/expo-passkeys/src/**/*'],
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,7 @@
"@types/react": "catalog:react",
"@types/react-dom": "catalog:react",
"@vitejs/plugin-react": "^4.5.1",
"@vitest/coverage-v8": "3.0.2",
"@vitest/coverage-v8": "3.0.5",
"chalk": "4.1.2",
"citty": "^0.1.6",
"conventional-changelog-conventionalcommits": "^4.6.3",
Expand Down
3 changes: 3 additions & 0 deletions packages/clerk-js/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@
"test:cache:clear": "jest --clearCache --useStderr",
"test:ci": "jest --maxWorkers=70%",
"test:coverage": "jest --collectCoverage && open coverage/lcov-report/index.html",
"test:jest": "jest",
"test:vitest": "vitest",
"watch": "rspack build --config rspack.config.js --env production --watch"
},
Expand Down Expand Up @@ -80,6 +81,7 @@
"swr": "2.3.3"
},
"devDependencies": {
"@emotion/jest": "^11.13.0",
"@rsdoctor/rspack-plugin": "^0.4.13",
"@rspack/cli": "^1.2.8",
"@rspack/core": "^1.2.8",
Expand All @@ -88,6 +90,7 @@
"@swc/jest": "^0.2.38",
"@types/cloudflare-turnstile": "^0.2.2",
"@types/webpack-env": "^1.18.8",
"jsdom": "^24.1.1",
"webpack-merge": "^5.10.0"
},
"peerDependencies": {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
/**
* @jest-environment node
* @vitest-environment node
*/

import { describe, expect, it } from 'vitest';

describe('clerk/headless', () => {
it('JS-689: should not error when loading headless', () => {
expect(() => {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,29 +1,31 @@
import { afterAll, afterEach, beforeEach, describe, expect, it, vi } from 'vitest';

import type { DevBrowser } from '../auth/devBrowser';
import { Clerk } from '../clerk';
import type { DisplayConfig } from '../resources/internal';
import { Client, Environment } from '../resources/internal';

const mockClientFetch = jest.fn();
const mockEnvironmentFetch = jest.fn();
const mockClientFetch = vi.fn();
const mockEnvironmentFetch = vi.fn();

jest.mock('../resources/Client');
jest.mock('../resources/Environment');
vi.mock('../resources/Client');
vi.mock('../resources/Environment');

// Because Jest, don't ask me why...
jest.mock('../auth/devBrowser', () => ({
vi.mock('../auth/devBrowser', () => ({
createDevBrowser: (): DevBrowser => ({
clear: jest.fn(),
setup: jest.fn(),
getDevBrowserJWT: jest.fn(() => 'deadbeef'),
setDevBrowserJWT: jest.fn(),
removeDevBrowserJWT: jest.fn(),
clear: vi.fn(),
setup: vi.fn(),
getDevBrowserJWT: vi.fn(() => 'deadbeef'),
setDevBrowserJWT: vi.fn(),
removeDevBrowserJWT: vi.fn(),
}),
}));

Client.getOrCreateInstance = jest.fn().mockImplementation(() => {
Client.getOrCreateInstance = vi.fn().mockImplementation(() => {
return { fetch: mockClientFetch };
});
Environment.getInstance = jest.fn().mockImplementation(() => {
Environment.getInstance = vi.fn().mockImplementation(() => {
return { fetch: mockEnvironmentFetch };
});

Expand Down Expand Up @@ -59,14 +61,14 @@ const developmentPublishableKey = 'pk_test_Y2xlcmsuYWJjZWYuMTIzNDUuZGV2LmxjbGNsZ
const productionPublishableKey = 'pk_live_Y2xlcmsuYWJjZWYuMTIzNDUucHJvZC5sY2xjbGVyay5jb20k';

describe('Clerk singleton - Redirects', () => {
const mockNavigate = jest.fn((to: string) => Promise.resolve(to));
const mockNavigate = vi.fn((to: string) => Promise.resolve(to));
const mockedLoadOptions = { routerPush: mockNavigate, routerReplace: mockNavigate };

let mockWindowLocation;
let mockHref: jest.Mock;
let mockHref: vi.Mock;

beforeEach(() => {
mockHref = jest.fn();
mockHref = vi.fn();
mockWindowLocation = {
host: 'test.host',
hostname: 'test.host',
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import type { InstanceType } from '@clerk/types';
import { afterAll, beforeAll, beforeEach, describe, expect, it, vi } from 'vitest';

import { SUPPORTED_FAPI_VERSION } from '../constants';
import { createFapiClient } from '../fapiClient';
Expand All @@ -24,11 +25,13 @@ type RecursivePartial<T> = {
[P in keyof T]?: RecursivePartial<T[P]>;
};

const originalFetch = global.fetch;

// @ts-ignore -- We don't need to fully satisfy the fetch types for the sake of this mock
global.fetch = jest.fn(() =>
global.fetch = vi.fn(() =>
Promise.resolve<RecursivePartial<Response>>({
headers: {
get: jest.fn(() => 'sess_43'),
get: vi.fn(() => 'sess_43'),
},
json: () => Promise.resolve({ foo: 42 }),
}),
Expand All @@ -54,12 +57,13 @@ beforeAll(() => {
});

beforeEach(() => {
(global.fetch as jest.Mock).mockClear();
(global.fetch as vi.Mock).mockClear();
});

afterAll(() => {
window.location = oldWindowLocation;
delete window.Clerk;
global.fetch = originalFetch;
});

describe('buildUrl(options)', () => {
Expand Down Expand Up @@ -184,10 +188,10 @@ describe('request', () => {
});

it('returns array response as array', async () => {
(global.fetch as jest.Mock).mockResolvedValueOnce(
(global.fetch as vi.Mock).mockResolvedValueOnce(
Promise.resolve<RecursivePartial<Response>>({
headers: {
get: jest.fn(() => 'sess_43'),
get: vi.fn(() => 'sess_43'),
},
json: () => Promise.resolve([{ foo: 42 }]),
}),
Expand All @@ -201,7 +205,7 @@ describe('request', () => {
});

it('handles the empty body on 204 response, returning null', async () => {
(global.fetch as jest.Mock).mockResolvedValueOnce(
(global.fetch as vi.Mock).mockResolvedValueOnce(
Promise.resolve<RecursivePartial<Response>>({
status: 204,
json: () => {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
import type { TokenResource } from '@clerk/types';
import { afterAll, beforeAll, describe, expect, it, vi } from 'vitest';

import { Token } from '../resources/internal';
import { SessionTokenCache } from '../tokenCache';

// This is required since abstract TS methods are undefined in Jest
jest.mock('../resources/Base', () => {
vi.mock('../resources/Base', () => {
class BaseResource {}

return {
Expand All @@ -17,11 +18,11 @@ const jwt =

describe('MemoryTokenCache', () => {
beforeAll(() => {
jest.useFakeTimers();
vi.useFakeTimers();
});

afterAll(() => {
jest.useRealTimers();
vi.useRealTimers();
});

describe('clear()', () => {
Expand Down Expand Up @@ -87,7 +88,7 @@ describe('MemoryTokenCache', () => {
expect(isResolved).toBe(false);

// Wait tokenResolver to resolve
jest.advanceTimersByTime(100);
vi.advanceTimersByTime(100);
await tokenResolver;

// Cache is not empty, retrieve the resolved tokenResolver
Expand All @@ -98,7 +99,7 @@ describe('MemoryTokenCache', () => {
});

// Advance the timer to force the JWT expiration
jest.advanceTimersByTime(60 * 1000);
vi.advanceTimersByTime(60 * 1000);

// Cache is empty, tokenResolver has been removed due to JWT expiration
expect(cache.get(key)).toBeUndefined();
Expand All @@ -125,11 +126,11 @@ describe('MemoryTokenCache', () => {
expect(cache.get(key)).toMatchObject(key);

// 44s since token created
jest.advanceTimersByTime(45 * 1000);
vi.advanceTimersByTime(45 * 1000);
expect(cache.get(key)).toMatchObject(key);

// 46s since token created
jest.advanceTimersByTime(1 * 1000);
vi.advanceTimersByTime(1 * 1000);
expect(cache.get(key)).toBeUndefined();
});

Expand All @@ -150,15 +151,15 @@ describe('MemoryTokenCache', () => {
expect(cache.get(key)).toMatchObject(key);

// 45s since token created
jest.advanceTimersByTime(45 * 1000);
vi.advanceTimersByTime(45 * 1000);
expect(cache.get(key, 0)).toMatchObject(key);

// 54s since token created
jest.advanceTimersByTime(9 * 1000);
vi.advanceTimersByTime(9 * 1000);
expect(cache.get(key, 0)).toMatchObject(key);

// 55s since token created
jest.advanceTimersByTime(1 * 1000);
vi.advanceTimersByTime(1 * 1000);
expect(cache.get(key, 0)).toBeUndefined();
});
});
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
jest.mock('@clerk/shared/keys', () => {
return { getCookieSuffix: jest.fn() };
import { afterEach, beforeEach, describe, expect, test, vi } from 'vitest';

vi.mock('@clerk/shared/keys', () => {
return { getCookieSuffix: vi.fn() };
});
jest.mock('@clerk/shared/logger', () => {
return { logger: { logOnce: jest.fn() } };
vi.mock('@clerk/shared/logger', () => {
return { logger: { logOnce: vi.fn() } };
});
import { getCookieSuffix as getSharedCookieSuffix } from '@clerk/shared/keys';
import { logger } from '@clerk/shared/logger';
Expand All @@ -11,12 +13,12 @@ import { getCookieSuffix } from '../cookieSuffix';

describe('getCookieSuffix', () => {
beforeEach(() => {
(getSharedCookieSuffix as jest.Mock).mockRejectedValue(new Error('mocked error for insecure context'));
(getSharedCookieSuffix as vi.Mock).mockRejectedValue(new Error('mocked error for insecure context'));
});

afterEach(() => {
(getSharedCookieSuffix as jest.Mock).mockReset();
(logger.logOnce as jest.Mock).mockReset();
(getSharedCookieSuffix as vi.Mock).mockReset();
(logger.logOnce as vi.Mock).mockReset();
});

describe('getCookieSuffix(publishableKey, subtle?)', () => {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';

import type { FapiClient } from '../../fapiClient';
import { createDevBrowser } from '../devBrowser';

Expand All @@ -8,7 +10,7 @@ type RecursivePartial<T> = {
describe('Thrown errors', () => {
beforeEach(() => {
// @ts-ignore
global.fetch = jest.fn(() =>
global.fetch = vi.fn(() =>
Promise.resolve<RecursivePartial<Response>>({
ok: false,
json: () =>
Expand All @@ -29,17 +31,17 @@ describe('Thrown errors', () => {

afterEach(() => {
// @ts-ignore
global.fetch?.mockClear();
vi.mocked(global.fetch)?.mockClear();
});

// Note: The test runs without any initial or mocked values on __clerk_db_jwt cookies.
// It is expected to modify the test accordingly if cookies are mocked for future extra testing.
it('throws any FAPI errors during dev browser creation', async () => {
const mockCreateFapiClient = jest.fn().mockImplementation(() => {
const mockCreateFapiClient = vi.fn().mockImplementation(() => {
return {
buildUrl: jest.fn(() => 'https://white-koala-42.clerk.accounts.dev/dev_browser'),
onAfterResponse: jest.fn(),
onBeforeRequest: jest.fn(),
buildUrl: vi.fn(() => 'https://white-koala-42.clerk.accounts.dev/dev_browser'),
onAfterResponse: vi.fn(),
onBeforeRequest: vi.fn(),
};
});

Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import { beforeEach, describe, expect, it, vi } from 'vitest';

import type { getCookieDomain as _getCookieDomain } from '../getCookieDomain';

type CookieHandler = NonNullable<Parameters<typeof _getCookieDomain>[1]>;
Expand All @@ -6,7 +8,7 @@ describe('getCookieDomain', () => {
let getCookieDomain: typeof _getCookieDomain;
beforeEach(async () => {
// We're dynamically importing getCookieDomain here to reset the module-level cache
jest.resetModules();
vi.resetModules();
getCookieDomain = await import('../getCookieDomain').then(m => m.getCookieDomain);
});

Expand All @@ -20,14 +22,14 @@ describe('getCookieDomain', () => {
// assume that the Public Suffix List is correctly handled by the browser.
const hostname = 'app.fr.hosting.co.uk';
const handler: CookieHandler = {
get: jest
get: vi
.fn()
.mockReturnValueOnce(undefined)
.mockReturnValueOnce(undefined)
.mockReturnValueOnce(undefined)
.mockReturnValueOnce('1'),
set: jest.fn().mockReturnValue(undefined),
remove: jest.fn().mockReturnValue(undefined),
set: vi.fn().mockReturnValue(undefined),
remove: vi.fn().mockReturnValue(undefined),
};
const result = getCookieDomain(hostname, handler);
expect(result).toBe(hostname);
Expand All @@ -53,9 +55,9 @@ describe('getCookieDomain', () => {

it('returns undefined if the domain could not be determined', () => {
const handler: CookieHandler = {
get: jest.fn().mockReturnValue(undefined),
set: jest.fn().mockReturnValue(undefined),
remove: jest.fn().mockReturnValue(undefined),
get: vi.fn().mockReturnValue(undefined),
set: vi.fn().mockReturnValue(undefined),
remove: vi.fn().mockReturnValue(undefined),
};
const hostname = 'app.hello.co.uk';
const result = getCookieDomain(hostname, handler);
Expand All @@ -65,9 +67,9 @@ describe('getCookieDomain', () => {
it('uses cached value if there is one', () => {
const hostname = 'clerk.com';
const handler: CookieHandler = {
get: jest.fn().mockReturnValue('1'),
set: jest.fn().mockReturnValue(undefined),
remove: jest.fn().mockReturnValue(undefined),
get: vi.fn().mockReturnValue('1'),
set: vi.fn().mockReturnValue(undefined),
remove: vi.fn().mockReturnValue(undefined),
};
expect(getCookieDomain(hostname, handler)).toBe(hostname);
expect(getCookieDomain(hostname, handler)).toBe(hostname);
Expand Down
Loading
Loading