mirror of
https://github.com/Sea-Haven-Industries/shoc-frontend-new.git
synced 2026-10-05 22:22:08 +00:00
feat(auth): disable login submit until email and password are valid
This commit is contained in:
parent
43b53bb610
commit
9a3b49f6e2
5 changed files with 66 additions and 4 deletions
|
|
@ -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<LoginFormValues>;
|
||||
handleSubmit: UseFormHandleSubmit<LoginFormValues>;
|
||||
watch: UseFormWatch<LoginFormValues>;
|
||||
errors: FieldErrors<LoginFormValues>;
|
||||
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 (
|
||||
<Box
|
||||
component="form"
|
||||
|
|
@ -74,6 +84,7 @@ export function LoginForm({
|
|||
size="lg"
|
||||
loading={isLoggingIn}
|
||||
loadingText="Signing in…"
|
||||
disabled={isSubmitDisabled}
|
||||
className="mt-1!"
|
||||
>
|
||||
Sign In
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@ export default function LoginPage() {
|
|||
const {
|
||||
register,
|
||||
handleSubmit,
|
||||
watch,
|
||||
formState: { errors },
|
||||
} = useForm<LoginFormValues>({
|
||||
resolver: zodResolver(loginSchema),
|
||||
|
|
@ -30,6 +31,7 @@ export default function LoginPage() {
|
|||
<LoginForm
|
||||
register={register}
|
||||
handleSubmit={handleSubmit}
|
||||
watch={watch}
|
||||
errors={errors}
|
||||
onSubmit={login}
|
||||
isLoggingIn={isLoggingIn}
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import { z } from "zod";
|
||||
|
||||
export const loginSchema = z.object({
|
||||
username: z.string().min(1, "Email is required"),
|
||||
username: z.string().min(1, "Email is required").pipe(z.email("Enter a valid email")),
|
||||
password: z.string().min(1, "Password is required"),
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { fireEvent, screen, waitFor, within } from "@testing-library/react";
|
||||
import { useForm, type UseFormHandleSubmit } from "react-hook-form";
|
||||
import { useForm, type UseFormHandleSubmit, type UseFormWatch } from "react-hook-form";
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
|
||||
import { LoginForm, type LoginFormProps } from "@/app/(auth)/_components/login-form";
|
||||
|
|
@ -14,6 +14,22 @@ function createMockHandleSubmit(): UseFormHandleSubmit<LoginFormValues> {
|
|||
};
|
||||
}
|
||||
|
||||
function createMockWatch(
|
||||
values: LoginFormValues = { username: "", password: "" },
|
||||
): UseFormWatch<LoginFormValues> {
|
||||
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<LoginFormValues>;
|
||||
}
|
||||
|
||||
type LoginFormHarnessProps = {
|
||||
onSubmit?: (values: LoginFormValues) => void;
|
||||
isLoggingIn?: boolean;
|
||||
|
|
@ -28,6 +44,7 @@ function LoginFormHarness({
|
|||
const {
|
||||
register,
|
||||
handleSubmit,
|
||||
watch,
|
||||
formState: { errors },
|
||||
} = useForm<LoginFormValues>({
|
||||
resolver: zodResolver(loginSchema),
|
||||
|
|
@ -38,6 +55,7 @@ function LoginFormHarness({
|
|||
<LoginForm
|
||||
register={register}
|
||||
handleSubmit={handleSubmit}
|
||||
watch={watch}
|
||||
errors={errors}
|
||||
onSubmit={onSubmit}
|
||||
isLoggingIn={isLoggingIn}
|
||||
|
|
@ -62,6 +80,7 @@ describe("LoginForm", () => {
|
|||
<LoginForm
|
||||
register={vi.fn()}
|
||||
handleSubmit={createMockHandleSubmit()}
|
||||
watch={createMockWatch()}
|
||||
errors={{
|
||||
username: { type: "required", message: "Email is required" },
|
||||
password: { type: "required", message: "Password is required" },
|
||||
|
|
@ -97,6 +116,7 @@ describe("LoginForm", () => {
|
|||
<LoginForm
|
||||
register={register as LoginFormProps["register"]}
|
||||
handleSubmit={createMockHandleSubmit()}
|
||||
watch={createMockWatch({ username: "user@seahavenind.com", password: "secret" })}
|
||||
errors={{}}
|
||||
onSubmit={vi.fn()}
|
||||
isLoggingIn
|
||||
|
|
@ -117,6 +137,26 @@ describe("LoginForm", () => {
|
|||
expect(within(form!).getByRole("button", { name: /signing in/i })).toBeDisabled();
|
||||
});
|
||||
|
||||
it("keeps submit disabled until email and password are valid", async () => {
|
||||
renderWithProviders(<LoginFormHarness />, { 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();
|
||||
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue