Skip to content

Commit cdf901f

Browse files
fix: defer email validation to after first blur on signup form (calcom#27765)
* fix: defer email validation to after first blur on signup form Change react-hook-form validation mode from "onChange" to "onTouched" so that the "Invalid email" error only appears after the user has interacted with the field and moved away, not on every keystroke. Password strength hints still update in real-time since onTouched validates on change after the field has been touched (blurred once). Fixes calcom#19163 * test: add unit tests for signup email validation mode Verify the onTouched form validation behavior: - No error shown while user is typing - Error appears only after blur with invalid email - No error for valid email after blur - Revalidation on each keystroke after first blur * remove eslint comment --------- Co-authored-by: Dhairyashil <dhairyashil10101010@gmail.com>
1 parent 3052cf5 commit cdf901f

2 files changed

Lines changed: 104 additions & 1 deletion

File tree

Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
import { zodResolver } from "@hookform/resolvers/zod";
2+
import { render, screen, fireEvent, waitFor } from "@testing-library/react";
3+
import userEvent from "@testing-library/user-event";
4+
import { useForm, FormProvider } from "react-hook-form";
5+
import { describe, expect, it } from "vitest";
6+
import { z } from "zod";
7+
8+
import { signupSchema as apiSignupSchema } from "@calcom/prisma/zod-utils";
9+
10+
const signupSchema = apiSignupSchema.extend({
11+
apiError: z.string().optional(),
12+
cfToken: z.string().optional(),
13+
});
14+
15+
type FormValues = z.infer<typeof signupSchema>;
16+
17+
function TestSignupForm() {
18+
const formMethods = useForm<FormValues>({
19+
resolver: zodResolver(signupSchema),
20+
defaultValues: {
21+
username: "",
22+
email: "",
23+
password: "",
24+
},
25+
mode: "onTouched",
26+
});
27+
28+
const {
29+
register,
30+
formState: { errors },
31+
} = formMethods;
32+
33+
return (
34+
<FormProvider {...formMethods}>
35+
<form>
36+
<label htmlFor="email">Email</label>
37+
<input id="email" type="email" data-testid="email-input" {...register("email")} />
38+
{errors.email && <span data-testid="email-error">{errors.email.message}</span>}
39+
40+
<label htmlFor="password">Password</label>
41+
<input id="password" type="password" data-testid="password-input" {...register("password")} />
42+
{errors.password && <span data-testid="password-error">{errors.password.message}</span>}
43+
</form>
44+
</FormProvider>
45+
);
46+
}
47+
48+
describe("Signup form validation mode", () => {
49+
it("should not show email error while user is still typing", async () => {
50+
const user = userEvent.setup();
51+
render(<TestSignupForm />);
52+
53+
const emailInput = screen.getByTestId("email-input");
54+
await user.type(emailInput, "test");
55+
56+
expect(screen.queryByTestId("email-error")).not.toBeInTheDocument();
57+
});
58+
59+
it("should show email error after the field is blurred with invalid value", async () => {
60+
const user = userEvent.setup();
61+
render(<TestSignupForm />);
62+
63+
const emailInput = screen.getByTestId("email-input");
64+
await user.type(emailInput, "invalid-email");
65+
fireEvent.blur(emailInput);
66+
67+
await waitFor(() => {
68+
expect(screen.getByTestId("email-error")).toBeInTheDocument();
69+
});
70+
});
71+
72+
it("should not show email error if field is blurred with valid email", async () => {
73+
const user = userEvent.setup();
74+
render(<TestSignupForm />);
75+
76+
const emailInput = screen.getByTestId("email-input");
77+
await user.type(emailInput, "test@example.com");
78+
fireEvent.blur(emailInput);
79+
80+
await waitFor(() => {
81+
expect(screen.queryByTestId("email-error")).not.toBeInTheDocument();
82+
});
83+
});
84+
85+
it("should revalidate on each keystroke after the field has been touched and blurred", async () => {
86+
const user = userEvent.setup();
87+
render(<TestSignupForm />);
88+
89+
const emailInput = screen.getByTestId("email-input");
90+
91+
await user.type(emailInput, "bad");
92+
fireEvent.blur(emailInput);
93+
await waitFor(() => {
94+
expect(screen.getByTestId("email-error")).toBeInTheDocument();
95+
});
96+
97+
await user.clear(emailInput);
98+
await user.type(emailInput, "valid@email.com");
99+
await waitFor(() => {
100+
expect(screen.queryByTestId("email-error")).not.toBeInTheDocument();
101+
});
102+
});
103+
});

apps/web/modules/signup-view.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -200,7 +200,7 @@ export default function Signup({
200200
const formMethods = useForm<FormValues>({
201201
resolver: zodResolver(signupSchema),
202202
defaultValues: prepopulateFormValues satisfies FormValues,
203-
mode: "onChange",
203+
mode: "onTouched",
204204
});
205205
const {
206206
register,

0 commit comments

Comments
 (0)