diff --git a/.changeset/chatty-wombats-rest.md b/.changeset/chatty-wombats-rest.md new file mode 100644 index 00000000000..a845151cc84 --- /dev/null +++ b/.changeset/chatty-wombats-rest.md @@ -0,0 +1,2 @@ +--- +--- diff --git a/eslint.config.mjs b/eslint.config.mjs index 079990b1443..83e6cc3b04a 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -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/**/*'], diff --git a/package.json b/package.json index c484b2a8a5b..4d60d2d2302 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/packages/clerk-js/package.json b/packages/clerk-js/package.json index 40a680bd5ff..36c01bbcb9c 100644 --- a/packages/clerk-js/package.json +++ b/packages/clerk-js/package.json @@ -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" }, @@ -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", @@ -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": { diff --git a/packages/clerk-js/src/__tests__/headless.test.ts b/packages/clerk-js/src/__tests__/headless.spec.ts similarity index 78% rename from packages/clerk-js/src/__tests__/headless.test.ts rename to packages/clerk-js/src/__tests__/headless.spec.ts index 949b6e30805..8ea5196a2a9 100644 --- a/packages/clerk-js/src/__tests__/headless.test.ts +++ b/packages/clerk-js/src/__tests__/headless.spec.ts @@ -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(() => { diff --git a/packages/clerk-js/src/core/__tests__/clerk.redirects.test.ts b/packages/clerk-js/src/core/__tests__/clerk.redirects.spec.ts similarity index 94% rename from packages/clerk-js/src/core/__tests__/clerk.redirects.test.ts rename to packages/clerk-js/src/core/__tests__/clerk.redirects.spec.ts index c744fffd27f..a7d45c04331 100644 --- a/packages/clerk-js/src/core/__tests__/clerk.redirects.test.ts +++ b/packages/clerk-js/src/core/__tests__/clerk.redirects.spec.ts @@ -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 }; }); @@ -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', diff --git a/packages/clerk-js/src/core/__tests__/fapiClient.test.ts b/packages/clerk-js/src/core/__tests__/fapiClient.spec.ts similarity index 94% rename from packages/clerk-js/src/core/__tests__/fapiClient.test.ts rename to packages/clerk-js/src/core/__tests__/fapiClient.spec.ts index 5e4ef4723f4..728b259fe94 100644 --- a/packages/clerk-js/src/core/__tests__/fapiClient.test.ts +++ b/packages/clerk-js/src/core/__tests__/fapiClient.spec.ts @@ -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'; @@ -24,11 +25,13 @@ type RecursivePartial = { [P in keyof T]?: RecursivePartial; }; +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>({ headers: { - get: jest.fn(() => 'sess_43'), + get: vi.fn(() => 'sess_43'), }, json: () => Promise.resolve({ foo: 42 }), }), @@ -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)', () => { @@ -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>({ headers: { - get: jest.fn(() => 'sess_43'), + get: vi.fn(() => 'sess_43'), }, json: () => Promise.resolve([{ foo: 42 }]), }), @@ -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>({ status: 204, json: () => { diff --git a/packages/clerk-js/src/core/__tests__/tokenCache.test.ts b/packages/clerk-js/src/core/__tests__/tokenCache.spec.ts similarity index 90% rename from packages/clerk-js/src/core/__tests__/tokenCache.test.ts rename to packages/clerk-js/src/core/__tests__/tokenCache.spec.ts index 6dcf3833ab4..cca0d8e84a1 100644 --- a/packages/clerk-js/src/core/__tests__/tokenCache.test.ts +++ b/packages/clerk-js/src/core/__tests__/tokenCache.spec.ts @@ -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 { @@ -17,11 +18,11 @@ const jwt = describe('MemoryTokenCache', () => { beforeAll(() => { - jest.useFakeTimers(); + vi.useFakeTimers(); }); afterAll(() => { - jest.useRealTimers(); + vi.useRealTimers(); }); describe('clear()', () => { @@ -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 @@ -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(); @@ -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(); }); @@ -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(); }); }); diff --git a/packages/clerk-js/src/core/auth/__tests__/cookieSuffix.test.ts b/packages/clerk-js/src/core/auth/__tests__/cookieSuffix.spec.ts similarity index 72% rename from packages/clerk-js/src/core/auth/__tests__/cookieSuffix.test.ts rename to packages/clerk-js/src/core/auth/__tests__/cookieSuffix.spec.ts index 1adb90dfbbb..c50eda73114 100644 --- a/packages/clerk-js/src/core/auth/__tests__/cookieSuffix.test.ts +++ b/packages/clerk-js/src/core/auth/__tests__/cookieSuffix.spec.ts @@ -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'; @@ -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?)', () => { diff --git a/packages/clerk-js/src/core/auth/__tests__/devBrowser.test.ts b/packages/clerk-js/src/core/auth/__tests__/devBrowser.spec.ts similarity index 83% rename from packages/clerk-js/src/core/auth/__tests__/devBrowser.test.ts rename to packages/clerk-js/src/core/auth/__tests__/devBrowser.spec.ts index 787824c65e2..26fbe011864 100644 --- a/packages/clerk-js/src/core/auth/__tests__/devBrowser.test.ts +++ b/packages/clerk-js/src/core/auth/__tests__/devBrowser.spec.ts @@ -1,3 +1,5 @@ +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; + import type { FapiClient } from '../../fapiClient'; import { createDevBrowser } from '../devBrowser'; @@ -8,7 +10,7 @@ type RecursivePartial = { describe('Thrown errors', () => { beforeEach(() => { // @ts-ignore - global.fetch = jest.fn(() => + global.fetch = vi.fn(() => Promise.resolve>({ ok: false, json: () => @@ -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(), }; }); diff --git a/packages/clerk-js/src/core/auth/__tests__/getCookieDomain.test.ts b/packages/clerk-js/src/core/auth/__tests__/getCookieDomain.spec.ts similarity index 83% rename from packages/clerk-js/src/core/auth/__tests__/getCookieDomain.test.ts rename to packages/clerk-js/src/core/auth/__tests__/getCookieDomain.spec.ts index 5f2b335f22e..d12f504cfb3 100644 --- a/packages/clerk-js/src/core/auth/__tests__/getCookieDomain.test.ts +++ b/packages/clerk-js/src/core/auth/__tests__/getCookieDomain.spec.ts @@ -1,3 +1,5 @@ +import { beforeEach, describe, expect, it, vi } from 'vitest'; + import type { getCookieDomain as _getCookieDomain } from '../getCookieDomain'; type CookieHandler = NonNullable[1]>; @@ -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); }); @@ -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); @@ -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); @@ -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); diff --git a/packages/clerk-js/src/core/auth/__tests__/getSecureAttribute.test.ts b/packages/clerk-js/src/core/auth/__tests__/getSecureAttribute.spec.ts similarity index 95% rename from packages/clerk-js/src/core/auth/__tests__/getSecureAttribute.test.ts rename to packages/clerk-js/src/core/auth/__tests__/getSecureAttribute.spec.ts index 36b2b699de6..97f9b1eff0c 100644 --- a/packages/clerk-js/src/core/auth/__tests__/getSecureAttribute.test.ts +++ b/packages/clerk-js/src/core/auth/__tests__/getSecureAttribute.spec.ts @@ -1,10 +1,12 @@ +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; + import { getSecureAttribute } from '../getSecureAttribute'; describe('getSecureAttribute', () => { let windowSpy: any; beforeEach(() => { - windowSpy = jest.spyOn(window, 'window', 'get'); + windowSpy = vi.spyOn(window, 'window', 'get'); }); afterEach(() => { diff --git a/packages/clerk-js/src/core/fraudProtection.test.ts b/packages/clerk-js/src/core/fraudProtection.spec.ts similarity index 82% rename from packages/clerk-js/src/core/fraudProtection.test.ts rename to packages/clerk-js/src/core/fraudProtection.spec.ts index 98118289571..16139bdfccb 100644 --- a/packages/clerk-js/src/core/fraudProtection.test.ts +++ b/packages/clerk-js/src/core/fraudProtection.spec.ts @@ -1,3 +1,5 @@ +import { beforeEach, describe, expect, it, type Mock, vi } from 'vitest'; + import { FraudProtection } from './fraudProtection'; import type { Clerk, Client } from './resources/internal'; import { ClerkAPIResponseError } from './resources/internal'; @@ -7,7 +9,7 @@ describe('FraudProtectionService', () => { let mockClerk: Clerk; let mockClient: typeof Client; let solveCaptcha: any; - let mockManagedInModal: jest.Mock; + let mockManagedInModal: Mock; function MockCaptchaChallenge() { // @ts-ignore - we don't need to implement the entire class @@ -22,14 +24,14 @@ describe('FraudProtectionService', () => { }; beforeEach(() => { - mockManagedInModal = jest.fn().mockResolvedValue( + mockManagedInModal = vi.fn().mockResolvedValue( new Promise(r => { solveCaptcha = r; }), ); const mockClientInstance = { - __internal_sendCaptchaToken: jest.fn().mockResolvedValue({}), + __internal_sendCaptchaToken: vi.fn().mockResolvedValue({}), }; mockClient = { @@ -45,7 +47,7 @@ describe('FraudProtectionService', () => { }); it('does not handle requests that did not throw', async () => { - const fn1 = jest.fn().mockResolvedValue('result'); + const fn1 = vi.fn().mockResolvedValue('result'); const fn1res = sut.execute(mockClerk, fn1); @@ -64,17 +66,17 @@ describe('FraudProtectionService', () => { data: [{ code: 'no-idea' } as any], status: 401, }); - const fn1 = jest.fn().mockRejectedValueOnce(unrelatedError); + const fn1 = vi.fn().mockRejectedValueOnce(unrelatedError); const fn1res = sut.execute(mockClerk, fn1); - expect(fn1res).rejects.toEqual(unrelatedError); + await expect(fn1res).rejects.toEqual(unrelatedError); expect(mockManagedInModal).toHaveBeenCalledTimes(0); expect(mockClient.getOrCreateInstance().__internal_sendCaptchaToken).toHaveBeenCalledTimes(0); expect(fn1).toHaveBeenCalledTimes(1); }); it('handles parallel requests that began at the same time by handling any requests that returned requires_captcha', async () => { - const fn1 = jest.fn().mockRejectedValueOnce(createCaptchaError()).mockResolvedValueOnce('result1'); - const fn2 = jest.fn().mockResolvedValue('result2'); + const fn1 = vi.fn().mockRejectedValueOnce(createCaptchaError()).mockResolvedValueOnce('result1'); + const fn2 = vi.fn().mockResolvedValue('result2'); const fn1res = sut.execute(mockClerk, fn1); const fn2res = sut.execute(mockClerk, fn2); @@ -93,8 +95,8 @@ describe('FraudProtectionService', () => { }); it('handles parallel requests that returned 401 requires_captcha', async () => { - const fn1 = jest.fn().mockRejectedValueOnce(createCaptchaError()).mockResolvedValueOnce('result1'); - const fn2 = jest.fn().mockRejectedValueOnce(createCaptchaError()).mockResolvedValueOnce('result2'); + const fn1 = vi.fn().mockRejectedValueOnce(createCaptchaError()).mockResolvedValueOnce('result1'); + const fn2 = vi.fn().mockRejectedValueOnce(createCaptchaError()).mockResolvedValueOnce('result2'); const fn1res = sut.execute(mockClerk, fn1); const fn2res = sut.execute(mockClerk, fn2); @@ -115,8 +117,8 @@ describe('FraudProtectionService', () => { }); it('handles requests that were made in close succession by blocking all other requests if the first returns requires_captcha', async () => { - const fn1 = jest.fn().mockRejectedValueOnce(createCaptchaError()).mockResolvedValueOnce('result1'); - const fn2 = jest.fn().mockResolvedValue('result2'); + const fn1 = vi.fn().mockRejectedValueOnce(createCaptchaError()).mockResolvedValueOnce('result1'); + const fn2 = vi.fn().mockResolvedValue('result2'); // Start the first request const fn1res = sut.execute(mockClerk, fn1); @@ -147,10 +149,10 @@ describe('FraudProtectionService', () => { }); // both with fail in parallel but fn2 will temporarily be blocked from retrying - const fn1 = jest.fn().mockRejectedValueOnce(createCaptchaError()).mockRejectedValueOnce(unrelatedError); - const fn2 = jest.fn().mockRejectedValueOnce(createCaptchaError()).mockResolvedValue('result2'); + const fn1 = vi.fn().mockRejectedValueOnce(createCaptchaError()).mockRejectedValueOnce(unrelatedError); + const fn2 = vi.fn().mockRejectedValueOnce(createCaptchaError()).mockResolvedValue('result2'); // fn3 will be blocked until the captcha is solved - const fn3 = jest.fn().mockResolvedValue('result3'); + const fn3 = vi.fn().mockResolvedValue('result3'); const fn1res = sut.execute(mockClerk, fn1); const fn2res = sut.execute(mockClerk, fn2); @@ -163,7 +165,7 @@ describe('FraudProtectionService', () => { solveCaptcha(); // fn1 rejects - expect(fn1res).rejects.toEqual(unrelatedError); + await expect(fn1res).rejects.toEqual(unrelatedError); // but the other requests will be unblocked and retried await Promise.all([fn2res, fn3res]); diff --git a/packages/clerk-js/src/core/resources/__tests__/AuthConfig.test.ts b/packages/clerk-js/src/core/resources/__tests__/AuthConfig.spec.ts similarity index 89% rename from packages/clerk-js/src/core/resources/__tests__/AuthConfig.test.ts rename to packages/clerk-js/src/core/resources/__tests__/AuthConfig.spec.ts index 95a50f77110..bfce1d5c021 100644 --- a/packages/clerk-js/src/core/resources/__tests__/AuthConfig.test.ts +++ b/packages/clerk-js/src/core/resources/__tests__/AuthConfig.spec.ts @@ -1,8 +1,10 @@ +import { describe, expect, it, vi } from 'vitest'; + import { unixEpochToDate } from '../../../utils/date'; import { AuthConfig } from '../AuthConfig'; -jest.mock('../../../utils/date', () => ({ - unixEpochToDate: jest.fn(timestamp => new Date(timestamp)), +vi.mock('../../../utils/date', () => ({ + unixEpochToDate: vi.fn(timestamp => new Date(timestamp)), })); describe('AuthConfig', () => { diff --git a/packages/clerk-js/src/core/resources/__tests__/Base.test.ts b/packages/clerk-js/src/core/resources/__tests__/Base.spec.ts similarity index 86% rename from packages/clerk-js/src/core/resources/__tests__/Base.test.ts rename to packages/clerk-js/src/core/resources/__tests__/Base.spec.ts index bf7f8d6c57f..0eb8022b2a2 100644 --- a/packages/clerk-js/src/core/resources/__tests__/Base.test.ts +++ b/packages/clerk-js/src/core/resources/__tests__/Base.spec.ts @@ -1,3 +1,5 @@ +import { describe, expect, it, vi } from 'vitest'; + import { BaseResource } from '../internal'; class TestResource extends BaseResource { @@ -20,7 +22,7 @@ describe('BaseResource', () => { // @ts-expect-error - We're not about to mock the entire FapiClient getFapiClient: () => { return { - request: jest.fn().mockResolvedValue({ + request: vi.fn().mockResolvedValue({ payload: {}, status: 429, statusText: 'Too Many Requests', @@ -28,7 +30,7 @@ describe('BaseResource', () => { }), }; }, - __internal_setCountry: jest.fn(), + __internal_setCountry: vi.fn(), }; const resource = new TestResource(); const errResponse = await resource.fetch().catch(err => err); @@ -41,7 +43,7 @@ describe('BaseResource', () => { // @ts-expect-error - We're not about to mock the entire FapiClient getFapiClient: () => { return { - request: jest.fn().mockResolvedValue({ + request: vi.fn().mockResolvedValue({ payload: {}, status: 429, statusText: 'Too Many Requests', @@ -49,7 +51,7 @@ describe('BaseResource', () => { }), }; }, - __internal_setCountry: jest.fn(), + __internal_setCountry: vi.fn(), }; const resource = new TestResource(); const errResponse = await resource.fetch().catch(err => err); diff --git a/packages/clerk-js/src/core/resources/__tests__/Client.test.ts b/packages/clerk-js/src/core/resources/__tests__/Client.spec.ts similarity index 94% rename from packages/clerk-js/src/core/resources/__tests__/Client.test.ts rename to packages/clerk-js/src/core/resources/__tests__/Client.spec.ts index 9f23796622c..51e5c218396 100644 --- a/packages/clerk-js/src/core/resources/__tests__/Client.test.ts +++ b/packages/clerk-js/src/core/resources/__tests__/Client.spec.ts @@ -1,8 +1,20 @@ import type { ClientJSON, ClientJSONSnapshot } from '@clerk/types'; +import { afterAll, beforeAll, describe, expect, it, vi } from 'vitest'; -import { createSession, createSignIn, createSignUp, createUser } from '../../test/fixtures'; +import { createSession, createSignIn, createSignUp, createUser } from '../../vitest/fixtures'; import { BaseResource, Client } from '../internal'; +const FIXED_DATE = new Date('2025-01-01T00:00:00Z'); + +beforeAll(() => { + vi.useFakeTimers(); + vi.setSystemTime(FIXED_DATE); +}); + +afterAll(() => { + vi.useRealTimers(); +}); + describe('Client Singleton', () => { describe('__internal_sendCaptchaToken', () => { it('sends captcha token', async () => { @@ -16,12 +28,12 @@ describe('Client Singleton', () => { sign_in: createSignIn({ id: 'test_sign_in_id' }, user), sign_up: createSignUp({ id: 'test_sign_up_id' }), // This is only for testing purposes, this will never happen sessions: [session], - created_at: jest.now() - 1000, - updated_at: jest.now(), + created_at: Date.now() - 1000, + updated_at: Date.now(), } as any; // @ts-expect-error This is a private method that we are mocking - BaseResource._baseFetch = jest.fn(); + BaseResource._baseFetch = vi.fn(); const client = Client.getOrCreateInstance().fromJSON(clientObjectJSON); await client.__internal_sendCaptchaToken({ captcha_token: 'test_captcha_token' }); @@ -46,12 +58,12 @@ describe('Client Singleton', () => { sign_in: createSignIn({ id: 'test_sign_in_id' }, user), sign_up: createSignUp({ id: 'test_sign_up_id' }), // This is only for testing purposes, this will never happen sessions: [session], - created_at: jest.now() - 1000, - updated_at: jest.now(), + created_at: Date.now() - 1000, + updated_at: Date.now(), } as any; // @ts-expect-error This is a private method that we are mocking - BaseResource._baseFetch = jest.fn().mockResolvedValueOnce(Promise.resolve(null)); + BaseResource._baseFetch = vi.fn().mockResolvedValueOnce(Promise.resolve(null)); const client = Client.getOrCreateInstance().fromJSON(clientObjectJSON); await client.__internal_sendCaptchaToken({ captcha_token: 'test_captcha_token' }); @@ -78,14 +90,14 @@ describe('Client Singleton', () => { sign_in: createSignIn({ id: 'test_sign_in_id' }, user), sign_up: createSignUp({ id: 'test_sign_up_id' }), // This is only for testing purposes, this will never happen sessions: [session], - created_at: jest.now() - 1000, - updated_at: jest.now(), + created_at: Date.now() - 1000, + updated_at: Date.now(), }; const destroyedSession = createSession( { id: 'test_session_id', - abandon_at: jest.now(), + abandon_at: Date.now(), status: 'ended', last_active_token: undefined, }, @@ -99,12 +111,12 @@ describe('Client Singleton', () => { sign_in: null, sign_up: null, sessions: [destroyedSession], - created_at: jest.now() - 1000, - updated_at: jest.now(), + created_at: Date.now() - 1000, + updated_at: Date.now(), }; // @ts-expect-error This is a private method that we are mocking - BaseResource._fetch = jest.fn().mockReturnValue( + BaseResource._fetch = vi.fn().mockReturnValue( Promise.resolve({ client: null, response: clientObjectDeletedJSON, diff --git a/packages/clerk-js/src/core/resources/__tests__/Environment.test.ts b/packages/clerk-js/src/core/resources/__tests__/Environment.spec.ts similarity index 99% rename from packages/clerk-js/src/core/resources/__tests__/Environment.test.ts rename to packages/clerk-js/src/core/resources/__tests__/Environment.spec.ts index 9499a32e608..87b54aefcc4 100644 --- a/packages/clerk-js/src/core/resources/__tests__/Environment.test.ts +++ b/packages/clerk-js/src/core/resources/__tests__/Environment.spec.ts @@ -1,4 +1,5 @@ import type { EnvironmentJSONSnapshot } from '@clerk/types'; +import { describe, expect, it } from 'vitest'; import { Environment } from '../internal'; diff --git a/packages/clerk-js/src/core/resources/__tests__/ExternalAccount.test.ts b/packages/clerk-js/src/core/resources/__tests__/ExternalAccount.spec.ts similarity index 83% rename from packages/clerk-js/src/core/resources/__tests__/ExternalAccount.test.ts rename to packages/clerk-js/src/core/resources/__tests__/ExternalAccount.spec.ts index c61986913bb..9c59639cdb1 100644 --- a/packages/clerk-js/src/core/resources/__tests__/ExternalAccount.test.ts +++ b/packages/clerk-js/src/core/resources/__tests__/ExternalAccount.spec.ts @@ -1,3 +1,5 @@ +import { describe, expect, it, vi } from 'vitest'; + import { BaseResource, ExternalAccount } from '../internal'; describe('External account', () => { @@ -10,7 +12,7 @@ describe('External account', () => { }; // @ts-ignore - BaseResource._fetch = jest.fn().mockReturnValue(Promise.resolve({ response: externalAccountJSON })); + BaseResource._fetch = vi.fn().mockReturnValue(Promise.resolve({ response: externalAccountJSON })); const externalAccount = new ExternalAccount({ id: targetId }, '/me/external_accounts'); await externalAccount.reauthorize({ additionalScopes: ['read', 'write'], redirectUrl: 'https://test.com' }); @@ -36,7 +38,7 @@ describe('External account', () => { }; // @ts-ignore - BaseResource._fetch = jest.fn().mockReturnValue(Promise.resolve({ response: deletedObjectJSON })); + BaseResource._fetch = vi.fn().mockReturnValue(Promise.resolve({ response: deletedObjectJSON })); const externalAccount = new ExternalAccount({ id: targetId }, '/me/external_accounts'); await externalAccount.destroy(); diff --git a/packages/clerk-js/src/core/resources/__tests__/Image.test.ts b/packages/clerk-js/src/core/resources/__tests__/Image.spec.ts similarity index 86% rename from packages/clerk-js/src/core/resources/__tests__/Image.test.ts rename to packages/clerk-js/src/core/resources/__tests__/Image.spec.ts index c5118f68309..89f7888081b 100644 --- a/packages/clerk-js/src/core/resources/__tests__/Image.test.ts +++ b/packages/clerk-js/src/core/resources/__tests__/Image.spec.ts @@ -1,3 +1,5 @@ +import { describe, expect, it, vi } from 'vitest'; + import { Image } from '../Image'; import { BaseResource } from '../internal'; @@ -10,7 +12,7 @@ describe('Image', () => { }; // @ts-ignore - BaseResource._fetch = jest.fn().mockReturnValue( + BaseResource._fetch = vi.fn().mockReturnValue( Promise.resolve({ client: {}, response: mockResponse, diff --git a/packages/clerk-js/src/core/resources/__tests__/Session.test.ts b/packages/clerk-js/src/core/resources/__tests__/Session.spec.ts similarity index 98% rename from packages/clerk-js/src/core/resources/__tests__/Session.test.ts rename to packages/clerk-js/src/core/resources/__tests__/Session.spec.ts index 50ad42cd666..702432b5a19 100644 --- a/packages/clerk-js/src/core/resources/__tests__/Session.test.ts +++ b/packages/clerk-js/src/core/resources/__tests__/Session.spec.ts @@ -1,9 +1,10 @@ import type { InstanceType, OrganizationJSON, SessionJSON } from '@clerk/types'; +import { afterEach, beforeEach, describe, expect, it, type Mock, vi } from 'vitest'; import { eventBus } from '../../events'; import { createFapiClient } from '../../fapiClient'; -import { clerkMock, createUser, mockJwt, mockNetworkFailedFetch } from '../../test/fixtures'; import { SessionTokenCache } from '../../tokenCache'; +import { clerkMock, createUser, mockJwt, mockNetworkFailedFetch } from '../../vitest/fixtures'; import { BaseResource, Organization, Session } from '../internal'; const baseFapiClientOptions = { @@ -23,7 +24,7 @@ describe('Session', () => { let dispatchSpy; beforeEach(() => { - dispatchSpy = jest.spyOn(eventBus, 'emit'); + dispatchSpy = vi.spyOn(eventBus, 'emit'); BaseResource.clerk = clerkMock() as any; }); @@ -192,7 +193,7 @@ describe('Session', () => { writable: true, value: false, }); - warnSpy = jest.spyOn(console, 'warn').mockReturnValue(); + warnSpy = vi.spyOn(console, 'warn').mockReturnValue(); }); afterEach(() => { @@ -244,7 +245,7 @@ describe('Session', () => { await session.getToken(); - expect((BaseResource.fapiClient.request as jest.Mock).mock.calls[0][0]).toMatchObject({ + expect((BaseResource.fapiClient.request as Mock).mock.calls[0][0]).toMatchObject({ body: { organizationId: 'newActiveOrganization' }, }); }); diff --git a/packages/clerk-js/src/core/resources/__tests__/Token.test.ts b/packages/clerk-js/src/core/resources/__tests__/Token.spec.ts similarity index 93% rename from packages/clerk-js/src/core/resources/__tests__/Token.test.ts rename to packages/clerk-js/src/core/resources/__tests__/Token.spec.ts index 94e5e473354..fd19452f5e0 100644 --- a/packages/clerk-js/src/core/resources/__tests__/Token.test.ts +++ b/packages/clerk-js/src/core/resources/__tests__/Token.spec.ts @@ -1,8 +1,9 @@ import type { InstanceType } from '@clerk/types'; +import { afterEach, beforeEach, describe, expect, it, type Mock, vi } from 'vitest'; import { SUPPORTED_FAPI_VERSION } from '../../constants'; import { createFapiClient } from '../../fapiClient'; -import { mockFetch, mockNetworkFailedFetch } from '../../test/fixtures'; +import { mockFetch, mockNetworkFailedFetch } from '../../vitest/fixtures'; import { BaseResource } from '../internal'; import { Token } from '../Token'; @@ -17,7 +18,7 @@ const baseFapiClientOptions = { describe('Token', () => { describe('create', () => { afterEach(() => { - (global.fetch as jest.Mock)?.mockClear(); + (global.fetch as Mock)?.mockClear(); BaseResource.clerk = null as any; }); @@ -49,7 +50,7 @@ describe('Token', () => { writable: true, value: false, }); - warnSpy = jest.spyOn(console, 'warn').mockReturnValue(); + warnSpy = vi.spyOn(console, 'warn').mockReturnValue(); }); afterEach(() => { diff --git a/packages/clerk-js/src/core/resources/__tests__/User.test.ts b/packages/clerk-js/src/core/resources/__tests__/User.spec.ts similarity index 90% rename from packages/clerk-js/src/core/resources/__tests__/User.test.ts rename to packages/clerk-js/src/core/resources/__tests__/User.spec.ts index bfec0f83123..04eda1aa6d4 100644 --- a/packages/clerk-js/src/core/resources/__tests__/User.test.ts +++ b/packages/clerk-js/src/core/resources/__tests__/User.spec.ts @@ -1,4 +1,5 @@ import type { UserJSON } from '@clerk/types'; +import { describe, expect, it, vi } from 'vitest'; import { BaseResource } from '../internal'; import { User } from '../User'; @@ -14,7 +15,7 @@ describe('User', () => { }; // @ts-ignore - BaseResource._fetch = jest.fn().mockReturnValue(Promise.resolve({ response: externalAccountJSON })); + BaseResource._fetch = vi.fn().mockReturnValue(Promise.resolve({ response: externalAccountJSON })); const user = new User({ email_addresses: [], @@ -49,7 +50,7 @@ describe('User', () => { }; // @ts-ignore - BaseResource._fetch = jest.fn().mockReturnValue(Promise.resolve({ response: web3WalletJSON })); + BaseResource._fetch = vi.fn().mockReturnValue(Promise.resolve({ response: web3WalletJSON })); const user = new User({ email_addresses: [], @@ -148,7 +149,7 @@ describe('User', () => { }; // @ts-ignore - BaseResource._fetch = jest.fn().mockReturnValue(Promise.resolve({ response: totpJSON })); + BaseResource._fetch = vi.fn().mockReturnValue(Promise.resolve({ response: totpJSON })); const user = new User({ email_addresses: [], @@ -176,7 +177,7 @@ describe('User', () => { }; // @ts-ignore - BaseResource._fetch = jest.fn().mockReturnValue(Promise.resolve({ response: totpJSON })); + BaseResource._fetch = vi.fn().mockReturnValue(Promise.resolve({ response: totpJSON })); const user = new User({ email_addresses: [], @@ -203,7 +204,7 @@ describe('User', () => { }; // @ts-ignore - BaseResource._fetch = jest.fn().mockReturnValue(Promise.resolve({ response: deletedObjectJSON })); + BaseResource._fetch = vi.fn().mockReturnValue(Promise.resolve({ response: deletedObjectJSON })); const user = new User({ email_addresses: [], @@ -229,7 +230,7 @@ describe('User', () => { }; // @ts-ignore - BaseResource._fetch = jest.fn().mockReturnValue(Promise.resolve({ response: backupCodeJSON })); + BaseResource._fetch = vi.fn().mockReturnValue(Promise.resolve({ response: backupCodeJSON })); const user = new User({ email_addresses: [], @@ -270,7 +271,7 @@ describe('User', () => { it('.updatePassword triggers a request to change password', async () => { // @ts-ignore - BaseResource._fetch = jest.fn().mockReturnValue(Promise.resolve({ response: {} })); + BaseResource._fetch = vi.fn().mockReturnValue(Promise.resolve({ response: {} })); const user = new User({} as unknown as UserJSON); const params = { @@ -289,7 +290,7 @@ describe('User', () => { it('.removePassword triggers a request to remove password', async () => { // @ts-ignore - BaseResource._fetch = jest.fn().mockReturnValue(Promise.resolve({ response: {} })); + BaseResource._fetch = vi.fn().mockReturnValue(Promise.resolve({ response: {} })); const user = new User({} as unknown as UserJSON); const params = { diff --git a/packages/clerk-js/src/core/resources/__tests__/UserSettings.test.ts b/packages/clerk-js/src/core/resources/__tests__/UserSettings.spec.ts similarity index 99% rename from packages/clerk-js/src/core/resources/__tests__/UserSettings.test.ts rename to packages/clerk-js/src/core/resources/__tests__/UserSettings.spec.ts index 6c9b55da98f..86a215c8589 100644 --- a/packages/clerk-js/src/core/resources/__tests__/UserSettings.test.ts +++ b/packages/clerk-js/src/core/resources/__tests__/UserSettings.spec.ts @@ -1,4 +1,5 @@ import type { UserSettingsJSON } from '@clerk/types'; +import { describe, expect, it } from 'vitest'; import { UserSettings } from '../internal'; diff --git a/packages/clerk-js/src/core/resources/__tests__/Web3Wallet.test.ts b/packages/clerk-js/src/core/resources/__tests__/Web3Wallet.spec.ts similarity index 84% rename from packages/clerk-js/src/core/resources/__tests__/Web3Wallet.test.ts rename to packages/clerk-js/src/core/resources/__tests__/Web3Wallet.spec.ts index 0bb32228eb5..9848dbe4561 100644 --- a/packages/clerk-js/src/core/resources/__tests__/Web3Wallet.test.ts +++ b/packages/clerk-js/src/core/resources/__tests__/Web3Wallet.spec.ts @@ -1,4 +1,5 @@ import type { Web3WalletJSON } from '@clerk/types'; +import { describe, expect, it, vi } from 'vitest'; import { BaseResource, Web3Wallet } from '../internal'; @@ -10,7 +11,7 @@ describe('Web3 wallet', () => { } as Web3WalletJSON; // @ts-ignore - BaseResource._fetch = jest.fn().mockReturnValue(Promise.resolve({ response: web3WalletJSON })); + BaseResource._fetch = vi.fn().mockReturnValue(Promise.resolve({ response: web3WalletJSON })); const web3Wallet = new Web3Wallet(web3WalletJSON, '/me/web3_wallets'); await web3Wallet.create(); @@ -33,7 +34,7 @@ describe('Web3 wallet', () => { } as Web3WalletJSON; // @ts-ignore - BaseResource._fetch = jest.fn().mockReturnValue(Promise.resolve({ response: web3WalletJSON })); + BaseResource._fetch = vi.fn().mockReturnValue(Promise.resolve({ response: web3WalletJSON })); const web3Wallet = new Web3Wallet(web3WalletJSON, '/me/web3_wallets'); await web3Wallet.prepareVerification({ strategy: 'web3_metamask_signature' }); @@ -56,7 +57,7 @@ describe('Web3 wallet', () => { } as Web3WalletJSON; // @ts-ignore - BaseResource._fetch = jest.fn().mockReturnValue(Promise.resolve({ response: web3WalletJSON })); + BaseResource._fetch = vi.fn().mockReturnValue(Promise.resolve({ response: web3WalletJSON })); const web3Wallet = new Web3Wallet(web3WalletJSON, '/me/web3_wallets'); await web3Wallet.attemptVerification({ signature: 'mock-signature' }); @@ -81,7 +82,7 @@ describe('Web3 wallet', () => { }; // @ts-ignore - BaseResource._fetch = jest.fn().mockReturnValue(Promise.resolve({ response: deletedObjectJSON })); + BaseResource._fetch = vi.fn().mockReturnValue(Promise.resolve({ response: deletedObjectJSON })); const web3Wallet = new Web3Wallet({ id: targetId }, '/me/web3_wallets'); await web3Wallet.destroy(); diff --git a/packages/clerk-js/src/core/vitest/fixtures.ts b/packages/clerk-js/src/core/vitest/fixtures.ts new file mode 100644 index 00000000000..f7d1587fc97 --- /dev/null +++ b/packages/clerk-js/src/core/vitest/fixtures.ts @@ -0,0 +1,285 @@ +import type { + Clerk, + EmailAddressJSON, + ExternalAccountJSON, + OAuthProvider, + OrganizationCustomRoleKey, + OrganizationJSON, + OrganizationMembershipJSON, + OrganizationPermissionKey, + PhoneNumberJSON, + SessionJSON, + SignInJSON, + SignUpJSON, + UserJSON, +} from '@clerk/types'; +import { vi } from 'vitest'; + +export const mockJwt = + 'eyJhbGciOiJSUzI1NiIsImtpZCI6Imluc18yR0lvUWhiVXB5MGhYN0IyY1ZrdVRNaW5Yb0QiLCJ0eXAiOiJKV1QifQ.eyJhenAiOiJodHRwczovL2FjY291bnRzLmluc3BpcmVkLnB1bWEtNzQubGNsLmRldiIsImV4cCI6MTY2NjY0ODMxMCwiaWF0IjoxNjY2NjQ4MjUwLCJpc3MiOiJodHRwczovL2NsZXJrLmluc3BpcmVkLnB1bWEtNzQubGNsLmRldiIsIm5iZiI6MTY2NjY0ODI0MCwic2lkIjoic2Vzc18yR2JEQjRlbk5kQ2E1dlMxenBDM1h6Zzl0SzkiLCJzdWIiOiJ1c2VyXzJHSXBYT0VwVnlKdzUxcmtabjlLbW5jNlN4ciJ9.n1Usc-DLDftqA0Xb-_2w8IGs4yjCmwc5RngwbSRvwevuZOIuRoeHmE2sgCdEvjfJEa7ewL6EVGVcM557TWPW--g_J1XQPwBy8tXfz7-S73CEuyRFiR97L2AHRdvRtvGtwR-o6l8aHaFxtlmfWbQXfg4kFJz2UGe9afmh3U9-f_4JOZ5fa3mI98UMy1-bo20vjXeWQ9aGrqaxHQxjnzzC-1Kpi5LdPvhQ16H0dPB8MHRTSM5TAuLKTpPV7wqixmbtcc2-0k6b9FKYZNqRVTaIyV-lifZloBvdzlfOF8nW1VVH_fx-iW5Q3hovHFcJIULHEC1kcAYTubbxzpgeVQepGg'; + +export type OrgParams = Partial & { + role?: OrganizationCustomRoleKey; + permissions?: OrganizationPermissionKey[]; +}; + +type WithUserParams = Omit< + Partial, + 'email_addresses' | 'phone_numbers' | 'external_accounts' | 'organization_memberships' +> & { + email_addresses?: Array>; + phone_numbers?: Array>; + external_accounts?: Array>; + organization_memberships?: Array; +}; + +type WithSessionParams = Partial; + +export const getOrganizationId = (orgParams: OrgParams) => orgParams?.id || orgParams?.name || 'test_id'; + +export const createOrganizationMembership = (params: OrgParams): OrganizationMembershipJSON => { + const { role, permissions, ...orgParams } = params; + return { + created_at: new Date().getTime(), + id: getOrganizationId(orgParams), + object: 'organization_membership', + organization: { + created_at: new Date().getTime(), + id: getOrganizationId(orgParams), + image_url: + 'https://img.clerk.com/eyJ0eXBlIjoiZGVmYXVsdCIsImlpZCI6Imluc18xbHlXRFppb2JyNjAwQUtVZVFEb1NsckVtb00iLCJyaWQiOiJ1c2VyXzJKbElJQTN2VXNjWXh1N2VUMnhINmFrTGgxOCIsImluaXRpYWxzIjoiREsifQ?width=160', + max_allowed_memberships: 3, + members_count: 1, + name: 'Org', + object: 'organization', + pending_invitations_count: 0, + public_metadata: {}, + slug: null, + updated_at: new Date().getTime(), + ...orgParams, + } as OrganizationJSON, + public_metadata: {}, + role: role || 'admin', + permissions: permissions || [ + 'org:sys_domains:manage', + 'org:sys_domains:read', + 'org:sys_memberships:manage', + 'org:sys_memberships:read', + 'org:sys_profile:delete', + 'org:sys_profile:manage', + ], + updated_at: new Date().getTime(), + } as OrganizationMembershipJSON; +}; + +export const createEmail = (params?: Partial): EmailAddressJSON => { + return { + object: 'email_address', + id: params?.email_address || '', + email_address: 'test@clerk.com', + reserved: false, + verification: { + status: 'verified', + strategy: 'email_link', + attempts: null, + expire_at: 1635977979774, + }, + linked_to: [], + ...params, + } as EmailAddressJSON; +}; + +export const createPhoneNumber = (params?: Partial): PhoneNumberJSON => { + return { + object: 'phone_number', + id: params?.phone_number || '', + phone_number: '+30 691 1111111', + reserved: false, + verification: { + status: 'verified', + strategy: 'phone_code', + attempts: null, + expire_at: 1635977979774, + }, + linked_to: [], + ...params, + } as PhoneNumberJSON; +}; + +export const createExternalAccount = (params?: Partial): ExternalAccountJSON => { + return { + id: params?.provider || '', + object: 'external_account', + provider: 'google', + identification_id: '98675202', + provider_user_id: '3232', + approved_scopes: '', + email_address: 'test@clerk.com', + first_name: 'First name', + last_name: 'Last name', + image_url: '', + username: '', + phoneNumber: '', + verification: { + status: 'verified', + strategy: '', + attempts: null, + expire_at: 1635977979774, + }, + ...params, + } as ExternalAccountJSON; +}; + +export const createUser = (params?: WithUserParams): UserJSON => { + const res = { + object: 'user', + id: params?.id || 'user_123', + primary_email_address_id: '', + primary_phone_number_id: '', + primary_web3_wallet_id: '', + image_url: '', + username: 'testUsername', + web3_wallets: [], + password: '', + profile_image_id: '', + first_name: 'FirstName', + last_name: 'LastName', + password_enabled: false, + totp_enabled: false, + backup_code_enabled: false, + two_factor_enabled: false, + public_metadata: {}, + unsafe_metadata: {}, + last_sign_in_at: null, + updated_at: new Date().getTime(), + created_at: new Date().getTime(), + ...params, + email_addresses: (params?.email_addresses || []).map(e => + typeof e === 'string' ? createEmail({ email_address: e }) : createEmail(e), + ), + phone_numbers: (params?.phone_numbers || []).map(n => + typeof n === 'string' ? createPhoneNumber({ phone_number: n }) : createPhoneNumber(n), + ), + external_accounts: (params?.external_accounts || []).map(p => + typeof p === 'string' ? createExternalAccount({ provider: p }) : createExternalAccount(p), + ), + organization_memberships: (params?.organization_memberships || []).map(o => + typeof o === 'string' ? createOrganizationMembership({ name: o }) : createOrganizationMembership(o), + ), + } as UserJSON; + res.primary_email_address_id = res.email_addresses[0]?.id; + return res; +}; + +export const createSession = (sessionParams: WithSessionParams = {}, user: Partial = {}) => { + return { + object: 'session', + id: sessionParams.id, + status: sessionParams.status, + expire_at: sessionParams.expire_at || Date.now() + 5000, + abandon_at: sessionParams.abandon_at, + last_active_at: sessionParams.last_active_at || Date.now(), + last_active_organization_id: sessionParams.last_active_organization_id, + actor: sessionParams.actor, + user: createUser({}), + public_user_data: { + first_name: user.first_name, + last_name: user.last_name, + image_url: user.image_url, + has_image: user.has_image, + identifier: user.email_addresses?.find(e => e.id === user.primary_email_address_id)?.email_address || '', + }, + created_at: sessionParams.created_at || Date.now() - 1000, + updated_at: sessionParams.updated_at || Date.now(), + last_active_token: { + object: 'token', + jwt: mockJwt, + }, + } as SessionJSON; +}; + +export const createSignIn = (signInParams: Partial = {}, user: Partial = {}) => { + return { + id: signInParams.id, + created_session_id: signInParams.created_session_id, + status: signInParams.status, + first_factor_verification: signInParams.first_factor_verification, + identifier: signInParams.identifier, + object: 'sign_in', + second_factor_verification: signInParams.second_factor_verification, + supported_first_factors: signInParams.supported_first_factors, + supported_second_factors: signInParams.supported_second_factors, + user_data: { + first_name: user.first_name, + last_name: user.last_name, + image_url: user.image_url, + has_image: user.has_image, + }, + } as SignInJSON; +}; + +export const createSignUp = (signUpParams: Partial = {}) => { + return { + id: signUpParams.id, + created_session_id: signUpParams.created_session_id, + status: signUpParams.status, + abandon_at: signUpParams.abandon_at, + created_user_id: signUpParams.created_user_id, + email_address: signUpParams.email_address, + external_account: signUpParams.external_account, + external_account_strategy: signUpParams.external_account_strategy, + first_name: signUpParams.first_name, + has_password: signUpParams.has_password, + last_name: signUpParams.last_name, + missing_fields: signUpParams.missing_fields, + object: 'sign_up', + optional_fields: signUpParams.optional_fields, + phone_number: signUpParams.phone_number, + required_fields: signUpParams.required_fields, + unsafe_metadata: signUpParams.unsafe_metadata, + unverified_fields: signUpParams.unverified_fields, + username: signUpParams.username, + verifications: signUpParams.verifications, + web3_wallet: signUpParams.web3_wallet, + } as SignUpJSON; +}; + +export const clerkMock = (params?: Partial) => { + return { + getFapiClient: vi.fn().mockReturnValue({ + request: vi.fn().mockReturnValue({ payload: { object: 'token', jwt: mockJwt }, status: 200 }), + }), + ...params, + }; +}; + +type RecursivePartial = { + [P in keyof T]?: RecursivePartial; +}; + +export const mockFetch = (ok = true, status = 200, responsePayload = {}) => { + // @ts-ignore + global.fetch = vi.fn(() => { + return Promise.resolve>({ + status, + statusText: status.toString(), + ok, + json: () => Promise.resolve(responsePayload), + }); + }); +}; + +export const mockNetworkFailedFetch = () => { + // @ts-ignore + global.fetch = vi.fn(() => { + return Promise.reject(new TypeError('Failed to fetch')); + }); +}; + +export const mockDevClerkInstance = { + frontendApi: 'clerk.example.com', + instanceType: 'development', + isSatellite: false, + version: 'test-0.0.0', + domain: '', +}; diff --git a/packages/clerk-js/src/ui/common/__tests__/redirects.test.ts b/packages/clerk-js/src/ui/common/__tests__/redirects.spec.ts similarity index 80% rename from packages/clerk-js/src/ui/common/__tests__/redirects.test.ts rename to packages/clerk-js/src/ui/common/__tests__/redirects.spec.ts index 11c706e8c65..c9be5720603 100644 --- a/packages/clerk-js/src/ui/common/__tests__/redirects.test.ts +++ b/packages/clerk-js/src/ui/common/__tests__/redirects.spec.ts @@ -1,12 +1,13 @@ -import { buildSSOCallbackURL, buildVerificationRedirectUrl, buildVerificationRedirectUrl } from '../redirects'; +import { describe, expect, it } from 'vitest'; + +import { buildSSOCallbackURL, buildVerificationRedirectUrl } from '../redirects'; describe('buildVerificationRedirectUrl(routing, baseUrl)', () => { it('defaults to hash based routing strategy on empty routing', function () { expect( buildVerificationRedirectUrl({ ctx: { path: '', authQueryString: '' } as any, baseUrl: '', intent: 'sign-in' }), - ).toBe('http://localhost/#/verify'); + ).toBe('http://localhost:3000/#/verify'); }); - it('returns the magic link redirect url for components using path based routing ', function () { expect( buildVerificationRedirectUrl({ @@ -14,7 +15,7 @@ describe('buildVerificationRedirectUrl(routing, baseUrl)', () => { baseUrl: '', intent: 'sign-in', }), - ).toBe('http://localhost/verify'); + ).toBe('http://localhost:3000/verify'); expect( buildVerificationRedirectUrl({ @@ -22,7 +23,7 @@ describe('buildVerificationRedirectUrl(routing, baseUrl)', () => { baseUrl: '', intent: 'sign-in', }), - ).toBe('http://localhost/sign-in/verify'); + ).toBe('http://localhost:3000/sign-in/verify'); expect( buildVerificationRedirectUrl({ @@ -34,7 +35,7 @@ describe('buildVerificationRedirectUrl(routing, baseUrl)', () => { baseUrl: '', intent: 'sign-in', }), - ).toBe('http://localhost/verify?redirectUrl=https://clerk.com'); + ).toBe('http://localhost:3000/verify?redirectUrl=https://clerk.com'); expect( buildVerificationRedirectUrl({ @@ -46,7 +47,7 @@ describe('buildVerificationRedirectUrl(routing, baseUrl)', () => { baseUrl: '', intent: 'sign-in', }), - ).toBe('http://localhost/sign-in/verify?redirectUrl=https://clerk.com'); + ).toBe('http://localhost:3000/sign-in/verify?redirectUrl=https://clerk.com'); expect( buildVerificationRedirectUrl({ @@ -58,9 +59,8 @@ describe('buildVerificationRedirectUrl(routing, baseUrl)', () => { baseUrl: 'https://accounts.clerk.com/sign-in', intent: 'sign-in', }), - ).toBe('http://localhost/sign-in/verify?redirectUrl=https://clerk.com'); + ).toBe('http://localhost:3000/sign-in/verify?redirectUrl=https://clerk.com'); }); - it('returns the magic link redirect url for components using hash based routing ', function () { expect( buildVerificationRedirectUrl({ @@ -71,7 +71,7 @@ describe('buildVerificationRedirectUrl(routing, baseUrl)', () => { baseUrl: '', intent: 'sign-in', }), - ).toBe('http://localhost/#/verify'); + ).toBe('http://localhost:3000/#/verify'); expect( buildVerificationRedirectUrl({ @@ -83,7 +83,7 @@ describe('buildVerificationRedirectUrl(routing, baseUrl)', () => { baseUrl: '', intent: 'sign-in', }), - ).toBe('http://localhost/#/verify'); + ).toBe('http://localhost:3000/#/verify'); expect( buildVerificationRedirectUrl({ @@ -95,7 +95,7 @@ describe('buildVerificationRedirectUrl(routing, baseUrl)', () => { baseUrl: '', intent: 'sign-in', }), - ).toBe('http://localhost/#/verify?redirectUrl=https://clerk.com'); + ).toBe('http://localhost:3000/#/verify?redirectUrl=https://clerk.com'); expect( buildVerificationRedirectUrl({ @@ -107,7 +107,7 @@ describe('buildVerificationRedirectUrl(routing, baseUrl)', () => { baseUrl: '', intent: 'sign-in', }), - ).toBe('http://localhost/#/verify?redirectUrl=https://clerk.com'); + ).toBe('http://localhost:3000/#/verify?redirectUrl=https://clerk.com'); expect( buildVerificationRedirectUrl({ @@ -119,9 +119,8 @@ describe('buildVerificationRedirectUrl(routing, baseUrl)', () => { baseUrl: 'https://accounts.clerk.com/sign-in', intent: 'sign-in', }), - ).toBe('http://localhost/#/verify?redirectUrl=https://clerk.com'); + ).toBe('http://localhost:3000/#/verify?redirectUrl=https://clerk.com'); }); - it('returns the magic link redirect url for components using virtual routing ', function () { expect( buildVerificationRedirectUrl({ @@ -156,7 +155,7 @@ describe('buildVerificationRedirectUrl(routing, baseUrl)', () => { baseUrl: '', intent: 'sign-up', }), - ).toBe('http://localhost/sign-up/create/verify'); + ).toBe('http://localhost:3000/sign-up/create/verify'); expect( buildVerificationRedirectUrl({ @@ -168,28 +167,28 @@ describe('buildVerificationRedirectUrl(routing, baseUrl)', () => { baseUrl: '', intent: 'sign-in', }), - ).toBe('http://localhost/sign-in/verify'); + ).toBe('http://localhost:3000/sign-in/verify'); }); }); describe('buildSSOCallbackURL(ctx, baseUrl)', () => { it('returns the SSO callback URL based on sign in|up component routing or the provided base URL', () => { // Default callback URLS - expect(buildSSOCallbackURL({}, '')).toBe('http://localhost/#/sso-callback'); - expect(buildSSOCallbackURL({}, 'http://test.host')).toBe('http://localhost/#/sso-callback'); + expect(buildSSOCallbackURL({}, '')).toBe('http://localhost:3000/#/sso-callback'); + expect(buildSSOCallbackURL({}, 'http://test.host')).toBe('http://localhost:3000/#/sso-callback'); expect(buildSSOCallbackURL({ authQueryString: 'redirect_url=%2Ffoo' }, 'http://test.host')).toBe( - 'http://localhost/#/sso-callback?redirect_url=%2Ffoo', + 'http://localhost:3000/#/sso-callback?redirect_url=%2Ffoo', ); // Components mounted with hash routing - expect(buildSSOCallbackURL({ routing: 'hash' }, 'http://test.host')).toBe('http://localhost/#/sso-callback'); + expect(buildSSOCallbackURL({ routing: 'hash' }, 'http://test.host')).toBe('http://localhost:3000/#/sso-callback'); expect(buildSSOCallbackURL({ routing: 'hash', authQueryString: 'redirect_url=%2Ffoo' }, 'http://test.host')).toBe( - 'http://localhost/#/sso-callback?redirect_url=%2Ffoo', + 'http://localhost:3000/#/sso-callback?redirect_url=%2Ffoo', ); // Components mounted with path routing expect(buildSSOCallbackURL({ routing: 'path', path: 'sign-in' }, 'http://test.host')).toBe( - 'http://localhost/sign-in/sso-callback', + 'http://localhost:3000/sign-in/sso-callback', ); expect( buildSSOCallbackURL( @@ -200,7 +199,7 @@ describe('buildSSOCallbackURL(ctx, baseUrl)', () => { }, 'http://test.host', ), - ).toBe('http://localhost/sign-in/sso-callback?redirect_url=%2Ffoo'); + ).toBe('http://localhost:3000/sign-in/sso-callback?redirect_url=%2Ffoo'); // Components mounted with virtual routing expect(buildSSOCallbackURL({ routing: 'virtual' }, 'http://test.host')).toBe('http://test.host/#/sso-callback'); @@ -219,6 +218,6 @@ describe('buildSSOCallbackURL(ctx, baseUrl)', () => { ssoCallbackUrl: 'http://test.host/ctx-sso-callback', routing: 'virtual', }), - ).toBe('http://localhost/#/sso-callback'); + ).toBe('http://localhost:3000/#/sso-callback'); }); }); diff --git a/packages/clerk-js/src/ui/components/OrganizationSwitcher/__tests__/vitestUtils.ts b/packages/clerk-js/src/ui/components/OrganizationSwitcher/__tests__/vitestUtils.ts new file mode 100644 index 00000000000..b99be7ff39e --- /dev/null +++ b/packages/clerk-js/src/ui/components/OrganizationSwitcher/__tests__/vitestUtils.ts @@ -0,0 +1,145 @@ +import type { + OrganizationCustomRoleKey, + OrganizationInvitationStatus, + OrganizationMembershipResource, + OrganizationResource, + OrganizationSuggestionResource, + OrganizationSuggestionStatus, + UserOrganizationInvitationResource, +} from '@clerk/types'; +import { vi } from 'vitest'; + +export type FakeOrganizationParams = { + id: string; + name: string; + slug: string; + imageUrl?: string; + membersCount: number; + pendingInvitationsCount: number; + adminDeleteEnabled: boolean; + maxAllowedMemberships: number; + createdAt?: Date; +}; + +export const createFakeOrganization = (params: FakeOrganizationParams): OrganizationResource => { + return { + pathRoot: '', + id: params.id, + name: params.name, + slug: params.slug, + hasImage: !!params.imageUrl, + imageUrl: params.imageUrl || '', + membersCount: params.membersCount, + pendingInvitationsCount: params.pendingInvitationsCount, + publicMetadata: {}, + adminDeleteEnabled: params.adminDeleteEnabled, + maxAllowedMemberships: params?.maxAllowedMemberships, + createdAt: params?.createdAt || new Date(), + updatedAt: new Date(), + update: vi.fn() as any, + getMemberships: vi.fn() as any, + addMember: vi.fn() as any, + inviteMember: vi.fn() as any, + inviteMembers: vi.fn() as any, + updateMember: vi.fn() as any, + removeMember: vi.fn() as any, + createDomain: vi.fn() as any, + getDomain: vi.fn() as any, + getDomains: vi.fn() as any, + getMembershipRequests: vi.fn() as any, + destroy: vi.fn() as any, + setLogo: vi.fn() as any, + reload: vi.fn() as any, + }; +}; + +type FakeOrganizationInvitationParams = { + id: string; + createdAt?: Date; + emailAddress: string; + role?: OrganizationCustomRoleKey; + status?: OrganizationInvitationStatus; + publicOrganizationData?: { hasImage?: boolean; id?: string; imageUrl?: string; name?: string; slug?: string }; +}; + +export const createFakeUserOrganizationInvitation = ( + params: FakeOrganizationInvitationParams, +): UserOrganizationInvitationResource => { + return { + pathRoot: '', + emailAddress: params.emailAddress, + publicOrganizationData: { + hasImage: false, + id: '', + imageUrl: '', + name: '', + slug: '', + ...params.publicOrganizationData, + }, + role: params.role || 'basic_member', + status: params.status || 'pending', + id: params.id, + createdAt: params?.createdAt || new Date(), + updatedAt: new Date(), + publicMetadata: {}, + accept: vi.fn() as any, + reload: vi.fn() as any, + }; +}; + +type FakeUserOrganizationMembershipParams = { + id: string; + createdAt?: Date; + role?: OrganizationCustomRoleKey; + organization: FakeOrganizationParams; +}; + +export const createFakeUserOrganizationMembership = ( + params: FakeUserOrganizationMembershipParams, +): OrganizationMembershipResource => { + return { + organization: createFakeOrganization(params.organization), + pathRoot: '', + role: params.role || 'basic_member', + permissions: [], + id: params.id, + createdAt: params?.createdAt || new Date(), + updatedAt: new Date(), + publicMetadata: {}, + publicUserData: {} as any, + update: vi.fn() as any, + destroy: vi.fn() as any, + reload: vi.fn() as any, + }; +}; + +type FakeOrganizationSuggestionParams = { + id: string; + createdAt?: Date; + emailAddress: string; + role?: OrganizationCustomRoleKey; + status?: OrganizationSuggestionStatus; + publicOrganizationData?: { hasImage?: boolean; id?: string; imageUrl?: string; name?: string; slug?: string }; +}; + +export const createFakeUserOrganizationSuggestion = ( + params: FakeOrganizationSuggestionParams, +): OrganizationSuggestionResource => { + return { + pathRoot: '', + publicOrganizationData: { + hasImage: false, + id: '', + imageUrl: '', + name: '', + slug: '', + ...params.publicOrganizationData, + }, + status: params.status || 'pending', + id: params.id, + createdAt: params?.createdAt || new Date(), + updatedAt: new Date(), + accept: vi.fn() as any, + reload: vi.fn() as any, + }; +}; diff --git a/packages/clerk-js/src/ui/components/SignUp/__tests__/SignUpStart.test.tsx b/packages/clerk-js/src/ui/components/SignUp/__tests__/SignUpStart.spec.tsx similarity index 97% rename from packages/clerk-js/src/ui/components/SignUp/__tests__/SignUpStart.test.tsx rename to packages/clerk-js/src/ui/components/SignUp/__tests__/SignUpStart.spec.tsx index 4cb14aedb62..2e4b61ad97b 100644 --- a/packages/clerk-js/src/ui/components/SignUp/__tests__/SignUpStart.test.tsx +++ b/packages/clerk-js/src/ui/components/SignUp/__tests__/SignUpStart.spec.tsx @@ -1,12 +1,13 @@ import { OAUTH_PROVIDERS } from '@clerk/shared/oauth'; import type { SignUpResource } from '@clerk/types'; +import { describe, expect, it, vi } from 'vitest'; import { CardStateProvider } from '@/ui/elements/contexts'; -import { render, screen, waitFor } from '../../../../testUtils'; +import { render, screen, waitFor } from '../../../../vitestUtils'; import { OptionsProvider } from '../../../contexts'; import { AppearanceProvider } from '../../../customizables'; -import { bindCreateFixtures } from '../../../utils/test/createFixtures'; +import { bindCreateFixtures } from '../../../utils/vitest/createFixtures'; import { SignUpStart } from '../SignUpStart'; const { createFixtures } = bindCreateFixtures('SignUp'); @@ -298,7 +299,7 @@ describe('SignUpStart', () => { }); Object.defineProperty(window, 'history', { writable: true, - value: { replaceState: jest.fn() }, + value: { replaceState: vi.fn() }, }); render( @@ -333,7 +334,7 @@ describe('SignUpStart', () => { }); Object.defineProperty(window, 'history', { writable: true, - value: { replaceState: jest.fn() }, + value: { replaceState: vi.fn() }, }); render( @@ -373,7 +374,7 @@ describe('SignUpStart', () => { }); Object.defineProperty(window, 'history', { writable: true, - value: { replaceState: jest.fn() }, + value: { replaceState: vi.fn() }, }); render( diff --git a/packages/clerk-js/src/ui/elements/__tests__/PlainInput.test.tsx b/packages/clerk-js/src/ui/elements/__tests__/PlainInput.spec.tsx similarity index 96% rename from packages/clerk-js/src/ui/elements/__tests__/PlainInput.test.tsx rename to packages/clerk-js/src/ui/elements/__tests__/PlainInput.spec.tsx index ff7cd32b4b3..17bfbf135ae 100644 --- a/packages/clerk-js/src/ui/elements/__tests__/PlainInput.test.tsx +++ b/packages/clerk-js/src/ui/elements/__tests__/PlainInput.spec.tsx @@ -1,9 +1,9 @@ -import { describe, it } from '@jest/globals'; import { fireEvent, render } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; +import { describe, expect, it } from 'vitest'; import { useFormControl } from '../../utils'; -import { bindCreateFixtures } from '../../utils/test/createFixtures'; +import { bindCreateFixtures } from '../../utils/vitest/createFixtures'; import { withCardStateProvider } from '../contexts'; import { Form } from '../Form'; @@ -135,7 +135,7 @@ describe('PlainInput', () => { await userEvent.click(getByRole('button', { name: /set error/i })); - expect(await findByText(/some error/i)).toBeInTheDocument(); + expect(await findByText(/Some Error/i)).toBeInTheDocument(); const label = getByLabelText(/some label/i); expect(label).toHaveAttribute('aria-invalid', 'true'); diff --git a/packages/clerk-js/src/ui/elements/__tests__/RadioGroup.test.tsx b/packages/clerk-js/src/ui/elements/__tests__/RadioGroup.spec.tsx similarity index 96% rename from packages/clerk-js/src/ui/elements/__tests__/RadioGroup.test.tsx rename to packages/clerk-js/src/ui/elements/__tests__/RadioGroup.spec.tsx index dcb8f2951d3..52fb2e9e39d 100644 --- a/packages/clerk-js/src/ui/elements/__tests__/RadioGroup.test.tsx +++ b/packages/clerk-js/src/ui/elements/__tests__/RadioGroup.spec.tsx @@ -1,9 +1,9 @@ -import { describe, it } from '@jest/globals'; import { fireEvent, render } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; +import { describe, expect, it } from 'vitest'; import { useFormControl } from '../../utils'; -import { bindCreateFixtures } from '../../utils/test/createFixtures'; +import { bindCreateFixtures } from '../../utils/vitest/createFixtures'; import { withCardStateProvider } from '../contexts'; import { Form } from '../Form'; @@ -167,7 +167,7 @@ describe('RadioGroup', () => { ); await userEvent.click(getByRole('button', { name: /set error/i })); - expect(await findByText(/some error/i)).toBeInTheDocument(); + expect(await findByText(/Some Error/i)).toBeInTheDocument(); const radios = getAllByRole('radio'); radios.forEach(radio => { diff --git a/packages/clerk-js/src/ui/hooks/__tests__/useCoreOrganizationList.test.tsx b/packages/clerk-js/src/ui/hooks/__tests__/useCoreOrganizationList.spec.tsx similarity index 98% rename from packages/clerk-js/src/ui/hooks/__tests__/useCoreOrganizationList.test.tsx rename to packages/clerk-js/src/ui/hooks/__tests__/useCoreOrganizationList.spec.tsx index 1f7a4a4137a..632858ab8c6 100644 --- a/packages/clerk-js/src/ui/hooks/__tests__/useCoreOrganizationList.test.tsx +++ b/packages/clerk-js/src/ui/hooks/__tests__/useCoreOrganizationList.spec.tsx @@ -1,13 +1,13 @@ import { useOrganizationList } from '@clerk/shared/react'; -import { describe } from '@jest/globals'; +import { describe, expect, it } from 'vitest'; -import { act, renderHook, waitFor } from '../../../testUtils'; +import { act, renderHook, waitFor } from '../../../vitestUtils'; import { createFakeUserOrganizationInvitation, createFakeUserOrganizationMembership, createFakeUserOrganizationSuggestion, -} from '../../components/OrganizationSwitcher/__tests__/utlis'; -import { bindCreateFixtures } from '../../utils/test/createFixtures'; +} from '../../components/OrganizationSwitcher/__tests__/vitestUtils'; +import { bindCreateFixtures } from '../../utils/vitest/createFixtures'; const { createFixtures } = bindCreateFixtures('OrganizationSwitcher'); diff --git a/packages/clerk-js/src/ui/hooks/__tests__/useDevMode.test.tsx b/packages/clerk-js/src/ui/hooks/__tests__/useDevMode.spec.tsx similarity index 93% rename from packages/clerk-js/src/ui/hooks/__tests__/useDevMode.test.tsx rename to packages/clerk-js/src/ui/hooks/__tests__/useDevMode.spec.tsx index 59db4893c46..cf932e78391 100644 --- a/packages/clerk-js/src/ui/hooks/__tests__/useDevMode.test.tsx +++ b/packages/clerk-js/src/ui/hooks/__tests__/useDevMode.spec.tsx @@ -1,20 +1,21 @@ import type { EnvironmentResource } from '@clerk/types'; import { renderHook } from '@testing-library/react'; +import { describe, expect, test, vi } from 'vitest'; import { useDevMode } from '../useDevMode'; -const mockUseEnvironment = jest.fn(); -const mockUseOptions = jest.fn(); -const mockUseAppearance = jest.fn(); +const mockUseEnvironment = vi.fn(); +const mockUseOptions = vi.fn(); +const mockUseAppearance = vi.fn(); -jest.mock('../../contexts', () => { +vi.mock('../../contexts', () => { return { useEnvironment: () => mockUseEnvironment(), useOptions: () => mockUseOptions(), }; }); -jest.mock('../../customizables', () => { +vi.mock('../../customizables', () => { return { useAppearance: () => mockUseAppearance(), }; diff --git a/packages/clerk-js/src/ui/hooks/__tests__/useSupportEmail.test.tsx b/packages/clerk-js/src/ui/hooks/__tests__/useSupportEmail.spec.tsx similarity index 88% rename from packages/clerk-js/src/ui/hooks/__tests__/useSupportEmail.test.tsx rename to packages/clerk-js/src/ui/hooks/__tests__/useSupportEmail.spec.tsx index 7f31c0dc218..8b342df0090 100644 --- a/packages/clerk-js/src/ui/hooks/__tests__/useSupportEmail.test.tsx +++ b/packages/clerk-js/src/ui/hooks/__tests__/useSupportEmail.spec.tsx @@ -1,16 +1,17 @@ import { renderHook } from '@testing-library/react'; +import { describe, expect, test, vi } from 'vitest'; import { useSupportEmail } from '../useSupportEmail'; -const mockUseOptions = jest.fn(); -const mockUseEnvironment = jest.fn(); +const mockUseOptions = vi.fn(); +const mockUseEnvironment = vi.fn(); -jest.mock('@clerk/shared/react', () => ({ +vi.mock('@clerk/shared/react', () => ({ useClerk: () => ({ publishableKey: 'pk_live_Y2xlcmsuY2xlcmsuY29tJA', }), })); -jest.mock('../../contexts', () => { +vi.mock('../../contexts', () => { return { useEnvironment: () => mockUseEnvironment(), useOptions: () => mockUseOptions(), diff --git a/packages/clerk-js/src/ui/localization/__tests__/applyTokensToString.test.ts b/packages/clerk-js/src/ui/localization/__tests__/applyTokensToString.spec.ts similarity index 97% rename from packages/clerk-js/src/ui/localization/__tests__/applyTokensToString.test.ts rename to packages/clerk-js/src/ui/localization/__tests__/applyTokensToString.spec.ts index ed9ff255d7e..49ec57a07c8 100644 --- a/packages/clerk-js/src/ui/localization/__tests__/applyTokensToString.test.ts +++ b/packages/clerk-js/src/ui/localization/__tests__/applyTokensToString.spec.ts @@ -1,3 +1,5 @@ +import { beforeEach, describe, expect, it, vi } from 'vitest'; + import { applyTokensToString } from '../applyTokensToString'; describe('applyTokensToString', function () { @@ -34,7 +36,7 @@ describe('applyTokensToString', function () { describe('Date related tokens and modifiers', () => { beforeEach(() => { - jest.spyOn(console, 'warn').mockImplementation(() => {}); + vi.spyOn(console, 'warn').mockImplementation(() => {}); }); const tokens = { diff --git a/packages/clerk-js/src/ui/router/__mocks__/RouteContext.tsx b/packages/clerk-js/src/ui/router/__mocks__/RouteContext.tsx index 7693d51fe48..35d50486d09 100644 --- a/packages/clerk-js/src/ui/router/__mocks__/RouteContext.tsx +++ b/packages/clerk-js/src/ui/router/__mocks__/RouteContext.tsx @@ -1,11 +1,12 @@ import { noop } from '@clerk/shared/utils'; +import { vi } from 'vitest'; export const useRouter = () => ({ - resolve: jest.fn(() => ({ + resolve: vi.fn(() => ({ toURL: { href: 'http://test.host/test-href', }, })), - matches: jest.fn(noop), - navigate: jest.fn(noop), + matches: vi.fn(noop), + navigate: vi.fn(noop), }); diff --git a/packages/clerk-js/src/ui/router/__tests__/PathRouter.test.tsx b/packages/clerk-js/src/ui/router/__tests__/PathRouter.spec.tsx similarity index 94% rename from packages/clerk-js/src/ui/router/__tests__/PathRouter.test.tsx rename to packages/clerk-js/src/ui/router/__tests__/PathRouter.spec.tsx index 52a2dff21a8..90ab5eae5d6 100644 --- a/packages/clerk-js/src/ui/router/__tests__/PathRouter.test.tsx +++ b/packages/clerk-js/src/ui/router/__tests__/PathRouter.spec.tsx @@ -1,13 +1,14 @@ import { render, screen, waitFor } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; import React from 'react'; +import { afterAll, beforeAll, beforeEach, describe, expect, it, vi } from 'vitest'; import type { Clerk } from '../../../core/clerk'; import { PathRouter, Route, useRouter } from '..'; -const mockNavigate = jest.fn(); +const mockNavigate = vi.fn(); -jest.mock('@clerk/shared/react', () => { +vi.mock('@clerk/shared/react', () => { return { useClerk: () => { return { diff --git a/packages/clerk-js/src/ui/router/__tests__/Switch.test.tsx b/packages/clerk-js/src/ui/router/__tests__/Switch.spec.tsx similarity index 94% rename from packages/clerk-js/src/ui/router/__tests__/Switch.test.tsx rename to packages/clerk-js/src/ui/router/__tests__/Switch.spec.tsx index c27bd019140..1ebcdd0a7e3 100644 --- a/packages/clerk-js/src/ui/router/__tests__/Switch.test.tsx +++ b/packages/clerk-js/src/ui/router/__tests__/Switch.spec.tsx @@ -1,13 +1,14 @@ import { render, screen } from '@testing-library/react'; import React from 'react'; +import { afterAll, afterEach, describe, expect, it, vi } from 'vitest'; import { HashRouter, Route, Switch } from '../../router'; -const mockNavigate = jest.fn(); +const mockNavigate = vi.fn(); -jest.mock('@clerk/shared/react', () => ({ +vi.mock('@clerk/shared/react', () => ({ useClerk: () => ({ - navigate: jest.fn(to => { + navigate: vi.fn(to => { mockNavigate(to); if (to) { // @ts-ignore @@ -29,7 +30,7 @@ const setWindowOrigin = (origin: string) => { describe('', () => { afterEach(() => { - jest.clearAllMocks(); + vi.clearAllMocks(); }); afterAll(() => { diff --git a/packages/clerk-js/src/ui/router/__tests__/VirtualRouter.test.tsx b/packages/clerk-js/src/ui/router/__tests__/VirtualRouter.spec.tsx similarity index 93% rename from packages/clerk-js/src/ui/router/__tests__/VirtualRouter.test.tsx rename to packages/clerk-js/src/ui/router/__tests__/VirtualRouter.spec.tsx index 18b7eef065f..a9ecc9649e2 100644 --- a/packages/clerk-js/src/ui/router/__tests__/VirtualRouter.test.tsx +++ b/packages/clerk-js/src/ui/router/__tests__/VirtualRouter.spec.tsx @@ -1,14 +1,15 @@ import { render, screen } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; import React from 'react'; +import { afterAll, beforeAll, beforeEach, describe, expect, it, vi } from 'vitest'; import { Route, useRouter, VirtualRouter } from '..'; -const mockNavigate = jest.fn(); +const mockNavigate = vi.fn(); -jest.mock('@clerk/shared/react', () => ({ +vi.mock('@clerk/shared/react', () => ({ useClerk: () => ({ - navigate: jest.fn(to => { + navigate: vi.fn(to => { mockNavigate(to); if (to) { // @ts-ignore diff --git a/packages/clerk-js/src/ui/utils/__tests__/normalizeColorString.test.ts b/packages/clerk-js/src/ui/utils/__tests__/normalizeColorString.spec.ts similarity index 91% rename from packages/clerk-js/src/ui/utils/__tests__/normalizeColorString.test.ts rename to packages/clerk-js/src/ui/utils/__tests__/normalizeColorString.spec.ts index 40ec028c83e..cc2ab505544 100644 --- a/packages/clerk-js/src/ui/utils/__tests__/normalizeColorString.test.ts +++ b/packages/clerk-js/src/ui/utils/__tests__/normalizeColorString.spec.ts @@ -1,12 +1,14 @@ +import { afterEach, beforeEach, describe, expect, test, vi } from 'vitest'; + import { normalizeColorString } from '../normalizeColorString'; describe('normalizeColorString', () => { beforeEach(() => { - jest.spyOn(console, 'warn').mockImplementation(() => {}) as jest.Mock; + vi.spyOn(console, 'warn').mockImplementation(() => {}) as vi.Mock; }); afterEach(() => { - jest.clearAllMocks(); + vi.clearAllMocks(); }); // Hex color tests @@ -34,7 +36,7 @@ describe('normalizeColorString', () => { expect(normalizeColorString('#12')).toBe('#12'); expect(console.warn).toHaveBeenCalledTimes(1); - (console.warn as jest.Mock).mockClear(); + (console.warn as vi.Mock).mockClear(); expect(normalizeColorString('#12345')).toBe('#12345'); expect(console.warn).toHaveBeenCalledTimes(1); }); @@ -78,11 +80,11 @@ describe('normalizeColorString', () => { expect(normalizeColorString('')).toBe(''); expect(console.warn).toHaveBeenCalledTimes(1); - (console.warn as jest.Mock).mockClear(); + (console.warn as vi.Mock).mockClear(); expect(normalizeColorString('invalid')).toBe('invalid'); expect(console.warn).toHaveBeenCalledTimes(1); - (console.warn as jest.Mock).mockClear(); + (console.warn as vi.Mock).mockClear(); expect(normalizeColorString('rgb(255,0)')).toBe('rgb(255,0)'); expect(console.warn).toHaveBeenCalledTimes(1); }); @@ -91,7 +93,7 @@ describe('normalizeColorString', () => { expect(normalizeColorString(null as any)).toBe(''); expect(console.warn).toHaveBeenCalledTimes(1); - (console.warn as jest.Mock).mockClear(); + (console.warn as vi.Mock).mockClear(); expect(normalizeColorString(123 as any)).toBe(123 as any); expect(console.warn).toHaveBeenCalledTimes(1); }); diff --git a/packages/clerk-js/src/ui/utils/test/createFixtures.tsx b/packages/clerk-js/src/ui/utils/test/createFixtures.tsx index 3fcda66ebd4..4187dfdda57 100644 --- a/packages/clerk-js/src/ui/utils/test/createFixtures.tsx +++ b/packages/clerk-js/src/ui/utils/test/createFixtures.tsx @@ -1,6 +1,5 @@ import type { ClerkOptions, ClientJSON, EnvironmentJSON, LoadedClerk } from '@clerk/types'; import { jest } from '@jest/globals'; -import React from 'react'; import { FlowMetadataProvider } from '@/ui/elements/contexts'; diff --git a/packages/clerk-js/src/ui/utils/vitest/createFixtures.tsx b/packages/clerk-js/src/ui/utils/vitest/createFixtures.tsx new file mode 100644 index 00000000000..897f65ca603 --- /dev/null +++ b/packages/clerk-js/src/ui/utils/vitest/createFixtures.tsx @@ -0,0 +1,131 @@ +// import { jest } from '@jest/globals'; +import type { ClerkOptions, ClientJSON, EnvironmentJSON, LoadedClerk } from '@clerk/types'; +import React from 'react'; +import { vi } from 'vitest'; + +import { FlowMetadataProvider } from '@/ui/elements/contexts'; + +import { Clerk as ClerkCtor } from '../../../core/clerk'; +import { Client, Environment } from '../../../core/resources'; +import { + ComponentContextProvider, + CoreClerkContextWrapper, + EnvironmentProvider, + OptionsProvider, +} from '../../contexts'; +import { AppearanceProvider } from '../../customizables'; +import { RouteContext } from '../../router'; +import { InternalThemeProvider } from '../../styledSystem'; +import type { AvailableComponentName, AvailableComponentProps } from '../../types'; +import { createClientFixtureHelpers, createEnvironmentFixtureHelpers } from './fixtureHelpers'; +import { createBaseClientJSON, createBaseEnvironmentJSON } from './fixtures'; +import { mockClerkMethods, mockRouteContextValue } from './mockHelpers'; + +const createInitialStateConfigParam = (baseEnvironment: EnvironmentJSON, baseClient: ClientJSON) => { + return { + ...createEnvironmentFixtureHelpers(baseEnvironment), + ...createClientFixtureHelpers(baseClient), + }; +}; + +type FParam = ReturnType; +type ConfigFn = (f: FParam) => void; + +export const bindCreateFixtures = ( + componentName: Parameters[0], + mockOpts?: { + router?: Parameters[0]; + }, +) => { + return { createFixtures: unboundCreateFixtures(componentName, mockOpts) }; +}; + +const unboundCreateFixtures = ( + componentName: AvailableComponentName, + mockOpts?: { + router?: Parameters[0]; + }, +) => { + const createFixtures = async (...configFns: ConfigFn[]) => { + const baseEnvironment = createBaseEnvironmentJSON(); + const baseClient = createBaseClientJSON(); + configFns = configFns.filter(Boolean); + + if (configFns.length) { + const fParam = createInitialStateConfigParam(baseEnvironment, baseClient); + configFns.forEach(configFn => configFn(fParam)); + } + + const environmentMock = new Environment(baseEnvironment); + Environment.getInstance().fetch = vi.fn(() => Promise.resolve(environmentMock)); + + // @ts-expect-error We cannot mess with the singleton when tests are running in parallel + const clientMock = new Client(baseClient); + Client.getOrCreateInstance().fetch = vi.fn(() => Promise.resolve(clientMock)); + + // Use a FAPI value for local production instances to avoid triggering the devInit flow during testing + const productionPublishableKey = 'pk_live_Y2xlcmsuYWJjZWYuMTIzNDUucHJvZC5sY2xjbGVyay5jb20k'; + const tempClerk = new ClerkCtor(productionPublishableKey); + await tempClerk.load(); + const clerkMock = mockClerkMethods(tempClerk as LoadedClerk); + const optionsMock = {} as ClerkOptions; + const routerMock = mockRouteContextValue(mockOpts?.router || {}); + + const fixtures = { + clerk: clerkMock, + session: clerkMock.session, + signIn: clerkMock.client.signIn, + signUp: clerkMock.client.signUp, + environment: environmentMock, + router: routerMock, + options: optionsMock, + }; + + let componentContextProps: AvailableComponentProps; + const props = { + setProps: (props: typeof componentContextProps) => { + componentContextProps = props; + }, + }; + + const MockClerkProvider = (props: any) => { + const { children } = props; + + const componentsWithoutContext = ['UsernameSection', 'UserProfileSection']; + const contextWrappedChildren = !componentsWithoutContext.includes(componentName) ? ( + + {children} + + ) : ( + <>{children} + ); + + return ( + new Map() }} + > + + + + + + {contextWrappedChildren} + + + + + + + ); + }; + + return { wrapper: MockClerkProvider, fixtures, props }; + }; + createFixtures.config = (fn: ConfigFn) => fn; + return createFixtures; +}; diff --git a/packages/clerk-js/src/ui/utils/vitest/fixtureHelpers.ts b/packages/clerk-js/src/ui/utils/vitest/fixtureHelpers.ts new file mode 100644 index 00000000000..567bf8ada6e --- /dev/null +++ b/packages/clerk-js/src/ui/utils/vitest/fixtureHelpers.ts @@ -0,0 +1,566 @@ +import type { + ClientJSON, + DisplayConfigJSON, + EmailAddressJSON, + EnvironmentJSON, + ExternalAccountJSON, + OAuthProvider, + OrganizationEnrollmentMode, + PhoneNumberJSON, + PublicUserDataJSON, + SamlAccountJSON, + SessionJSON, + SignInJSON, + SignUpJSON, + UserJSON, + UserSettingsJSON, + VerificationJSON, +} from '@clerk/types'; + +import { SIGN_UP_MODES } from '../../../core/constants'; +import type { OrgParams } from '../../../core/vitest/fixtures'; +import { createUser, getOrganizationId } from '../../../core/vitest/fixtures'; +import { createUserFixture } from './fixtures'; + +export const createEnvironmentFixtureHelpers = (baseEnvironment: EnvironmentJSON) => { + return { + ...createAuthConfigFixtureHelpers(baseEnvironment), + ...createDisplayConfigFixtureHelpers(baseEnvironment), + ...createOrganizationSettingsFixtureHelpers(baseEnvironment), + ...createUserSettingsFixtureHelpers(baseEnvironment), + }; +}; + +export const createClientFixtureHelpers = (baseClient: ClientJSON) => { + return { + ...createSignInFixtureHelpers(baseClient), + ...createSignUpFixtureHelpers(baseClient), + ...createUserFixtureHelpers(baseClient), + }; +}; + +const createUserFixtureHelpers = (baseClient: ClientJSON) => { + type WithUserParams = Omit< + Partial, + 'email_addresses' | 'phone_numbers' | 'external_accounts' | 'saml_accounts' | 'organization_memberships' + > & { + email_addresses?: Array>; + phone_numbers?: Array>; + external_accounts?: Array>; + saml_accounts?: Array>; + organization_memberships?: Array; + }; + + const createPublicUserData = (params: WithUserParams) => { + return { + first_name: 'FirstName', + last_name: 'LastName', + image_url: '', + identifier: 'email@test.com', + user_id: '', + ...params, + } as PublicUserDataJSON; + }; + + const withUser = (params: WithUserParams) => { + baseClient.sessions = baseClient.sessions || []; + + // set the first organization as active + let activeOrganization: string | null = null; + if (params?.organization_memberships?.length) { + activeOrganization = + typeof params.organization_memberships[0] === 'string' + ? params.organization_memberships[0] + : getOrganizationId(params.organization_memberships[0]); + } + + const session = { + status: 'active', + id: baseClient.sessions.length.toString(), + object: 'session', + last_active_organization_id: activeOrganization, + actor: null, + user: createUser(params), + public_user_data: createPublicUserData(params), + created_at: new Date().getTime(), + updated_at: new Date().getTime(), + last_active_token: { + jwt: 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE2NzU4NzY3OTAsImRhdGEiOiJmb29iYXIiLCJpYXQiOjE2NzU4NzY3MzB9.Z1BC47lImYvaAtluJlY-kBo0qOoAk42Xb-gNrB2SxJg', + }, + } as SessionJSON; + baseClient.sessions.push(session); + }; + + return { withUser }; +}; + +const createSignInFixtureHelpers = (baseClient: ClientJSON) => { + type SignInWithEmailAddressParams = { + identifier?: string; + supportPassword?: boolean; + supportEmailCode?: boolean; + supportEmailLink?: boolean; + supportResetPassword?: boolean; + supportPasskey?: boolean; + }; + + type SignInWithPhoneNumberParams = { + identifier?: string; + supportPassword?: boolean; + supportPhoneCode?: boolean; + supportResetPassword?: boolean; + }; + + type SignInFactorTwoParams = { + identifier?: string; + supportPhoneCode?: boolean; + supportTotp?: boolean; + supportBackupCode?: boolean; + supportResetPasswordEmail?: boolean; + supportResetPasswordPhone?: boolean; + }; + + const startSignInWithEmailAddress = (params?: SignInWithEmailAddressParams) => { + const { + identifier = 'hello@clerk.com', + supportPassword = true, + supportEmailCode, + supportEmailLink, + supportResetPassword, + supportPasskey, + } = params || {}; + baseClient.sign_in = { + status: 'needs_first_factor', + identifier, + supported_identifiers: ['email_address'], + supported_first_factors: [ + ...(supportPasskey ? [{ strategy: 'passkey' }] : []), + ...(supportPassword ? [{ strategy: 'password' }] : []), + ...(supportEmailCode ? [{ strategy: 'email_code', safe_identifier: identifier || 'n*****@clerk.com' }] : []), + ...(supportEmailLink ? [{ strategy: 'email_link', safe_identifier: identifier || 'n*****@clerk.com' }] : []), + ...(supportResetPassword + ? [ + { + strategy: 'reset_password_email_code', + safe_identifier: identifier || 'n*****@clerk.com', + emailAddressId: 'someEmailId', + }, + ] + : []), + ], + user_data: { ...(createUserFixture() as any) }, + } as SignInJSON; + }; + + const startSignInWithPhoneNumber = (params?: SignInWithPhoneNumberParams) => { + const { + identifier = '+301234567890', + supportPassword = true, + supportPhoneCode, + supportResetPassword, + } = params || {}; + baseClient.sign_in = { + status: 'needs_first_factor', + identifier, + supported_identifiers: ['phone_number'], + supported_first_factors: [ + ...(supportPassword ? [{ strategy: 'password' }] : []), + ...(supportPhoneCode ? [{ strategy: 'phone_code', safe_identifier: '+30********90' }] : []), + ...(supportResetPassword + ? [ + { + strategy: 'reset_password_phone_code', + safe_identifier: identifier || '+30********90', + phoneNumberId: 'someNumberId', + }, + ] + : []), + ], + user_data: { ...(createUserFixture() as any) }, + } as SignInJSON; + }; + + const startSignInFactorTwo = (params?: SignInFactorTwoParams) => { + const { + identifier = '+30 691 1111111', + supportPhoneCode = true, + supportTotp, + supportBackupCode, + supportResetPasswordEmail, + supportResetPasswordPhone, + } = params || {}; + baseClient.sign_in = { + status: 'needs_second_factor', + identifier, + ...(supportResetPasswordEmail + ? { + first_factor_verification: { + status: 'verified', + strategy: 'reset_password_email_code', + }, + } + : {}), + ...(supportResetPasswordPhone + ? { + first_factor_verification: { + status: 'verified', + strategy: 'reset_password_phone_code', + }, + } + : {}), + supported_identifiers: ['email_address', 'phone_number'], + supported_second_factors: [ + ...(supportPhoneCode ? [{ strategy: 'phone_code', safe_identifier: identifier || 'n*****@clerk.com' }] : []), + ...(supportTotp ? [{ strategy: 'totp', safe_identifier: identifier || 'n*****@clerk.com' }] : []), + ...(supportBackupCode ? [{ strategy: 'backup_code', safe_identifier: identifier || 'n*****@clerk.com' }] : []), + ], + user_data: { ...(createUserFixture() as any) }, + } as SignInJSON; + }; + + return { startSignInWithEmailAddress, startSignInWithPhoneNumber, startSignInFactorTwo }; +}; + +const createSignUpFixtureHelpers = (baseClient: ClientJSON) => { + type SignUpWithEmailAddressParams = { + emailAddress?: string; + supportEmailCode?: boolean; + supportEmailLink?: boolean; + emailVerificationStatus?: VerificationJSON['status']; + }; + + type SignUpWithPhoneNumberParams = { + phoneNumber?: string; + }; + + const startSignUpWithEmailAddress = (params?: SignUpWithEmailAddressParams) => { + const { + emailAddress = 'hello@clerk.com', + supportEmailLink = true, + supportEmailCode = true, + emailVerificationStatus = 'unverified', + } = params || {}; + baseClient.sign_up = { + id: 'sua_2HseAXFGN12eqlwARPMxyyUa9o9', + status: 'missing_requirements', + email_address: emailAddress, + verifications: (supportEmailLink || supportEmailCode) && { + email_address: { + strategy: (supportEmailLink && 'email_link') || (supportEmailCode && 'email_code'), + status: emailVerificationStatus, + }, + }, + missing_fields: [], + unverified_fields: emailVerificationStatus === 'unverified' ? ['email_address'] : [], + } as SignUpJSON; + }; + + const startSignUpWithPhoneNumber = (params?: SignUpWithPhoneNumberParams) => { + const { phoneNumber = '+301234567890' } = params || {}; + baseClient.sign_up = { + id: 'sua_2HseAXFGN12eqlwARPMxyyUa9o9', + status: 'missing_requirements', + phone_number: phoneNumber, + } as SignUpJSON; + }; + + const startSignUpWithMissingLegalAccepted = () => { + baseClient.sign_up = { + id: 'sua_2HseAXFGN12eqlwARPMxyyUa9o9', + status: 'missing_requirements', + legal_accepted_at: null, + missing_fields: ['legal_accepted'], + } as SignUpJSON; + }; + + const startSignUpWithMissingLegalAcceptedAndUnverifiedFields = (emailAddress = 'hello@clerk.com') => { + baseClient.sign_up = { + id: 'sua_2HseAXFGN12eqlwARPMxyyUa9o9', + status: 'missing_requirements', + legal_accepted_at: null, + missing_fields: ['legal_accepted'], + email_address: emailAddress, + unverified_fields: ['email_address'], + } as SignUpJSON; + }; + + return { + startSignUpWithEmailAddress, + startSignUpWithPhoneNumber, + startSignUpWithMissingLegalAccepted, + startSignUpWithMissingLegalAcceptedAndUnverifiedFields, + }; +}; + +const createAuthConfigFixtureHelpers = (environment: EnvironmentJSON) => { + const ac = environment.auth_config; + const withMultiSessionMode = () => { + // TODO: + ac.single_session_mode = false; + }; + const withReverification = () => { + ac.reverification = true; + }; + return { withMultiSessionMode, withReverification }; +}; + +const createDisplayConfigFixtureHelpers = (environment: EnvironmentJSON) => { + const dc = environment.display_config; + const withSupportEmail = (opts?: { email: string }) => { + dc.support_email = opts?.email || 'support@clerk.com'; + }; + const withoutClerkBranding = () => { + dc.branded = false; + }; + const withPreferredSignInStrategy = (opts: { strategy: DisplayConfigJSON['preferred_sign_in_strategy'] }) => { + dc.preferred_sign_in_strategy = opts.strategy; + }; + + const withTermsPrivacyPolicyUrls = (opts: { + termsOfService?: DisplayConfigJSON['terms_url']; + privacyPolicy?: DisplayConfigJSON['privacy_policy_url']; + }) => { + dc.terms_url = opts.termsOfService || ''; + dc.privacy_policy_url = opts.privacyPolicy || ''; + }; + return { withSupportEmail, withoutClerkBranding, withPreferredSignInStrategy, withTermsPrivacyPolicyUrls }; +}; + +const createOrganizationSettingsFixtureHelpers = (environment: EnvironmentJSON) => { + const os = environment.organization_settings; + const withOrganizations = () => { + os.enabled = true; + }; + const withMaxAllowedMemberships = ({ max = 5 }) => { + os.max_allowed_memberships = max; + }; + const withForceOrganizationSelection = () => { + os.force_organization_selection = true; + }; + + const withOrganizationDomains = (modes?: OrganizationEnrollmentMode[], defaultRole?: string) => { + os.domains.enabled = true; + os.domains.enrollment_modes = modes || ['automatic_invitation', 'manual_invitation']; + os.domains.default_role = defaultRole ?? null; + }; + return { withOrganizations, withMaxAllowedMemberships, withOrganizationDomains, withForceOrganizationSelection }; +}; + +const createUserSettingsFixtureHelpers = (environment: EnvironmentJSON) => { + const us = environment.user_settings; + us.password_settings = { + allowed_special_characters: '', + disable_hibp: false, + min_length: 8, + max_length: 999, + require_special_char: false, + require_numbers: false, + require_uppercase: false, + require_lowercase: false, + show_zxcvbn: false, + min_zxcvbn_strength: 0, + }; + us.sign_up = { + ...us.sign_up, + mode: SIGN_UP_MODES.PUBLIC, + }; + + us.username_settings = { + min_length: 4, + max_length: 40, + }; + + const emptyAttribute = { + first_factors: [], + second_factors: [], + verifications: [], + used_for_first_factor: false, + used_for_second_factor: false, + verify_at_sign_up: false, + }; + + const withPasswordComplexity = (opts?: Partial) => { + us.password_settings = { + ...us.password_settings, + ...opts, + }; + }; + + const withEmailAddress = (opts?: Partial) => { + us.attributes.email_address = { + ...emptyAttribute, + enabled: true, + required: false, + used_for_first_factor: true, + first_factors: ['email_code'], + used_for_second_factor: false, + second_factors: [], + verifications: ['email_code'], + verify_at_sign_up: true, + ...opts, + }; + }; + + const withEmailLink = () => { + withEmailAddress({ first_factors: ['email_link'], verifications: ['email_link'] }); + }; + + const withPhoneNumber = (opts?: Partial) => { + us.attributes.phone_number = { + ...emptyAttribute, + enabled: true, + required: false, + used_for_first_factor: true, + first_factors: ['phone_code'], + used_for_second_factor: false, + second_factors: [], + verifications: ['phone_code'], + verify_at_sign_up: true, + ...opts, + }; + }; + + const withPasskey = (opts?: Partial) => { + us.attributes.passkey = { + ...emptyAttribute, + enabled: true, + required: false, + used_for_first_factor: true, + first_factors: ['passkey'], + used_for_second_factor: false, + second_factors: [], + verifications: ['passkey'], + verify_at_sign_up: false, + ...opts, + }; + }; + + const withPasskeySettings = (opts?: Partial) => { + us.passkey_settings = { + ...us.passkey_settings, + ...opts, + }; + }; + + const withUsername = (opts?: Partial) => { + us.attributes.username = { + ...emptyAttribute, + enabled: true, + required: false, + used_for_first_factor: true, + ...opts, + }; + }; + + const withWeb3Wallet = (opts?: Partial) => { + us.attributes.web3_wallet = { + ...emptyAttribute, + enabled: true, + required: false, + used_for_first_factor: true, + first_factors: ['web3_metamask_signature'], + verifications: ['web3_metamask_signature'], + ...opts, + }; + }; + + const withName = (opts?: Partial) => { + const attr = { + ...emptyAttribute, + enabled: true, + required: false, + ...opts, + }; + us.attributes.first_name = attr; + us.attributes.last_name = attr; + }; + + const withPassword = (opts?: Partial) => { + us.attributes.password = { + ...emptyAttribute, + enabled: true, + required: false, + ...opts, + }; + }; + + const withSocialProvider = (opts: { provider: OAuthProvider; authenticatable?: boolean }) => { + const { authenticatable = true, provider } = opts || {}; + const strategy = 'oauth_' + provider; + // @ts-expect-error + us.social[strategy] = { + enabled: true, + authenticatable, + strategy: strategy, + }; + }; + + const withEnterpriseSso = () => { + us.saml = { enabled: true }; + us.enterprise_sso = { enabled: true }; + }; + + const withBackupCode = (opts?: Partial) => { + us.attributes.backup_code = { + ...emptyAttribute, + enabled: true, + required: false, + used_for_first_factor: false, + first_factors: [], + used_for_second_factor: true, + second_factors: ['backup_code'], + verifications: [], + verify_at_sign_up: false, + ...opts, + }; + }; + + const withAuthenticatorApp = (opts?: Partial) => { + us.attributes.authenticator_app = { + ...emptyAttribute, + enabled: false, + required: false, + used_for_first_factor: false, + first_factors: [], + used_for_second_factor: true, + second_factors: ['totp'], + verifications: [], + verify_at_sign_up: false, + ...opts, + }; + }; + + const withRestrictedMode = () => { + us.sign_up.mode = SIGN_UP_MODES.RESTRICTED; + }; + + const withLegalConsent = () => { + us.sign_up.legal_consent_enabled = true; + }; + + const withWaitlistMode = () => { + us.sign_up.mode = SIGN_UP_MODES.WAITLIST; + }; + + // TODO: Add the rest, consult pkg/generate/auth_config.go + + return { + withEmailAddress, + withEmailLink, + withPhoneNumber, + withUsername, + withWeb3Wallet, + withName, + withPassword, + withPasswordComplexity, + withSocialProvider, + withEnterpriseSso, + withBackupCode, + withAuthenticatorApp, + withPasskey, + withPasskeySettings, + withRestrictedMode, + withLegalConsent, + withWaitlistMode, + }; +}; diff --git a/packages/clerk-js/src/ui/utils/vitest/fixtures.ts b/packages/clerk-js/src/ui/utils/vitest/fixtures.ts new file mode 100644 index 00000000000..40858b78c88 --- /dev/null +++ b/packages/clerk-js/src/ui/utils/vitest/fixtures.ts @@ -0,0 +1,228 @@ +import type { + AuthConfigJSON, + ClientJSON, + DisplayConfigJSON, + EnvironmentJSON, + OrganizationSettingsJSON, + UserJSON, + UserSettingsJSON, +} from '@clerk/types'; + +import { containsAllOfType } from '../containsAllOf'; + +export const createBaseEnvironmentJSON = (): EnvironmentJSON => { + return { + id: 'env_1', + object: 'environment', + auth_config: createBaseAuthConfig(), + display_config: createBaseDisplayConfig(), + organization_settings: createBaseOrganizationSettings(), + user_settings: createBaseUserSettings(), + meta: { responseHeaders: { country: 'us' } }, + }; +}; + +const createBaseAuthConfig = (): AuthConfigJSON => { + return { + object: 'auth_config', + id: 'aac_1', + single_session_mode: true, + }; +}; + +const createBaseDisplayConfig = (): DisplayConfigJSON => { + return { + object: 'display_config', + id: 'display_config_1', + instance_environment_type: 'production', + application_name: 'TestApp', + theme: { + buttons: { + font_color: '#ffffff', + font_family: '"Inter", sans-serif', + font_weight: '600', + }, + general: { + color: '#6c47ff', + padding: '1em', + box_shadow: '0 2px 8px rgba(0, 0, 0, 0.2)', + font_color: '#151515', + font_family: '"Inter", sans-serif', + border_radius: '0.5em', + background_color: '#ffffff', + label_font_weight: '600', + }, + accounts: { + background_color: '#f2f2f2', + }, + }, + preferred_sign_in_strategy: 'password', + logo_image_url: 'https://images.clerk.com/uploaded/img_logo.png', + favicon_image_url: 'https://images.clerk.com/uploaded/img_favicon.png', + home_url: 'https://dashboard.clerk.com', + sign_in_url: 'https://dashboard.clerk.com/sign-in', + sign_up_url: 'https://dashboard.clerk.com/sign-up', + user_profile_url: 'https://accounts.clerk.com/user', + after_sign_in_url: 'https://dashboard.clerk.com', + after_sign_up_url: 'https://dashboard.clerk.com', + after_sign_out_one_url: 'https://accounts.clerk.com/sign-in/choose', + after_sign_out_all_url: 'https://dashboard.clerk.com/sign-in', + after_switch_session_url: 'https://dashboard.clerk.com', + organization_profile_url: 'https://accounts.clerk.com/organization', + create_organization_url: 'https://accounts.clerk.com/create-organization', + after_leave_organization_url: 'https://dashboard.clerk.com', + after_create_organization_url: 'https://dashboard.clerk.com', + support_email: '', + branded: true, + clerk_js_version: '4', + }; +}; + +const createBaseOrganizationSettings = (): OrganizationSettingsJSON => { + return { + enabled: false, + max_allowed_memberships: 5, + force_organization_selection: false, + domains: { + enabled: false, + enrollment_modes: [], + }, + } as unknown as OrganizationSettingsJSON; +}; + +const attributes = Object.freeze( + containsAllOfType()([ + 'email_address', + 'phone_number', + 'username', + 'web3_wallet', + 'first_name', + 'last_name', + 'password', + 'authenticator_app', + 'backup_code', + 'passkey', + ]), +); + +const socials = Object.freeze( + containsAllOfType()([ + 'oauth_facebook', + 'oauth_google', + 'oauth_hubspot', + 'oauth_github', + 'oauth_tiktok', + 'oauth_gitlab', + 'oauth_discord', + 'oauth_twitter', + 'oauth_twitch', + 'oauth_linkedin', + 'oauth_linkedin_oidc', + 'oauth_dropbox', + 'oauth_atlassian', + 'oauth_bitbucket', + 'oauth_microsoft', + 'oauth_notion', + 'oauth_apple', + 'oauth_line', + 'oauth_instagram', + 'oauth_coinbase', + 'oauth_spotify', + 'oauth_xero', + 'oauth_box', + 'oauth_slack', + 'oauth_linear', + 'oauth_x', + ]), +); + +const createBaseUserSettings = (): UserSettingsJSON => { + const attributeConfig = Object.fromEntries( + attributes.map(attribute => [ + attribute, + { + enabled: false, + required: false, + used_for_first_factor: false, + first_factors: [], + used_for_second_factor: false, + second_factors: [], + verifications: [], + verify_at_sign_up: false, + }, + ]), + ) as UserSettingsJSON['attributes']; + + const socialConfig: UserSettingsJSON['social'] = Object.fromEntries( + socials.map(social => [ + social, + { + enabled: false, + required: false, + authenticatable: false, + strategy: social, + }, + ]), + ); + + const passwordSettingsConfig = { + allowed_special_characters: '', + max_length: 0, + min_length: 8, + require_special_char: false, + require_numbers: false, + require_lowercase: false, + require_uppercase: false, + disable_hibp: true, + show_zxcvbn: false, + min_zxcvbn_strength: 0, + } as UserSettingsJSON['password_settings']; + + const passkeySettingsConfig = { + allow_autofill: false, + show_sign_in_button: false, + } as UserSettingsJSON['passkey_settings']; + + return { + attributes: { ...attributeConfig }, + actions: { delete_self: false, create_organization: false }, + social: { ...socialConfig }, + saml: { enabled: false }, + enterprise_sso: { enabled: false }, + sign_in: { + second_factor: { + required: false, + }, + }, + sign_up: { + custom_action_required: false, + progressive: true, + captcha_enabled: false, + disable_hibp: false, + mode: 'public', + }, + restrictions: { + allowlist: { + enabled: false, + }, + blocklist: { + enabled: false, + }, + }, + password_settings: passwordSettingsConfig, + passkey_settings: passkeySettingsConfig, + }; +}; + +export const createBaseClientJSON = (): ClientJSON => { + return {} as ClientJSON; +}; + +export const createUserFixture = (): UserJSON => { + return { + first_name: 'Firstname', + last_name: 'Lastname', + image_url: + 'https://img.clerk.com/eyJ0eXBlIjoicHJveHkiLCJzcmMiOiJodHRwczovL2xoMy5nb29nbGV1c2VyY29udGVudC5jb20vYS9BQ2c4b2NLTmR2TUtFQzN5cUVpMVFjV0UzQjExbF9WUEVOWW5manlLMlVQd0tCSWw9czEwMDAtYyIsInMiOiJkRkowS3dTSkRINndiODE5cXJTUUxxaWF1ZS9QcHdndC84L0lUUlpYNHpnIn0?width=160', + } as UserJSON; +}; diff --git a/packages/clerk-js/src/ui/utils/vitest/mockHelpers.ts b/packages/clerk-js/src/ui/utils/vitest/mockHelpers.ts new file mode 100644 index 00000000000..8be7b9ae20d --- /dev/null +++ b/packages/clerk-js/src/ui/utils/vitest/mockHelpers.ts @@ -0,0 +1,103 @@ +// import { jest } from '@jest/globals'; +import type { LoadedClerk } from '@clerk/types'; +import type { ActiveSessionResource } from '@clerk/types'; +import { type Mocked, vi } from 'vitest'; + +import type { RouteContextValue } from '../../router'; + +type FunctionLike = (...args: any) => any; + +type DeepVitestMocked = T extends FunctionLike + ? Mocked + : T extends object + ? { + [k in keyof T]: DeepVitestMocked; + } + : T; + +// Removing jest.Mock type for now, relying on inference +type MockMap = { + [K in { [K in keyof T]: T[K] extends (...args: any[]) => any ? K : never }[keyof T]]?: ReturnType; +}; + +const mockProp = (obj: T, k: keyof T, mocks?: MockMap) => { + if (typeof obj[k] === 'function') { + const mockFn = mocks?.[k] ?? vi.fn(); + (obj[k] as unknown as ReturnType) = mockFn; + } +}; + +const mockMethodsOf = | null = any>( + obj: T, + options?: { + exclude: (keyof T)[]; + mocks: MockMap; + }, +) => { + if (!obj) { + return; + } + Object.keys(obj) + .filter(key => !options?.exclude.includes(key as keyof T)) + // Pass the specific MockMap for the object T being mocked + .forEach(k => mockProp(obj, k, options?.mocks)); +}; + +export const mockClerkMethods = (clerk: LoadedClerk): DeepVitestMocked => { + // Cast clerk to any to allow mocking properties + const clerkAny = clerk as any; + + mockMethodsOf(clerkAny); + if (clerkAny.client) { + mockMethodsOf(clerkAny.client.signIn); + mockMethodsOf(clerkAny.client.signUp); + clerkAny.client.sessions?.forEach((session: ActiveSessionResource) => { + const sessionAny = session as any; + mockMethodsOf(sessionAny, { + exclude: ['checkAuthorization'], + mocks: { + // Ensure touch mock matches expected signature if available, otherwise basic mock + touch: vi.fn(() => Promise.resolve(session)), + }, + }); + if (sessionAny.user) { + mockMethodsOf(sessionAny.user); + sessionAny.user.emailAddresses?.forEach((m: any) => mockMethodsOf(m)); + sessionAny.user.phoneNumbers?.forEach((m: any) => mockMethodsOf(m)); + sessionAny.user.externalAccounts?.forEach((m: any) => mockMethodsOf(m)); + sessionAny.user.organizationMemberships?.forEach((m: any) => { + mockMethodsOf(m); + if (m.organization) { + mockMethodsOf(m.organization); + } + }); + sessionAny.user.passkeys?.forEach((m: any) => mockMethodsOf(m)); + } + }); + } + mockProp(clerkAny, 'navigate'); + mockProp(clerkAny, 'setActive'); + mockProp(clerkAny, 'redirectWithAuth'); + mockProp(clerkAny, '__internal_navigateWithError'); + return clerkAny as DeepVitestMocked; +}; + +export const mockRouteContextValue = ({ queryString = '' }: Partial>) => { + return { + basePath: '', + startPath: '', + flowStartPath: '', + fullPath: '', + indexPath: '', + currentPath: '', + queryString, + queryParams: {}, + getMatchData: vi.fn(), + matches: vi.fn(), + baseNavigate: vi.fn(), + navigate: vi.fn(() => Promise.resolve(true)), + resolve: vi.fn((to: string) => new URL(to, 'https://clerk.com')), + refresh: vi.fn(), + params: {}, + } as RouteContextValue; // Keep original type assertion, DeepVitestMocked applied to input only +}; diff --git a/packages/clerk-js/src/ui/utils/vitest/runFakeTimers.ts b/packages/clerk-js/src/ui/utils/vitest/runFakeTimers.ts new file mode 100644 index 00000000000..e5e2365184d --- /dev/null +++ b/packages/clerk-js/src/ui/utils/vitest/runFakeTimers.ts @@ -0,0 +1,36 @@ +// import { jest } from '@jest/globals'; +import { act } from '@testing-library/react'; +import { vi } from 'vitest'; + +type WithAct = (fn: T) => T; +const withAct = ((fn: any) => + (...args: any) => { + act(() => { + fn(...args); + }); + }) as WithAct; + +const advanceTimersByTime = withAct(vi.advanceTimersByTime.bind(vi)); +const runAllTimers = withAct(vi.runAllTimers.bind(vi)); +const runOnlyPendingTimers = withAct(vi.runOnlyPendingTimers.bind(vi)); + +const createFakeTimersHelpers = () => { + return { advanceTimersByTime, runAllTimers, runOnlyPendingTimers }; +}; + +type FakeTimersHelpers = ReturnType; +type RunFakeTimersCallback = (timers: FakeTimersHelpers) => void | Promise; + +export async function runFakeTimers(cb: (timers: FakeTimersHelpers) => void): Promise; +export async function runFakeTimers(cb: (timers: FakeTimersHelpers) => Promise): Promise; +export async function runFakeTimers(cb: RunFakeTimersCallback): Promise { + vi.useFakeTimers(); + try { + const result = cb(createFakeTimersHelpers()); + if (result instanceof Promise) { + await result; + } + } finally { + vi.useRealTimers(); + } +} diff --git a/packages/clerk-js/src/utils/__tests__/captcha.test.ts b/packages/clerk-js/src/utils/__tests__/captcha.spec.ts similarity index 92% rename from packages/clerk-js/src/utils/__tests__/captcha.test.ts rename to packages/clerk-js/src/utils/__tests__/captcha.spec.ts index 0b64aaee20b..63cb871f1a0 100644 --- a/packages/clerk-js/src/utils/__tests__/captcha.test.ts +++ b/packages/clerk-js/src/utils/__tests__/captcha.spec.ts @@ -1,3 +1,5 @@ +import { describe, expect, it } from 'vitest'; + import { shouldRetryTurnstileErrorCode } from '../captcha/turnstile'; describe('shouldRetryTurnstileErrorCode', () => { diff --git a/packages/clerk-js/src/utils/__tests__/completeSignUpFlow.test.ts b/packages/clerk-js/src/utils/__tests__/completeSignUpFlow.spec.ts similarity index 98% rename from packages/clerk-js/src/utils/__tests__/completeSignUpFlow.test.ts rename to packages/clerk-js/src/utils/__tests__/completeSignUpFlow.spec.ts index 8c82f685d0b..032e1f33416 100644 --- a/packages/clerk-js/src/utils/__tests__/completeSignUpFlow.test.ts +++ b/packages/clerk-js/src/utils/__tests__/completeSignUpFlow.spec.ts @@ -1,10 +1,11 @@ import type { SignUpField, SignUpResource } from '@clerk/types'; +import { beforeEach, describe, expect, it, vi } from 'vitest'; import { completeSignUpFlow } from '../completeSignUpFlow'; -const mockHandleComplete = jest.fn(); -const mockNavigate = jest.fn(); -const mockAuthenticateWithRedirect = jest.fn(); +const mockHandleComplete = vi.fn(); +const mockNavigate = vi.fn(); +const mockAuthenticateWithRedirect = vi.fn(); describe('completeSignUpFlow', () => { beforeEach(() => { diff --git a/packages/clerk-js/src/utils/__tests__/date.test.ts b/packages/clerk-js/src/utils/__tests__/date.spec.ts similarity index 82% rename from packages/clerk-js/src/utils/__tests__/date.test.ts rename to packages/clerk-js/src/utils/__tests__/date.spec.ts index dfb60addb73..3c1ff3a3ca7 100644 --- a/packages/clerk-js/src/utils/__tests__/date.test.ts +++ b/packages/clerk-js/src/utils/__tests__/date.spec.ts @@ -1,13 +1,15 @@ +import { afterAll, beforeAll, describe, expect, it, vi } from 'vitest'; + import { unixEpochToDate } from '../date'; describe('date utilities', () => { beforeAll(() => { - jest.useFakeTimers('modern'); - jest.setSystemTime(); + vi.useFakeTimers(); + vi.setSystemTime(Date.now()); }); afterAll(() => { - jest.useRealTimers(); + vi.useRealTimers(); }); describe('unixEpochToDate', () => { diff --git a/packages/clerk-js/src/utils/__tests__/dynamicParamParser.test.ts b/packages/clerk-js/src/utils/__tests__/dynamicParamParser.spec.ts similarity index 94% rename from packages/clerk-js/src/utils/__tests__/dynamicParamParser.test.ts rename to packages/clerk-js/src/utils/__tests__/dynamicParamParser.spec.ts index 3f9ceafd853..712de7756a4 100644 --- a/packages/clerk-js/src/utils/__tests__/dynamicParamParser.test.ts +++ b/packages/clerk-js/src/utils/__tests__/dynamicParamParser.spec.ts @@ -1,4 +1,4 @@ -import { describe } from '@jest/globals'; +import { describe, expect, it } from 'vitest'; import { createDynamicParamParser } from '../dynamicParamParser'; diff --git a/packages/clerk-js/src/utils/__tests__/errors.test.ts b/packages/clerk-js/src/utils/__tests__/errors.spec.ts similarity index 91% rename from packages/clerk-js/src/utils/__tests__/errors.test.ts rename to packages/clerk-js/src/utils/__tests__/errors.spec.ts index 9de84ae2216..f7c3477f5b8 100644 --- a/packages/clerk-js/src/utils/__tests__/errors.test.ts +++ b/packages/clerk-js/src/utils/__tests__/errors.spec.ts @@ -1,3 +1,5 @@ +import { describe, expect, it } from 'vitest'; + import type { ClerkAPIResponseError } from '../../core/resources/Error'; import { isError } from '../errors'; diff --git a/packages/clerk-js/src/utils/__tests__/ignoreEventValue.test.ts b/packages/clerk-js/src/utils/__tests__/ignoreEventValue.spec.ts similarity index 97% rename from packages/clerk-js/src/utils/__tests__/ignoreEventValue.test.ts rename to packages/clerk-js/src/utils/__tests__/ignoreEventValue.spec.ts index 0968e8c5101..3ed749b54b1 100644 --- a/packages/clerk-js/src/utils/__tests__/ignoreEventValue.test.ts +++ b/packages/clerk-js/src/utils/__tests__/ignoreEventValue.spec.ts @@ -1,3 +1,5 @@ +import { describe, expect, it } from 'vitest'; + import { ignoreEventValue } from '../ignoreEventValue'; const noop = (..._args: any[]): void => { diff --git a/packages/clerk-js/src/utils/__tests__/instance.test.ts b/packages/clerk-js/src/utils/__tests__/instance.spec.ts similarity index 95% rename from packages/clerk-js/src/utils/__tests__/instance.test.ts rename to packages/clerk-js/src/utils/__tests__/instance.spec.ts index f0e4c1cbcef..b73e8c68d0b 100644 --- a/packages/clerk-js/src/utils/__tests__/instance.test.ts +++ b/packages/clerk-js/src/utils/__tests__/instance.spec.ts @@ -1,3 +1,5 @@ +import { describe, expect, it } from 'vitest'; + import { validateFrontendApi } from '../instance'; describe('validateFrontendApi(str)', () => { diff --git a/packages/clerk-js/src/utils/__tests__/jwt.test.ts b/packages/clerk-js/src/utils/__tests__/jwt.spec.ts similarity index 94% rename from packages/clerk-js/src/utils/__tests__/jwt.test.ts rename to packages/clerk-js/src/utils/__tests__/jwt.spec.ts index f7f5e564b1a..3d58ea4f19f 100644 --- a/packages/clerk-js/src/utils/__tests__/jwt.test.ts +++ b/packages/clerk-js/src/utils/__tests__/jwt.spec.ts @@ -1,3 +1,5 @@ +import { describe, expect, it } from 'vitest'; + import { decode } from '../jwt'; const jwt = diff --git a/packages/clerk-js/src/utils/__tests__/localStorage.test.ts b/packages/clerk-js/src/utils/__tests__/localStorage.spec.ts similarity index 95% rename from packages/clerk-js/src/utils/__tests__/localStorage.test.ts rename to packages/clerk-js/src/utils/__tests__/localStorage.spec.ts index 792f0acfa7f..64ad8dd0e2f 100644 --- a/packages/clerk-js/src/utils/__tests__/localStorage.test.ts +++ b/packages/clerk-js/src/utils/__tests__/localStorage.spec.ts @@ -1,3 +1,5 @@ +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; + import { SafeLocalStorage } from '../localStorage'; describe('SafeLocalStorage', () => { @@ -26,7 +28,7 @@ describe('SafeLocalStorage', () => { afterEach(() => { mockStorage = {}; - jest.restoreAllMocks(); + vi.restoreAllMocks(); }); describe('setItem', () => { @@ -53,13 +55,13 @@ describe('SafeLocalStorage', () => { }); it('sets expiration when provided', () => { - jest.useFakeTimers(); + vi.useFakeTimers(); const now = Date.now(); SafeLocalStorage.setItem('test', 'value', 1000); const stored = JSON.parse(mockStorage['__clerk_test']); expect(stored.exp).toBe(now + 1000); - jest.useRealTimers(); + vi.useRealTimers(); }); it('stores complex objects correctly', () => { @@ -105,15 +107,15 @@ describe('SafeLocalStorage', () => { }); it('returns default value and removes item when expired', () => { - jest.useFakeTimers(); + vi.useFakeTimers(); SafeLocalStorage.setItem('test', 'value', 1_000); // Advance time beyond expiration - jest.advanceTimersByTime(1_001); + vi.advanceTimersByTime(1_001); expect(SafeLocalStorage.getItem('test', 'default')).toBe('default'); expect(mockStorage['__clerk_test']).toBeUndefined(); - jest.useRealTimers(); + vi.useRealTimers(); }); it('handles malformed JSON data by returning default value', () => { diff --git a/packages/clerk-js/src/utils/__tests__/memoizeStateListenerCallback.test.ts b/packages/clerk-js/src/utils/__tests__/memoizeStateListenerCallback.spec.ts similarity index 96% rename from packages/clerk-js/src/utils/__tests__/memoizeStateListenerCallback.test.ts rename to packages/clerk-js/src/utils/__tests__/memoizeStateListenerCallback.spec.ts index b553ae25003..1be794dde9f 100644 --- a/packages/clerk-js/src/utils/__tests__/memoizeStateListenerCallback.test.ts +++ b/packages/clerk-js/src/utils/__tests__/memoizeStateListenerCallback.spec.ts @@ -1,5 +1,7 @@ // TODO: jest fails because of a circular dependency on Client -> Base -> Client // This circular dep is a known issue we plan to address soon. Enable the tests then +import { describe, it } from 'vitest'; + describe.skip('memoizeStateListenerCallback', () => { it.skip('runs', () => { // TODO diff --git a/packages/clerk-js/src/utils/__tests__/organization.test.ts b/packages/clerk-js/src/utils/__tests__/organization.spec.ts similarity index 93% rename from packages/clerk-js/src/utils/__tests__/organization.test.ts rename to packages/clerk-js/src/utils/__tests__/organization.spec.ts index 4262f073372..b722ef4b27b 100644 --- a/packages/clerk-js/src/utils/__tests__/organization.test.ts +++ b/packages/clerk-js/src/utils/__tests__/organization.spec.ts @@ -1,3 +1,5 @@ +import { describe, expect, it } from 'vitest'; + import { isOrganizationId } from '../organization'; describe('isOrganizationId(string)', () => { diff --git a/packages/clerk-js/src/utils/__tests__/passkeys.test.ts b/packages/clerk-js/src/utils/__tests__/passkeys.spec.ts similarity index 99% rename from packages/clerk-js/src/utils/__tests__/passkeys.test.ts rename to packages/clerk-js/src/utils/__tests__/passkeys.spec.ts index c78c0f40b49..16516d1e745 100644 --- a/packages/clerk-js/src/utils/__tests__/passkeys.test.ts +++ b/packages/clerk-js/src/utils/__tests__/passkeys.spec.ts @@ -4,6 +4,7 @@ import type { PublicKeyCredentialWithAuthenticatorAssertionResponse, PublicKeyCredentialWithAuthenticatorAttestationResponse, } from '@clerk/types'; +import { describe, expect, it } from 'vitest'; import { bufferToBase64Url, diff --git a/packages/clerk-js/src/utils/__tests__/path.test.ts b/packages/clerk-js/src/utils/__tests__/path.spec.ts similarity index 91% rename from packages/clerk-js/src/utils/__tests__/path.test.ts rename to packages/clerk-js/src/utils/__tests__/path.spec.ts index fd1ae1c2ac2..97686eb9553 100644 --- a/packages/clerk-js/src/utils/__tests__/path.test.ts +++ b/packages/clerk-js/src/utils/__tests__/path.spec.ts @@ -1,3 +1,5 @@ +import { describe, expect, it } from 'vitest'; + import { joinPaths } from '../path'; describe('joinPaths(a, b)', () => { diff --git a/packages/clerk-js/src/utils/__tests__/queryStateParams.test.ts b/packages/clerk-js/src/utils/__tests__/queryStateParams.spec.ts similarity index 95% rename from packages/clerk-js/src/utils/__tests__/queryStateParams.test.ts rename to packages/clerk-js/src/utils/__tests__/queryStateParams.spec.ts index e19a7b5fb2d..9a5d44292eb 100644 --- a/packages/clerk-js/src/utils/__tests__/queryStateParams.test.ts +++ b/packages/clerk-js/src/utils/__tests__/queryStateParams.spec.ts @@ -1,3 +1,5 @@ +import { describe, expect, it } from 'vitest'; + import { appendModalState } from '../queryStateParams'; describe('appendModalState function', () => { diff --git a/packages/clerk-js/src/utils/__tests__/querystring.test.ts b/packages/clerk-js/src/utils/__tests__/querystring.spec.ts similarity index 98% rename from packages/clerk-js/src/utils/__tests__/querystring.test.ts rename to packages/clerk-js/src/utils/__tests__/querystring.spec.ts index 75a5b54a322..b1050cd89e6 100644 --- a/packages/clerk-js/src/utils/__tests__/querystring.test.ts +++ b/packages/clerk-js/src/utils/__tests__/querystring.spec.ts @@ -1,4 +1,5 @@ import { camelToSnake } from '@clerk/shared/underscore'; +import { describe, expect, it } from 'vitest'; import { getQueryParams, stringifyQueryParams } from '../querystring'; diff --git a/packages/clerk-js/src/utils/__tests__/redirectUrls.test.ts b/packages/clerk-js/src/utils/__tests__/redirectUrls.spec.ts similarity index 99% rename from packages/clerk-js/src/utils/__tests__/redirectUrls.test.ts rename to packages/clerk-js/src/utils/__tests__/redirectUrls.spec.ts index 8f7a3c4c51c..16cd00075d9 100644 --- a/packages/clerk-js/src/utils/__tests__/redirectUrls.test.ts +++ b/packages/clerk-js/src/utils/__tests__/redirectUrls.spec.ts @@ -1,5 +1,6 @@ import { snakeToCamel } from '@clerk/shared/underscore'; import type { RedirectOptions } from '@clerk/types'; +import { afterAll, beforeEach, describe, expect, it } from 'vitest'; import { RedirectUrls } from '../redirectUrls'; diff --git a/packages/clerk-js/src/utils/__tests__/resourceParams.test.ts b/packages/clerk-js/src/utils/__tests__/resourceParams.spec.ts similarity index 96% rename from packages/clerk-js/src/utils/__tests__/resourceParams.test.ts rename to packages/clerk-js/src/utils/__tests__/resourceParams.spec.ts index 31c694eae25..a15b9253808 100644 --- a/packages/clerk-js/src/utils/__tests__/resourceParams.test.ts +++ b/packages/clerk-js/src/utils/__tests__/resourceParams.spec.ts @@ -1,4 +1,5 @@ import type { UpdateUserParams } from '@clerk/types'; +import { describe, expect, it } from 'vitest'; import { normalizeUnsafeMetadata } from '../resourceParams'; diff --git a/packages/clerk-js/src/utils/__tests__/url.test.ts b/packages/clerk-js/src/utils/__tests__/url.spec.ts similarity index 97% rename from packages/clerk-js/src/utils/__tests__/url.test.ts rename to packages/clerk-js/src/utils/__tests__/url.spec.ts index 53176d95584..9e37b01cc08 100644 --- a/packages/clerk-js/src/utils/__tests__/url.test.ts +++ b/packages/clerk-js/src/utils/__tests__/url.spec.ts @@ -1,5 +1,6 @@ import { logger } from '@clerk/shared/logger'; import type { SignUpResource } from '@clerk/types'; +import { afterAll, beforeEach, describe, expect, it, test, vi } from 'vitest'; import { buildURL, @@ -148,7 +149,7 @@ describe('hasBannedProtocol(url)', () => { describe('buildURL(options: URLParams, skipOrigin)', () => { it('builds a URL()', () => { - expect(buildURL({}, { stringify: true })).toBe('http://localhost/'); + expect(buildURL({}, { stringify: true })).toBe('http://localhost:3000/'); expect( buildURL( { @@ -158,7 +159,7 @@ describe('buildURL(options: URLParams, skipOrigin)', () => { }, { stringify: true }, ), - ).toBe('http://localhost/my-path?my-search#my-hash?my-hashed-search'); + ).toBe('http://localhost:3000/my-path?my-search#my-hash?my-hashed-search'); expect( buildURL( { @@ -499,7 +500,7 @@ describe('isAllowedRedirect', () => { ['..//evil.com', ['https://www.clerk.com'], false], ]; - const warnMock = jest.spyOn(logger, 'warnOnce'); + const warnMock = vi.spyOn(logger, 'warnOnce'); beforeEach(() => warnMock.mockClear()); afterAll(() => warnMock.mockRestore()); @@ -516,11 +517,7 @@ describe('createAllowedRedirectOrigins', () => { const allowedRedirectOriginsValuesUndefined = createAllowedRedirectOrigins(undefined, frontendApi, 'production'); const allowedRedirectOriginsValuesEmptyArray = createAllowedRedirectOrigins([], frontendApi, 'production'); - const expectedAllowedRedirectOrigins = [ - 'http://localhost', // Current location - `https://example.com`, // Primary domain - `https://*.example.com`, // Wildcard subdomains - ]; + const expectedAllowedRedirectOrigins = ['http://localhost:3000', `https://example.com`, `https://*.example.com`]; expect(allowedRedirectOriginsValuesUndefined).toEqual(expectedAllowedRedirectOrigins); expect(allowedRedirectOriginsValuesEmptyArray).toEqual(expectedAllowedRedirectOrigins); @@ -532,10 +529,10 @@ describe('createAllowedRedirectOrigins', () => { const allowedRedirectOriginsValuesEmptyArray = createAllowedRedirectOrigins([], frontendApi, 'development'); const expectedAllowedRedirectOrigins = [ - 'http://localhost', // Current location - `https://foo-bar-42.accounts.dev`, // Account Portal - `https://*.foo-bar-42.accounts.dev`, // Account Portal subdomains - `https://foo-bar-42.clerk.accounts.dev`, // Frontend API + 'http://localhost:3000', + `https://foo-bar-42.accounts.dev`, + `https://*.foo-bar-42.accounts.dev`, + `https://foo-bar-42.clerk.accounts.dev`, ]; expect(allowedRedirectOriginsValuesUndefined).toEqual(expectedAllowedRedirectOrigins); diff --git a/packages/clerk-js/src/vitestUtils.ts b/packages/clerk-js/src/vitestUtils.ts new file mode 100644 index 00000000000..58500f47371 --- /dev/null +++ b/packages/clerk-js/src/vitestUtils.ts @@ -0,0 +1,78 @@ +// eslint-disable-next-line no-restricted-imports +import { matchers } from '@emotion/jest'; +import type { RenderOptions } from '@testing-library/react'; +import { render as _render } from '@testing-library/react'; +import UserEvent from '@testing-library/user-event'; +import { afterAll, beforeAll, describe, expect, type SpyInstance, vi } from 'vitest'; + +expect.extend(matchers); + +Element.prototype.scrollIntoView = vi.fn(); + +const render = (ui: React.ReactElement, options?: RenderOptions) => { + const userEvent = UserEvent.setup({ delay: null }); + return { ..._render(ui, { ...options }), userEvent }; +}; + +/** + * Helper method to mock a native runtime environment for specific test cases, currently targeted at React Native. + * Makes some assumptions about our runtime detection utilities in `packages/clerk-js/src/utils/runtime.ts`. + * + * Usage: + * + * ```js + * mockNativeRuntime(() => { + * // test cases + * it('simulates native', () => { + * expect(typeof document).toBe('undefined'); + * }); + * }); + * ``` + */ +export const mockNativeRuntime = (fn: () => void) => { + describe('native runtime', () => { + let spyDocument: SpyInstance; + let spyNavigator: SpyInstance; + + beforeAll(() => { + spyDocument = vi.spyOn(globalThis, 'document', 'get'); + spyDocument.mockReturnValue(undefined); + + spyNavigator = vi.spyOn(globalThis.navigator, 'product', 'get'); + spyNavigator.mockReturnValue('ReactNative'); + }); + + afterAll(() => { + spyDocument.mockRestore(); + spyNavigator.mockRestore(); + }); + + fn(); + }); +}; + +export const mockWebAuthn = (fn: () => void) => { + describe('with WebAuthn', () => { + let originalPublicKeyCredential: any; + beforeAll(() => { + originalPublicKeyCredential = global.PublicKeyCredential; + const publicKeyCredential: any = () => {}; + global.PublicKeyCredential = publicKeyCredential; + publicKeyCredential.isUserVerifyingPlatformAuthenticatorAvailable = () => Promise.resolve(true); + publicKeyCredential.isConditionalMediationAvailable = () => Promise.resolve(true); + }); + + afterAll(() => { + global.PublicKeyCredential = originalPublicKeyCredential; + }); + + fn(); + }); +}; + +export * from './ui/utils/vitest/runFakeTimers'; +export * from './ui/utils/vitest/createFixtures'; +// eslint-disable-next-line import/export +export * from '@testing-library/react'; +// eslint-disable-next-line import/export +export { render }; diff --git a/packages/clerk-js/vitest.config.mts b/packages/clerk-js/vitest.config.mts index f9d9cf2ecd4..542579f6c7d 100644 --- a/packages/clerk-js/vitest.config.mts +++ b/packages/clerk-js/vitest.config.mts @@ -1,14 +1,59 @@ -import { defineConfig } from 'vitest/config'; import react from '@vitejs/plugin-react'; +import { resolve } from 'path'; +import { defineConfig } from 'vitest/config'; + +function viteSvgMockPlugin() { + return { + name: 'svg-mock', + transform(code: string, id: string) { + if (id.endsWith('.svg') && process.env.NODE_ENV === 'test') { + return { + code: ` + import React from 'react'; + const SvgMock = React.forwardRef((props, ref) => React.createElement('span', { ref, ...props })); + export default SvgMock; + export { SvgMock as ReactComponent }; + `, + map: null, + }; + } + }, + }; +} export default defineConfig({ - plugins: [react({ jsxRuntime: 'automatic', jsxImportSource: '@emotion/react' })], + plugins: [react({ jsxRuntime: 'automatic', jsxImportSource: '@emotion/react' }), viteSvgMockPlugin()], define: { + __BUILD_VARIANT_CHIPS__: JSON.stringify(false), __PKG_NAME__: JSON.stringify('@clerk/clerk-js'), + __PKG_VERSION__: JSON.stringify('test'), }, test: { - include: ['**/*.spec.?(c|m)[jt]s?(x)'], + coverage: { + provider: 'v8', + enabled: true, + reporter: ['text', 'json', 'html'], + include: ['src/**/*.{ts,tsx}'], + exclude: [ + 'src/**/*.d.ts', + 'src/**/index.ts', + 'src/**/index.browser.ts', + 'src/**/index.chips.browser.ts', + 'src/**/index.headless.ts', + 'src/**/index.headless.browser.ts', + 'src/**/index.legacy.browser.ts', + 'src/**/coverage/**', + 'src/**/dist/**', + 'src/**/node_modules/**', + 'src/(ui|utils|core)/__tests__/**', + ], + }, environment: 'jsdom', + globals: false, + include: ['**/*.spec.?(c|m)[jt]s?(x)'], setupFiles: './vitest.setup.mts', }, + resolve: { + alias: [{ find: /^@\//, replacement: `${resolve(__dirname, 'src')}/` }], + }, }); diff --git a/packages/clerk-js/vitest.setup.mts b/packages/clerk-js/vitest.setup.mts index 5740515e5d5..72bbb1619f2 100644 --- a/packages/clerk-js/vitest.setup.mts +++ b/packages/clerk-js/vitest.setup.mts @@ -1,5 +1,95 @@ -import { afterEach } from 'vitest'; -import { cleanup } from '@testing-library/react'; import '@testing-library/jest-dom/vitest'; +import * as crypto from 'node:crypto'; +import { TextDecoder, TextEncoder } from 'node:util'; + +import { cleanup } from '@testing-library/react'; +import { afterAll, afterEach, beforeAll, vi } from 'vitest'; + afterEach(cleanup); + +// Store the original method +// eslint-disable-next-line @typescript-eslint/unbound-method +const ogToLocaleDateString = Date.prototype.toLocaleDateString; + +beforeAll(() => { + // Make sure our tests always use the same locale + Date.prototype.toLocaleDateString = function (...args: any[]) { + // Call original method with 'en-US' locale + return ogToLocaleDateString.call(this, 'en-US', args[1]); // Pass options if provided + }; + + // --- Setup from jest.jsdom-with-timezone.ts --- + // Set a default timezone (e.g., UTC) for consistency + process.env.TZ = 'UTC'; +}); + +afterAll(() => { + // Restore original Date method + Date.prototype.toLocaleDateString = ogToLocaleDateString; +}); + +// --- Setup from package jest.setup.ts --- + +// Mock Response class if not already defined by jsdom/happy-dom +class FakeResponse {} + +// Polyfill/mock global objects for the jsdom environment +if (typeof window !== 'undefined') { + Object.defineProperties(globalThis, { + TextDecoder: { value: TextDecoder }, + TextEncoder: { value: TextEncoder }, + Response: { value: FakeResponse }, + crypto: { value: crypto.webcrypto }, + }); + + // Mock ResizeObserver + window.ResizeObserver = + window.ResizeObserver || + vi.fn().mockImplementation(() => ({ + disconnect: vi.fn(), + observe: vi.fn(), + unobserve: vi.fn(), + })); + + // Mock matchMedia + Object.defineProperty(window, 'matchMedia', { + writable: true, + value: vi.fn().mockImplementation(query => ({ + matches: false, + media: query, + onchange: null, + addEventListener: vi.fn(), + removeEventListener: vi.fn(), + dispatchEvent: vi.fn(), + })), + }); + + // Mock IntersectionObserver + //@ts-expect-error - Mocking class + globalThis.IntersectionObserver = class IntersectionObserver { + constructor() {} + disconnect() { + return null; + } + observe() { + return null; + } + takeRecords() { + return []; // Return empty array as per spec + } + unobserve() { + return null; + } + }; +} + +// Mock jest-chrome if its functionality is needed +// Example: Mocking chrome.runtime.sendMessage +// global.chrome = { +// runtime: { +// sendMessage: vi.fn(), +// // ... other chrome APIs needed +// }, +// // ... other chrome namespaces needed +// }; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index f6cc990ba15..9707e66b295 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -131,8 +131,8 @@ importers: specifier: ^4.5.1 version: 4.5.1(vite@6.2.6(@types/node@22.15.29)(jiti@2.4.2)(lightningcss@1.27.0)(terser@5.39.0)(tsx@4.19.2)(yaml@2.7.1)) '@vitest/coverage-v8': - specifier: 3.0.2 - version: 3.0.2(vitest@3.0.5(@edge-runtime/vm@5.0.0)(@types/debug@4.1.12)(@types/node@22.15.29)(jiti@2.4.2)(jsdom@24.1.3)(lightningcss@1.27.0)(msw@2.8.7(@types/node@22.15.29)(typescript@5.8.3))(terser@5.39.0)(tsx@4.19.2)(yaml@2.7.1)) + specifier: 3.0.5 + version: 3.0.5(vitest@3.0.5(@edge-runtime/vm@5.0.0)(@types/debug@4.1.12)(@types/node@22.15.29)(jiti@2.4.2)(jsdom@24.1.3)(lightningcss@1.27.0)(msw@2.8.7(@types/node@22.15.29)(typescript@5.8.3))(terser@5.39.0)(tsx@4.19.2)(yaml@2.7.1)) chalk: specifier: 4.1.2 version: 4.1.2 @@ -501,6 +501,9 @@ importers: specifier: 2.3.3 version: 2.3.3(react@18.3.1) devDependencies: + '@emotion/jest': + specifier: ^11.13.0 + version: 11.13.0(@types/jest@29.5.12) '@rsdoctor/rspack-plugin': specifier: ^0.4.13 version: 0.4.13(@rspack/core@1.2.8(@swc/helpers@0.5.17))(webpack@5.94.0(@swc/core@1.11.29(@swc/helpers@0.5.17))(esbuild@0.25.0)) @@ -525,6 +528,9 @@ importers: '@types/webpack-env': specifier: ^1.18.8 version: 1.18.8 + jsdom: + specifier: ^24.1.1 + version: 24.1.3 webpack-merge: specifier: ^5.10.0 version: 5.10.0 @@ -3467,82 +3473,66 @@ packages: '@miniflare/cache@2.14.4': resolution: {integrity: sha512-ayzdjhcj+4mjydbNK7ZGDpIXNliDbQY4GPcY2KrYw0v1OSUdj5kZUkygD09fqoGRfAks0d91VelkyRsAXX8FQA==} engines: {node: '>=16.13'} - deprecated: Miniflare v2 is no longer supported. Please upgrade to Miniflare v4 '@miniflare/core@2.14.4': resolution: {integrity: sha512-FMmZcC1f54YpF4pDWPtdQPIO8NXfgUxCoR9uyrhxKJdZu7M6n8QKopPVNuaxR40jcsdxb7yKoQoFWnHfzJD9GQ==} engines: {node: '>=16.13'} - deprecated: Miniflare v2 is no longer supported. Please upgrade to Miniflare v4 '@miniflare/d1@2.14.4': resolution: {integrity: sha512-pMBVq9XWxTDdm+RRCkfXZP+bREjPg1JC8s8C0JTovA9OGmLQXqGTnFxIaS9vf1d8k3uSUGhDzPTzHr0/AUW1gA==} engines: {node: '>=16.7'} - deprecated: Miniflare v2 is no longer supported. Please upgrade to Miniflare v4 '@miniflare/durable-objects@2.14.4': resolution: {integrity: sha512-+JrmHP6gHHrjxV8S3axVw5lGHLgqmAGdcO/1HJUPswAyJEd3Ah2YnKhpo+bNmV4RKJCtEq9A2hbtVjBTD2YzwA==} engines: {node: '>=16.13'} - deprecated: Miniflare v2 is no longer supported. Please upgrade to Miniflare v4 '@miniflare/html-rewriter@2.14.4': resolution: {integrity: sha512-GB/vZn7oLbnhw+815SGF+HU5EZqSxbhIa3mu2L5MzZ2q5VOD5NHC833qG8c2GzDPhIaZ99ITY+ZJmbR4d+4aNQ==} engines: {node: '>=16.13'} - deprecated: Miniflare v2 is no longer supported. Please upgrade to Miniflare v4 '@miniflare/kv@2.14.4': resolution: {integrity: sha512-QlERH0Z+klwLg0xw+/gm2yC34Nnr/I0GcQ+ASYqXeIXBwjqOtMBa3YVQnocaD+BPy/6TUtSpOAShHsEj76R2uw==} engines: {node: '>=16.13'} - deprecated: Miniflare v2 is no longer supported. Please upgrade to Miniflare v4 '@miniflare/queues@2.14.4': resolution: {integrity: sha512-aXQ5Ik8Iq1KGMBzGenmd6Js/jJgqyYvjom95/N9GptCGpiVWE5F0XqC1SL5rCwURbHN+aWY191o8XOFyY2nCUA==} engines: {node: '>=16.7'} - deprecated: Miniflare v2 is no longer supported. Please upgrade to Miniflare v4 '@miniflare/r2@2.14.4': resolution: {integrity: sha512-4ctiZWh7Ty7LB3brUjmbRiGMqwyDZgABYaczDtUidblo2DxX4JZPnJ/ZAyxMPNJif32kOJhcg6arC2hEthR9Sw==} engines: {node: '>=16.13'} - deprecated: Miniflare v2 is no longer supported. Please upgrade to Miniflare v4 '@miniflare/runner-vm@2.14.4': resolution: {integrity: sha512-Nog0bB9SVhPbZAkTWfO4lpLAUsBXKEjlb4y+y66FJw77mPlmPlVdpjElCvmf8T3VN/pqh83kvELGM+/fucMf4g==} engines: {node: '>=16.13'} - deprecated: Miniflare v2 is no longer supported. Please upgrade to Miniflare v4 '@miniflare/shared-test-environment@2.14.4': resolution: {integrity: sha512-FdU2/8wEd00vIu+MfofLiHcfZWz+uCbE2VTL85KpyYfBsNGAbgRtzFMpOXdoXLqQfRu6MBiRwWpb2FbMrBzi7g==} engines: {node: '>=16.13'} - deprecated: Miniflare v2 is no longer supported. Please upgrade to Miniflare v4 '@miniflare/shared@2.14.4': resolution: {integrity: sha512-upl4RSB3hyCnITOFmRZjJj4A72GmkVrtfZTilkdq5Qe5TTlzsjVeDJp7AuNUM9bM8vswRo+N5jOiot6O4PVwwQ==} engines: {node: '>=16.13'} - deprecated: Miniflare v2 is no longer supported. Please upgrade to Miniflare v4 '@miniflare/sites@2.14.4': resolution: {integrity: sha512-O5npWopi+fw9W9Ki0gy99nuBbgDva/iXy8PDC4dAXDB/pz45nISDqldabk0rL2t4W2+lY6LXKzdOw+qJO1GQTA==} engines: {node: '>=16.13'} - deprecated: Miniflare v2 is no longer supported. Please upgrade to Miniflare v4 '@miniflare/storage-file@2.14.4': resolution: {integrity: sha512-JxcmX0hXf4cB0cC9+s6ZsgYCq+rpyUKRPCGzaFwymWWplrO3EjPVxKCcMxG44jsdgsII6EZihYUN2J14wwCT7A==} engines: {node: '>=16.13'} - deprecated: Miniflare v2 is no longer supported. Please upgrade to Miniflare v4 '@miniflare/storage-memory@2.14.4': resolution: {integrity: sha512-9jB5BqNkMZ3SFjbPFeiVkLi1BuSahMhc/W1Y9H0W89qFDrrD+z7EgRgDtHTG1ZRyi9gIlNtt9qhkO1B6W2qb2A==} engines: {node: '>=16.13'} - deprecated: Miniflare v2 is no longer supported. Please upgrade to Miniflare v4 '@miniflare/watcher@2.14.4': resolution: {integrity: sha512-PYn05ET2USfBAeXF6NZfWl0O32KVyE8ncQ/ngysrh3hoIV7l3qGGH7ubeFx+D8VWQ682qYhwGygUzQv2j1tGGg==} engines: {node: '>=16.13'} - deprecated: Miniflare v2 is no longer supported. Please upgrade to Miniflare v4 '@miniflare/web-sockets@2.14.4': resolution: {integrity: sha512-stTxvLdJ2IcGOs76AnvGYAzGvx8JvQPRxC5DW0P5zdAAnhL33noqb5LKdPt3P37BKp9FzBKZHuihQI9oVqwm0g==} engines: {node: '>=16.13'} - deprecated: Miniflare v2 is no longer supported. Please upgrade to Miniflare v4 '@modelcontextprotocol/sdk@1.7.0': resolution: {integrity: sha512-IYPe/FLpvF3IZrd/f5p5ffmWhMc3aEMuM2wGJASDqC2Ge7qatVCdbfPx3n/5xFeb19xN0j/911M2AaFuircsWA==} @@ -5498,11 +5488,11 @@ packages: vite: ^5.0.0 || ^6.0.0 vue: ^3.2.25 - '@vitest/coverage-v8@3.0.2': - resolution: {integrity: sha512-U+hZYb0FtgNDb6B3E9piAHzXXIuxuBw2cd6Lvepc9sYYY4KjgiwCBmo3Sird9ZRu3ggLpLBTfw1ZRr77ipiSfw==} + '@vitest/coverage-v8@3.0.5': + resolution: {integrity: sha512-zOOWIsj5fHh3jjGwQg+P+J1FW3s4jBu1Zqga0qW60yutsBtqEqNEJKWYh7cYn1yGD+1bdPsPdC/eL4eVK56xMg==} peerDependencies: - '@vitest/browser': 3.0.2 - vitest: 3.0.2 + '@vitest/browser': 3.0.5 + vitest: 3.0.5 peerDependenciesMeta: '@vitest/browser': optional: true @@ -14798,7 +14788,6 @@ snapshots: '@csstools/css-parser-algorithms': 3.0.4(@csstools/css-tokenizer@3.0.3) '@csstools/css-tokenizer': 3.0.3 lru-cache: 10.4.3 - optional: true '@astrojs/compiler@2.11.0': {} @@ -16079,14 +16068,12 @@ snapshots: '@jridgewell/trace-mapping': 0.3.9 optional: true - '@csstools/color-helpers@5.0.2': - optional: true + '@csstools/color-helpers@5.0.2': {} '@csstools/css-calc@2.1.3(@csstools/css-parser-algorithms@3.0.4(@csstools/css-tokenizer@3.0.3))(@csstools/css-tokenizer@3.0.3)': dependencies: '@csstools/css-parser-algorithms': 3.0.4(@csstools/css-tokenizer@3.0.3) '@csstools/css-tokenizer': 3.0.3 - optional: true '@csstools/css-color-parser@3.0.9(@csstools/css-parser-algorithms@3.0.4(@csstools/css-tokenizer@3.0.3))(@csstools/css-tokenizer@3.0.3)': dependencies: @@ -16094,15 +16081,12 @@ snapshots: '@csstools/css-calc': 2.1.3(@csstools/css-parser-algorithms@3.0.4(@csstools/css-tokenizer@3.0.3))(@csstools/css-tokenizer@3.0.3) '@csstools/css-parser-algorithms': 3.0.4(@csstools/css-tokenizer@3.0.3) '@csstools/css-tokenizer': 3.0.3 - optional: true '@csstools/css-parser-algorithms@3.0.4(@csstools/css-tokenizer@3.0.3)': dependencies: '@csstools/css-tokenizer': 3.0.3 - optional: true - '@csstools/css-tokenizer@3.0.3': - optional: true + '@csstools/css-tokenizer@3.0.3': {} '@cypress/request@3.0.6': dependencies: @@ -20448,7 +20432,7 @@ snapshots: vite: 6.2.6(@types/node@22.15.29)(jiti@2.4.2)(lightningcss@1.27.0)(terser@5.39.0)(tsx@4.19.2)(yaml@2.7.1) vue: 3.5.13(typescript@5.8.3) - '@vitest/coverage-v8@3.0.2(vitest@3.0.5(@edge-runtime/vm@5.0.0)(@types/debug@4.1.12)(@types/node@22.15.29)(jiti@2.4.2)(jsdom@24.1.3)(lightningcss@1.27.0)(msw@2.8.7(@types/node@22.15.29)(typescript@5.8.3))(terser@5.39.0)(tsx@4.19.2)(yaml@2.7.1))': + '@vitest/coverage-v8@3.0.5(vitest@3.0.5(@edge-runtime/vm@5.0.0)(@types/debug@4.1.12)(@types/node@22.15.29)(jiti@2.4.2)(jsdom@24.1.3)(lightningcss@1.27.0)(msw@2.8.7(@types/node@22.15.29)(typescript@5.8.3))(terser@5.39.0)(tsx@4.19.2)(yaml@2.7.1))': dependencies: '@ampproject/remapping': 2.3.0 '@bcoe/v8-coverage': 1.0.2 @@ -22435,7 +22419,6 @@ snapshots: dependencies: '@asamuzakjp/css-color': 3.1.5 rrweb-cssom: 0.8.0 - optional: true csstype@3.1.3: {} @@ -22502,7 +22485,6 @@ snapshots: dependencies: whatwg-mimetype: 4.0.0 whatwg-url: 14.2.0 - optional: true data-view-buffer@1.0.2: dependencies: @@ -24741,7 +24723,6 @@ snapshots: html-encoding-sniffer@4.0.0: dependencies: whatwg-encoding: 3.1.1 - optional: true html-entities@2.5.2: {} @@ -24790,7 +24771,6 @@ snapshots: debug: 4.4.0(supports-color@8.1.1) transitivePeerDependencies: - supports-color - optional: true http-proxy-middleware@2.0.6(@types/express@4.17.22): dependencies: @@ -25935,7 +25915,6 @@ snapshots: - bufferutil - supports-color - utf-8-validate - optional: true jsesc@3.0.2: {} @@ -29101,11 +29080,9 @@ snapshots: parseurl: 1.3.3 path-to-regexp: 8.2.0 - rrweb-cssom@0.7.1: - optional: true + rrweb-cssom@0.7.1: {} - rrweb-cssom@0.8.0: - optional: true + rrweb-cssom@0.8.0: {} rslog@1.2.3: {} @@ -30227,7 +30204,6 @@ snapshots: tr46@5.1.1: dependencies: punycode: 2.3.1 - optional: true tree-dump@1.0.2(tslib@2.8.1): dependencies: @@ -31277,7 +31253,6 @@ snapshots: w3c-xmlserializer@5.0.0: dependencies: xml-name-validator: 5.0.0 - optional: true walker@1.0.8: dependencies: @@ -31449,14 +31424,12 @@ snapshots: whatwg-encoding@3.1.1: dependencies: iconv-lite: 0.6.3 - optional: true whatwg-fetch@3.6.20: {} whatwg-mimetype@3.0.0: {} - whatwg-mimetype@4.0.0: - optional: true + whatwg-mimetype@4.0.0: {} whatwg-url-without-unicode@8.0.0-3: dependencies: @@ -31473,7 +31446,6 @@ snapshots: dependencies: tr46: 5.1.1 webidl-conversions: 7.0.0 - optional: true whatwg-url@5.0.0: dependencies: @@ -31632,8 +31604,7 @@ snapshots: xml-name-validator@4.0.0: {} - xml-name-validator@5.0.0: - optional: true + xml-name-validator@5.0.0: {} xml2js@0.6.0: dependencies: