From 60245dedae574716b7fd9fbde99b2209087d42c1 Mon Sep 17 00:00:00 2001 From: Monti-27 Date: Mon, 13 Apr 2026 01:56:47 +0530 Subject: [PATCH] fix invite signup loading state --- apps/web/modules/signup-view.submit.test.tsx | 161 +++++++++++++++++++ apps/web/modules/signup-view.tsx | 8 +- 2 files changed, 165 insertions(+), 4 deletions(-) create mode 100644 apps/web/modules/signup-view.submit.test.tsx diff --git a/apps/web/modules/signup-view.submit.test.tsx b/apps/web/modules/signup-view.submit.test.tsx new file mode 100644 index 00000000000000..fc78ede90a96e1 --- /dev/null +++ b/apps/web/modules/signup-view.submit.test.tsx @@ -0,0 +1,161 @@ +import { SIGNUP_ERROR_CODES } from "@calcom/features/auth/signup/constants"; +import * as TooltipPrimitive from "@radix-ui/react-tooltip"; +import { render, screen, waitFor } from "@testing-library/react"; +import userEvent from "@testing-library/user-event"; +import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; + +const { mockCapture, mockFetchSignup, mockFetchUsername, mockRouterPush, mockShowToast, mockSignIn } = + vi.hoisted(() => ({ + mockCapture: vi.fn(), + mockFetchSignup: vi.fn(), + mockFetchUsername: vi.fn(), + mockRouterPush: vi.fn(), + mockShowToast: vi.fn(), + mockSignIn: vi.fn(), + })); + +vi.mock("@calcom/features/auth/signup/lib/fetchSignup", () => ({ + fetchSignup: mockFetchSignup, + hasCheckoutSession: () => false, + isAccountUnderReview: () => false, + isUserAlreadyExistsError: (result: { ok: boolean; status?: number; error?: { message?: string } }) => + !result.ok && result.status === 409 && result.error?.message === SIGNUP_ERROR_CODES.USER_ALREADY_EXISTS, +})); + +vi.mock("@calcom/lib/fetchUsername", () => ({ + fetchUsername: mockFetchUsername, +})); + +vi.mock("@calcom/lib/hooks/useCompatSearchParams", () => ({ + useCompatSearchParams: () => new URLSearchParams(""), +})); + +vi.mock("@calcom/lib/hooks/useDebounce", () => ({ + useDebounce: (value: T) => value, +})); + +vi.mock("@calcom/lib/hooks/useLocale", () => ({ + useLocale: () => ({ + t: (key: string) => key, + i18n: { + language: "en", + }, + }), +})); + +vi.mock("@calcom/ui/components/toast", () => ({ + showToast: mockShowToast, +})); + +vi.mock("@dub/analytics/react", () => ({ + Analytics: () => null, +})); + +vi.mock("next/navigation", () => ({ + useRouter: () => ({ + push: mockRouterPush, + }), +})); + +vi.mock("next-auth/react", () => ({ + signIn: mockSignIn, +})); + +vi.mock("next/script", () => ({ + default: () => null, +})); + +vi.mock("posthog-js", () => ({ + default: { + capture: mockCapture, + }, +})); + +describe("Signup submit flow", () => { + beforeEach(() => { + vi.resetModules(); + vi.stubEnv("NEXT_PUBLIC_WEBSITE_URL", "https://cal.com"); + vi.stubEnv("NEXT_PUBLIC_WEBAPP_URL", "https://app.cal.com"); + vi.stubEnv("NEXT_PUBLIC_IS_E2E", "1"); + mockCapture.mockReset(); + mockFetchSignup.mockReset(); + mockFetchUsername.mockReset(); + mockRouterPush.mockReset(); + mockShowToast.mockReset(); + mockSignIn.mockReset(); + + mockFetchUsername.mockResolvedValue({ + data: { + available: true, + premium: false, + }, + }); + + mockSignIn.mockResolvedValue(undefined); + }); + + afterEach(() => { + vi.unstubAllEnvs(); + }); + + it("does not leave the form loading after a user already exists response", async () => { + const { default: Signup } = await import("./signup-view"); + + mockFetchSignup + .mockResolvedValueOnce({ + ok: false, + status: 409, + error: { + message: SIGNUP_ERROR_CODES.USER_ALREADY_EXISTS, + }, + }); + + const user = userEvent.setup(); + + render( + + + + ); + + await user.type(screen.getByTestId("signup-usernamefield"), "taken-user"); + await user.type(screen.getByTestId("signup-emailfield"), "taken@example.com"); + await user.type(screen.getByTestId("signup-passwordfield"), "Password123"); + + const submitButton = screen.getByTestId("signup-submit-button"); + + await user.click(submitButton); + + await waitFor(() => { + expect(mockFetchSignup).toHaveBeenCalledTimes(1); + }); + + await waitFor(() => { + expect(mockShowToast).toHaveBeenCalledTimes(1); + }); + + expect(submitButton).not.toBeDisabled(); + expect(mockRouterPush).not.toHaveBeenCalled(); + expect(mockFetchSignup).toHaveBeenCalledTimes(1); + expect(mockSignIn).not.toHaveBeenCalled(); + + await new Promise((resolve) => setTimeout(resolve, 3100)); + + expect(mockRouterPush).toHaveBeenCalledWith("/auth/login?callbackUrl=%2Fteams%3Ftoken%3Dinvite-token"); + }); +}); diff --git a/apps/web/modules/signup-view.tsx b/apps/web/modules/signup-view.tsx index f315c9461f9b5d..e25d0d6545bd6b 100644 --- a/apps/web/modules/signup-view.tsx +++ b/apps/web/modules/signup-view.tsx @@ -123,7 +123,7 @@ function UsernameField({ const debouncedUsername = useDebounce(username, 600); useEffect(() => { - if (formState.isSubmitting || formState.isSubmitSuccessful) return; + if (formState.isSubmitting) return; async function checkUsername() { // If the username can't be changed, there is no point in doing the username availability check @@ -140,7 +140,7 @@ function UsernameField({ } checkUsername(); // eslint-disable-next-line react-hooks/exhaustive-deps - }, [debouncedUsername, disabled, orgSlug, formState.isSubmitting, formState.isSubmitSuccessful]); + }, [debouncedUsername, disabled, orgSlug, formState.isSubmitting]); return (
@@ -214,7 +214,7 @@ export default function Signup({ const { register, watch, - formState: { isSubmitting, errors, isSubmitSuccessful }, + formState: { isSubmitting, errors }, } = formMethods; useEffect(() => { @@ -229,7 +229,7 @@ export default function Signup({ setUserConsentToCookie(!consent); } - const loadingSubmitState = isSubmitSuccessful || isSubmitting; + const loadingSubmitState = isSubmitting; const displayBackButton = token ? false : displayEmailForm; const isPlatformUser = redirectUrl?.includes("platform") && redirectUrl?.includes("new");