Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
1f96646
feat(repo): re-enable unit tests for clerk-js and ui packages
jacekradko Jan 15, 2026
2491917
fix(ui): check annualMonthlyFee amount instead of object existence
jacekradko Jan 15, 2026
d90c6fd
fix(clerk-js): put individual ClerkAPIErrors in raw when no field errors
jacekradko Jan 15, 2026
99f4ca6
fix(clerk-js): add sessions array to finalize test mocks
jacekradko Jan 15, 2026
2577566
fix(shared): include props redirectUrl in toSearchParams
jacekradko Jan 15, 2026
2a25d42
fix(clerk-js): remove dead create-fixtures and add @emotion/react devDep
jacekradko Jan 15, 2026
b47a44c
fix(ui): fix vitest path aliases and inline containsAllOfType utility
jacekradko Jan 15, 2026
97d44ef
fix(ui): add local test utilities with ModuleManagerProvider and orga…
jacekradko Jan 15, 2026
69aec6f
fix(ui): add optional chaining for organizationCreationDefaults.form …
jacekradko Jan 15, 2026
74e541d
fix(ui): add clearFetchCache and fix test assertions in TaskChooseOrg…
jacekradko Jan 15, 2026
56be312
revert(ui): undo TaskChooseOrganization test changes that broke other…
jacekradko Jan 15, 2026
a07e651
Merge branch 'main' into jacek/user-4359-re-enable-unit-tests
jacekradko Jan 16, 2026
9ede558
fix(ui): fix unit test failures in OrganizationProfile and TaskChoose…
jacekradko Jan 16, 2026
4840d28
fix(ui): fix TaskChooseOrganization max memberships test
jacekradko Jan 16, 2026
2918bba
fix(ui): fix TaskChooseOrganization disabled screen test
jacekradko Jan 16, 2026
796ecd4
fix(ui): fix visibleFields filter in SignUpStart to handle emailOrPho…
jacekradko Jan 16, 2026
6c7552b
fix(ui): fix SignUpStart tests for optional fields and initialValues
jacekradko Jan 16, 2026
cf35d3d
Merge branch 'main' into jacek/user-4359-re-enable-unit-tests
jacekradko Jan 16, 2026
534fe7c
format
jacekradko Jan 16, 2026
75d856c
chore: empty changeset
jacekradko Jan 16, 2026
cf12b38
revert: undo source file changes, keep only test-related changes
jacekradko Jan 16, 2026
cee2879
Merge branch 'main' into jacek/user-4359-re-enable-unit-tests
jacekradko Jan 16, 2026
b736c83
fix(shared): include props redirectUrl in toSearchParams
jacekradko Jan 16, 2026
e2b57c6
fix(clerk-js): use error.errors array for raw errors without field er…
jacekradko Jan 16, 2026
a77dc02
fix(ui): check annualMonthlyFee amount instead of object existence
jacekradko Jan 16, 2026
8ae6cec
fix(ui): handle emailOrPhone case in visibleFields filter
jacekradko Jan 16, 2026
d5510f6
fix(ui): add optional chaining for organizationCreationDefaults.form
jacekradko Jan 16, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .changeset/sweet-olives-notice.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
---
---
3 changes: 2 additions & 1 deletion packages/clerk-js/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@
"lint:attw": "attw --pack . --profile node16 --ignore-rules named-exports",
"lint:publint": "publint || true",
"postbuild:disabled": "node ../../scripts/search-for-rhc.mjs file dist/clerk.no-rhc.mjs",
"test:disabled": "vitest --watch=false",
"test": "vitest --watch=false",
"test:sandbox:integration": "playwright test",
"test:sandbox:integration:ui": "playwright test --ui",
"test:sandbox:integration:update-snapshots": "playwright test --update-snapshots",
Expand Down Expand Up @@ -80,6 +80,7 @@
"devDependencies": {
"@clerk/msw": "workspace:^",
"@clerk/testing": "workspace:^",
"@emotion/react": "11.11.1",
"@rsdoctor/rspack-plugin": "^0.4.13",
"@rspack/cli": "^1.6.0",
"@rspack/core": "^1.6.0",
Expand Down
9 changes: 5 additions & 4 deletions packages/clerk-js/src/core/__tests__/signals.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -62,9 +62,10 @@ describe('errorsToParsedErrors', () => {
const result = errorsToParsedErrors(error, initialFields);

expect(result.fields).toEqual({ emailAddress: null, password: null });
// When there are no field errors, individual ClerkAPIError instances are put in raw
expect(result.raw).toEqual([error.errors[0]]);
// Note: global is null when errors are processed individually without field errors
expect(result.global).toBeNull();
// raw contains the full error so consumers can access any property they need
expect(result.raw).toEqual([error]);
// global contains the wrapped error for display purposes
expect(result.global).toBeTruthy();
expect(result.global?.length).toBe(1);
});
});
4 changes: 2 additions & 2 deletions packages/clerk-js/src/core/resources/__tests__/SignIn.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1857,7 +1857,7 @@ describe('SignIn', () => {
const mockSetActive = vi.fn().mockResolvedValue({});

SignIn.clerk = {
client: { reload: mockReload },
client: { reload: mockReload, sessions: [] },
setActive: mockSetActive,
} as any;

Expand All @@ -1874,7 +1874,7 @@ describe('SignIn', () => {
const mockNavigate = vi.fn();

SignIn.clerk = {
client: { reload: mockReload },
client: { reload: mockReload, sessions: [] },
setActive: mockSetActive,
} as any;

Expand Down
8 changes: 7 additions & 1 deletion packages/clerk-js/src/test/fixtures.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,13 @@ import type {
UserSettingsJSON,
} from '@clerk/shared/types';

import { containsAllOfType } from '../ui/utils/containsAllOf';
/**
* Enforces that an array contains ALL keys of T
*/
const containsAllOfType =
<T>() =>
<U extends Readonly<T[]>>(array: U & ([T] extends [U[number]] ? unknown : 'Invalid')) =>
array;

export const createBaseEnvironmentJSON = (): EnvironmentJSON => {
return {
Expand Down
1 change: 0 additions & 1 deletion packages/clerk-js/src/test/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,6 @@ export const mockWebAuthn = (fn: () => void) => {
});
};

export * from './create-fixtures';
// Export everything from @testing-library/react except render, then export our custom render
export {
screen,
Expand Down
2 changes: 1 addition & 1 deletion packages/shared/src/internal/clerk-js/redirectUrls.ts
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@ export class RedirectUrls {
this.fromSearchParams.signInFallbackRedirectUrl ||
this.fromProps.signInFallbackRedirectUrl ||
this.fromOptions.signInFallbackRedirectUrl;
const redirectUrl = this.fromSearchParams.redirectUrl;
const redirectUrl = this.fromSearchParams.redirectUrl || this.fromProps.redirectUrl;

const res: RedirectOptions & { redirectUrl?: string | null } = {
signUpForceRedirectUrl,
Expand Down
2 changes: 1 addition & 1 deletion packages/ui/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -63,9 +63,9 @@
"lint:disabled": "eslint src",
"lint:publint": "publint",
"showerrors": "tsc",
"test": "vitest",
"test:ci": "vitest --maxWorkers=70%",
"test:coverage": "vitest --collectCoverage && open coverage/lcov-report/index.html",
"test:disabled": "vitest",
"type-check": "tsc --noEmit"
},
"dependencies": {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -155,6 +155,7 @@ describe('InviteMembersPage', () => {
{ wrapper },
);
await userEvent.type(getByTestId('tag-input'), 'test+1@clerk.com,');
await waitFor(() => expect(getByRole('button', { name: /mydefaultrole/i })).toBeInTheDocument());
await userEvent.click(getByRole('button', { name: /mydefaultrole/i }));
});

Expand Down Expand Up @@ -285,7 +286,7 @@ describe('InviteMembersPage', () => {
});

fixtures.clerk.organization?.inviteMembers.mockResolvedValueOnce([{}] as OrganizationInvitationResource[]);
const { getByRole, userEvent, getByTestId } = render(
const { getByRole, userEvent, getByTestId, getByText } = render(
<Action.Root>
<InviteMembersScreen />
</Action.Root>,
Expand All @@ -294,7 +295,7 @@ describe('InviteMembersPage', () => {
await userEvent.type(getByTestId('tag-input'), 'test+1@clerk.com,');
await waitFor(() => expect(getByRole('button', { name: /select role/i })).toBeInTheDocument());
await userEvent.click(getByRole('button', { name: /select role/i }));
await userEvent.click(getByRole('button', { name: /admin/i }));
await userEvent.click(getByText(/^admin$/i));
await waitFor(() => expect(getByRole('button', { name: 'Send invitations' })).not.toBeDisabled());
});

Expand Down Expand Up @@ -359,7 +360,9 @@ describe('InviteMembersPage', () => {

expect(getByRole('button', { name: 'Send invitations' })).toBeDisabled();
await userEvent.type(getByTestId('tag-input'), 'test+1@clerk.com,');
expect(getByRole('button', { name: 'Send invitations' })).not.toBeDisabled();
// Wait for the default role to be applied and the button to become enabled
await waitFor(() => expect(getByRole('button', { name: 'Send invitations' })).not.toBeDisabled());
await waitFor(() => expect(getByRole('button', { name: /mydefaultrole/i })).toBeInTheDocument());
await userEvent.click(getByRole('button', { name: /mydefaultrole/i }));
});
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -697,13 +697,24 @@ describe('OrganizationMembers', () => {
],
});

const { container, getByText } = render(<OrganizationMembers />, { wrapper });
const { container, findByText, queryAllByRole } = render(<OrganizationMembers />, { wrapper });

await waitForLoadingCompleted(container);

expect(getByText('Roles are temporarily locked')).toBeInTheDocument();
// Wait for roles to be fetched (buttons becoming disabled indicates roles have loaded)
await waitFor(() => {
const adminButtons = queryAllByRole('button', { name: 'Admin' });
expect(adminButtons.length).toBeGreaterThan(0);
adminButtons.forEach(button => expect(button).toBeDisabled());
});

// Now check for the alert
expect(await findByText('Roles are temporarily locked')).toBeInTheDocument();
// Use regex to match both curly and straight apostrophes
expect(
getByText("We are updating the available roles. Once that's done, you'll be able to update roles again."),
await findByText(
/We are updating the available roles\. Once that.s done, you.ll be able to update roles again\./,
),
).toBeInTheDocument();
});

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ const valueResolution = (params: UsePricingFooterStateParams): [boolean, boolean
// Active subscription
if (subscription.status === 'active') {
const isCanceled = !!subscription.canceledAt;
const isSwitchingPaidPeriod = planPeriod !== subscription.planPeriod && Boolean(plan.annualMonthlyFee);
const isSwitchingPaidPeriod = planPeriod !== subscription.planPeriod && Boolean(plan.annualMonthlyFee?.amount);
const isActiveFreeTrial = plan.freeTrialEnabled && subscription.isFreeTrial;

if (isCanceled || isSwitchingPaidPeriod) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,12 +35,12 @@ export const CreateOrganizationScreen = (props: CreateOrganizationScreenProps) =
const { organizationSettings } = useEnvironment();
const [file, setFile] = useState<File | null>();

const nameField = useFormControl('name', props.organizationCreationDefaults?.form.name ?? '', {
const nameField = useFormControl('name', props.organizationCreationDefaults?.form?.name ?? '', {
type: 'text',
label: localizationKeys('taskChooseOrganization.createOrganization.formFieldLabel__name'),
placeholder: localizationKeys('taskChooseOrganization.createOrganization.formFieldInputPlaceholder__name'),
});
const slugField = useFormControl('slug', props.organizationCreationDefaults?.form.slug ?? '', {
const slugField = useFormControl('slug', props.organizationCreationDefaults?.form?.slug ?? '', {
type: 'text',
label: localizationKeys('taskChooseOrganization.createOrganization.formFieldLabel__slug'),
placeholder: localizationKeys('taskChooseOrganization.createOrganization.formFieldInputPlaceholder__slug'),
Expand Down Expand Up @@ -99,7 +99,7 @@ export const CreateOrganizationScreen = (props: CreateOrganizationScreenProps) =
};

const isSubmitButtonDisabled = !nameField.value || !isLoaded;
const defaultLogoUrl = file === undefined ? props.organizationCreationDefaults?.form.logo : undefined;
const defaultLogoUrl = file === undefined ? props.organizationCreationDefaults?.form?.logo : undefined;

return (
<>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import userEvent from '@testing-library/user-event';
import { describe, expect, it } from 'vitest';
import { beforeEach, describe, expect, it } from 'vitest';

import { clearFetchCache } from '@/ui/hooks/useFetch';
import { bindCreateFixtures } from '@/test/create-fixtures';
import { render } from '@/test/utils';
import {
Expand All @@ -9,7 +10,6 @@ import {
} from '@/ui/components/OrganizationSwitcher/__tests__/test-utils';

import { TaskChooseOrganization } from '..';
import { clearFetchCache } from '../../../../../hooks';

const { createFixtures } = bindCreateFixtures('TaskChooseOrganization');

Expand Down Expand Up @@ -222,6 +222,8 @@ describe('TaskChooseOrganization', () => {
email_addresses: ['test@clerk.com'],
create_organization_enabled: true,
tasks: [{ key: 'choose-organization' }],
// Include an organization membership so user has reached max memberships
organization_memberships: [{ name: 'Existing Org', slug: 'org1' }],
});
});

Expand Down Expand Up @@ -298,11 +300,12 @@ describe('TaskChooseOrganization', () => {
});
});

const { queryByText } = render(<TaskChooseOrganization />, { wrapper });
const { queryByText, findByText } = render(<TaskChooseOrganization />, { wrapper });

// Wait for loading to complete and the disabled screen to render
expect(await findByText(/you must belong to an organization/i)).toBeInTheDocument();
expect(await findByText(/contact your organization admin for an invitation/i)).toBeInTheDocument();
expect(queryByText(/create new organization/i)).not.toBeInTheDocument();
expect(queryByText(/you must belong to an organization/i)).toBeInTheDocument();
expect(queryByText(/contact your organization admin for an invitation/i)).toBeInTheDocument();
});

it('with existing memberships or suggestions, displays create organization screen', async () => {
Expand Down
8 changes: 7 additions & 1 deletion packages/ui/src/components/SignUp/SignUpStart.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -378,7 +378,13 @@ function SignUpStartInternal(): JSX.Element {
}

const canToggleEmailPhone = emailOrPhone(attributes, isProgressiveSignUp);
const visibleFields = Object.entries(fields).filter(([_, opts]) => showOptionalFields || opts?.required);
const visibleFields = Object.entries(fields).filter(([key, opts]) => {
// In case both email & phone are optional (emailOrPhone case), always show the active identifier
if ((key === 'emailAddress' || key === 'phoneNumber') && canToggleEmailPhone) {
return !!opts;
}
return showOptionalFields || opts?.required;
});
const shouldShowForm = showFormFields(userSettings) && visibleFields.length > 0;

const showOauthProviders =
Expand Down
38 changes: 31 additions & 7 deletions packages/ui/src/components/SignUp/__tests__/SignUpStart.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -89,12 +89,24 @@ describe('SignUpStart', () => {
});

it('should keep email optional when phone is primary with password', async () => {
const { wrapper } = await createFixtures(f => {
const { wrapper: Wrapper } = await createFixtures(f => {
f.withEmailAddress({ required: false });
f.withPhoneNumber({ required: true });
f.withPassword({ required: true });
});
render(<SignUpStart />, { wrapper });

const wrapperWithOptionalFields = ({ children }: { children: React.ReactNode }) => (
<Wrapper>
<AppearanceProvider
appearanceKey={'signUp'}
appearance={{ options: { showOptionalFields: true } }}
>
{children}
</AppearanceProvider>
</Wrapper>
);

render(<SignUpStart />, { wrapper: wrapperWithOptionalFields });

const emailAddress = screen.getByLabelText('Email address', { selector: 'input' });
expect(emailAddress.ariaRequired).toBe('false');
Expand Down Expand Up @@ -168,12 +180,24 @@ describe('SignUpStart', () => {
});

it('enables optional phone number', async () => {
const { wrapper } = await createFixtures(f => {
const { wrapper: Wrapper } = await createFixtures(f => {
f.withEmailAddress({ required: true });
f.withPhoneNumber({ required: false });
f.withPassword({ required: true });
});
render(<SignUpStart />, { wrapper });

const wrapperWithOptionalFields = ({ children }: { children: React.ReactNode }) => (
<Wrapper>
<AppearanceProvider
appearanceKey={'signUp'}
appearance={{ options: { showOptionalFields: true } }}
>
{children}
</AppearanceProvider>
</Wrapper>
);

render(<SignUpStart />, { wrapper: wrapperWithOptionalFields });
expect(screen.getByText('Phone number').nextElementSibling?.textContent).toBe('Optional');
});

Expand Down Expand Up @@ -287,7 +311,7 @@ describe('SignUpStart', () => {
describe('initialValues', () => {
it('prefills the emailAddress field with the correct initial value', async () => {
const { wrapper, props } = await createFixtures(f => {
f.withEmailAddress();
f.withEmailAddress({ required: true });
});
props.setProps({ initialValues: { emailAddress: 'foo@clerk.com' } });

Expand All @@ -297,7 +321,7 @@ describe('SignUpStart', () => {

it('prefills the phoneNumber field with the correct initial value', async () => {
const { wrapper, props } = await createFixtures(f => {
f.withPhoneNumber();
f.withPhoneNumber({ required: true });
});
props.setProps({ initialValues: { phoneNumber: '+306911111111' } });

Expand All @@ -307,7 +331,7 @@ describe('SignUpStart', () => {

it('prefills the username field with the correct initial value', async () => {
const { wrapper, props } = await createFixtures(f => {
f.withUsername();
f.withUsername({ required: true });
});

props.setProps({ initialValues: { username: 'foo' } });
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import type { VerificationJSON } from '@clerk/shared/types';
import { describe, expect, it } from 'vitest';

import { EmailAddress, PhoneNumber } from '../../../../core/resources';
import { EmailAddress, PhoneNumber } from '@/core/resources';
import { sortIdentificationBasedOnVerification } from '../utils';

describe('UserProfile utils', () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { describe, expect, it } from 'vitest';

import { bindCreateFixtures } from '@/test/create-fixtures';

import { act, renderHook, waitFor } from '../../../test/utils';
import { act, renderHook, waitFor } from '@/test/utils';
import {
createFakeUserOrganizationInvitation,
createFakeUserOrganizationMembership,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { OAUTH_PROVIDERS } from '@clerk/shared/oauth';
import { renderHook } from '@testing-library/react';
import { describe, expect, it } from 'vitest';

import { bindCreateFixtures } from '../../../test/utils';
import { bindCreateFixtures } from '@/test/utils';
import { useEnabledThirdPartyProviders } from '../useEnabledThirdPartyProviders';

const { createFixtures } = bindCreateFixtures('SignUp');
Expand Down
Loading
Loading