mirror of
https://github.com/Sea-Haven-Industries/shoc-frontend-new.git
synced 2026-10-07 06:59:01 +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 { Box } from "@mui/material";
|
||||||
|
|
||||||
import { Button } from "@/components/ui/button";
|
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 { FormField } from "@/components/ui/form-field";
|
||||||
import { PasswordField } from "@/components/ui/password-field";
|
import { PasswordField } from "@/components/ui/password-field";
|
||||||
import { TextField } from "@/components/ui/text-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 = {
|
export type LoginFormProps = {
|
||||||
register: UseFormRegister<LoginFormValues>;
|
register: UseFormRegister<LoginFormValues>;
|
||||||
handleSubmit: UseFormHandleSubmit<LoginFormValues>;
|
handleSubmit: UseFormHandleSubmit<LoginFormValues>;
|
||||||
|
watch: UseFormWatch<LoginFormValues>;
|
||||||
errors: FieldErrors<LoginFormValues>;
|
errors: FieldErrors<LoginFormValues>;
|
||||||
onSubmit: (values: LoginFormValues) => void;
|
onSubmit: (values: LoginFormValues) => void;
|
||||||
isLoggingIn: boolean;
|
isLoggingIn: boolean;
|
||||||
|
|
@ -20,11 +26,15 @@ export type LoginFormProps = {
|
||||||
export function LoginForm({
|
export function LoginForm({
|
||||||
register,
|
register,
|
||||||
handleSubmit,
|
handleSubmit,
|
||||||
|
watch,
|
||||||
errors,
|
errors,
|
||||||
onSubmit,
|
onSubmit,
|
||||||
isLoggingIn,
|
isLoggingIn,
|
||||||
loginError,
|
loginError,
|
||||||
}: LoginFormProps) {
|
}: LoginFormProps) {
|
||||||
|
const [username, password] = watch(["username", "password"]);
|
||||||
|
const isSubmitDisabled = !loginSchema.safeParse({ username, password }).success;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box
|
<Box
|
||||||
component="form"
|
component="form"
|
||||||
|
|
@ -74,6 +84,7 @@ export function LoginForm({
|
||||||
size="lg"
|
size="lg"
|
||||||
loading={isLoggingIn}
|
loading={isLoggingIn}
|
||||||
loadingText="Signing in…"
|
loadingText="Signing in…"
|
||||||
|
disabled={isSubmitDisabled}
|
||||||
className="mt-1!"
|
className="mt-1!"
|
||||||
>
|
>
|
||||||
Sign In
|
Sign In
|
||||||
|
|
|
||||||
|
|
@ -14,6 +14,7 @@ export default function LoginPage() {
|
||||||
const {
|
const {
|
||||||
register,
|
register,
|
||||||
handleSubmit,
|
handleSubmit,
|
||||||
|
watch,
|
||||||
formState: { errors },
|
formState: { errors },
|
||||||
} = useForm<LoginFormValues>({
|
} = useForm<LoginFormValues>({
|
||||||
resolver: zodResolver(loginSchema),
|
resolver: zodResolver(loginSchema),
|
||||||
|
|
@ -30,6 +31,7 @@ export default function LoginPage() {
|
||||||
<LoginForm
|
<LoginForm
|
||||||
register={register}
|
register={register}
|
||||||
handleSubmit={handleSubmit}
|
handleSubmit={handleSubmit}
|
||||||
|
watch={watch}
|
||||||
errors={errors}
|
errors={errors}
|
||||||
onSubmit={login}
|
onSubmit={login}
|
||||||
isLoggingIn={isLoggingIn}
|
isLoggingIn={isLoggingIn}
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|
||||||
export const loginSchema = z.object({
|
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"),
|
password: z.string().min(1, "Password is required"),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
import { zodResolver } from "@hookform/resolvers/zod";
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
import { fireEvent, screen, waitFor, within } from "@testing-library/react";
|
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 { describe, expect, it, vi } from "vitest";
|
||||||
|
|
||||||
import { LoginForm, type LoginFormProps } from "@/app/(auth)/_components/login-form";
|
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 = {
|
type LoginFormHarnessProps = {
|
||||||
onSubmit?: (values: LoginFormValues) => void;
|
onSubmit?: (values: LoginFormValues) => void;
|
||||||
isLoggingIn?: boolean;
|
isLoggingIn?: boolean;
|
||||||
|
|
@ -28,6 +44,7 @@ function LoginFormHarness({
|
||||||
const {
|
const {
|
||||||
register,
|
register,
|
||||||
handleSubmit,
|
handleSubmit,
|
||||||
|
watch,
|
||||||
formState: { errors },
|
formState: { errors },
|
||||||
} = useForm<LoginFormValues>({
|
} = useForm<LoginFormValues>({
|
||||||
resolver: zodResolver(loginSchema),
|
resolver: zodResolver(loginSchema),
|
||||||
|
|
@ -38,6 +55,7 @@ function LoginFormHarness({
|
||||||
<LoginForm
|
<LoginForm
|
||||||
register={register}
|
register={register}
|
||||||
handleSubmit={handleSubmit}
|
handleSubmit={handleSubmit}
|
||||||
|
watch={watch}
|
||||||
errors={errors}
|
errors={errors}
|
||||||
onSubmit={onSubmit}
|
onSubmit={onSubmit}
|
||||||
isLoggingIn={isLoggingIn}
|
isLoggingIn={isLoggingIn}
|
||||||
|
|
@ -62,6 +80,7 @@ describe("LoginForm", () => {
|
||||||
<LoginForm
|
<LoginForm
|
||||||
register={vi.fn()}
|
register={vi.fn()}
|
||||||
handleSubmit={createMockHandleSubmit()}
|
handleSubmit={createMockHandleSubmit()}
|
||||||
|
watch={createMockWatch()}
|
||||||
errors={{
|
errors={{
|
||||||
username: { type: "required", message: "Email is required" },
|
username: { type: "required", message: "Email is required" },
|
||||||
password: { type: "required", message: "Password is required" },
|
password: { type: "required", message: "Password is required" },
|
||||||
|
|
@ -97,6 +116,7 @@ describe("LoginForm", () => {
|
||||||
<LoginForm
|
<LoginForm
|
||||||
register={register as LoginFormProps["register"]}
|
register={register as LoginFormProps["register"]}
|
||||||
handleSubmit={createMockHandleSubmit()}
|
handleSubmit={createMockHandleSubmit()}
|
||||||
|
watch={createMockWatch({ username: "user@seahavenind.com", password: "secret" })}
|
||||||
errors={{}}
|
errors={{}}
|
||||||
onSubmit={vi.fn()}
|
onSubmit={vi.fn()}
|
||||||
isLoggingIn
|
isLoggingIn
|
||||||
|
|
@ -117,6 +137,26 @@ describe("LoginForm", () => {
|
||||||
expect(within(form!).getByRole("button", { name: /signing in/i })).toBeDisabled();
|
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 () => {
|
it("submits credentials via onSubmit", async () => {
|
||||||
const onSubmit = vi.fn();
|
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", () => {
|
it("accepts valid credentials", () => {
|
||||||
const credentials = { username: "user@seahavenind.com", password: "secret" };
|
const credentials = { username: "user@seahavenind.com", password: "secret" };
|
||||||
const result = loginSchema.safeParse(credentials);
|
const result = loginSchema.safeParse(credentials);
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue