diff --git a/src/app/(auth)/_components/login-form.tsx b/src/app/(auth)/_components/login-form.tsx index 4533576b..e06ba713 100644 --- a/src/app/(auth)/_components/login-form.tsx +++ b/src/app/(auth)/_components/login-form.tsx @@ -1,4 +1,9 @@ -import type { FieldErrors, UseFormHandleSubmit, UseFormRegister } from "react-hook-form"; +import type { + FieldErrors, + UseFormHandleSubmit, + UseFormRegister, + UseFormWatch, +} from "react-hook-form"; import { Box } from "@mui/material"; import { Button } from "@/components/ui/button"; @@ -6,11 +11,12 @@ import { FormErrorMessage } from "@/components/ui/form-error-message"; import { FormField } from "@/components/ui/form-field"; import { PasswordField } from "@/components/ui/password-field"; import { TextField } from "@/components/ui/text-field"; -import type { LoginFormValues } from "@/domain/auth/schemas/login-schema"; +import { loginSchema, type LoginFormValues } from "@/domain/auth/schemas/login-schema"; export type LoginFormProps = { register: UseFormRegister; handleSubmit: UseFormHandleSubmit; + watch: UseFormWatch; errors: FieldErrors; onSubmit: (values: LoginFormValues) => void; isLoggingIn: boolean; @@ -20,11 +26,15 @@ export type LoginFormProps = { export function LoginForm({ register, handleSubmit, + watch, errors, onSubmit, isLoggingIn, loginError, }: LoginFormProps) { + const [username, password] = watch(["username", "password"]); + const isSubmitDisabled = !loginSchema.safeParse({ username, password }).success; + return ( Sign In diff --git a/src/app/(auth)/login.tsx b/src/app/(auth)/login.tsx index 0047bc06..9149bc8d 100644 --- a/src/app/(auth)/login.tsx +++ b/src/app/(auth)/login.tsx @@ -14,6 +14,7 @@ export default function LoginPage() { const { register, handleSubmit, + watch, formState: { errors }, } = useForm({ resolver: zodResolver(loginSchema), @@ -30,6 +31,7 @@ export default function LoginPage() { { }; } +function createMockWatch( + values: LoginFormValues = { username: "", password: "" }, +): UseFormWatch { + return ((name?: unknown) => { + if (name === undefined) { + return values; + } + + if (Array.isArray(name)) { + return name.map((field) => values[field as keyof LoginFormValues]); + } + + return values[name as keyof LoginFormValues]; + }) as UseFormWatch; +} + type LoginFormHarnessProps = { onSubmit?: (values: LoginFormValues) => void; isLoggingIn?: boolean; @@ -28,6 +44,7 @@ function LoginFormHarness({ const { register, handleSubmit, + watch, formState: { errors }, } = useForm({ resolver: zodResolver(loginSchema), @@ -38,6 +55,7 @@ function LoginFormHarness({ { { { expect(within(form!).getByRole("button", { name: /signing in/i })).toBeDisabled(); }); + it("keeps submit disabled until email and password are valid", async () => { + renderWithProviders(, { withAuth: false }); + + const form = document.querySelector("form"); + expect(form).not.toBeNull(); + + const submitButton = within(form!).getByRole("button", { name: /sign in/i }); + const emailInput = within(form!).getByLabelText("Email"); + const passwordInput = within(form!).getByLabelText("Password"); + + expect(submitButton).toBeDisabled(); + + fireEvent.change(emailInput, { target: { value: "not-an-email" } }); + fireEvent.change(passwordInput, { target: { value: "secret123" } }); + expect(submitButton).toBeDisabled(); + + fireEvent.change(emailInput, { target: { value: "user@seahavenind.com" } }); + expect(submitButton).toBeEnabled(); + }); + it("submits credentials via onSubmit", async () => { const onSubmit = vi.fn(); diff --git a/src/test/domain/auth/schemas/login-schema.test.ts b/src/test/domain/auth/schemas/login-schema.test.ts index 4f66b0c6..5b5ce9b3 100644 --- a/src/test/domain/auth/schemas/login-schema.test.ts +++ b/src/test/domain/auth/schemas/login-schema.test.ts @@ -21,6 +21,15 @@ describe("loginSchema", () => { } }); + it("rejects invalid email", () => { + const result = loginSchema.safeParse({ username: "not-an-email", password: "secret" }); + + expect(result.success).toBe(false); + if (!result.success) { + expect(result.error.flatten().fieldErrors.username).toEqual(["Enter a valid email"]); + } + }); + it("accepts valid credentials", () => { const credentials = { username: "user@seahavenind.com", password: "secret" }; const result = loginSchema.safeParse(credentials);