diff --git a/package.json b/package.json index 082e808..4e7f1a4 100644 --- a/package.json +++ b/package.json @@ -8,7 +8,7 @@ "build": "next build", "start": "next start", "lint": "next lint", - "test": "vitest --passWithNoTests", + "test": "vitest", "cy:open": "wait-on http://localhost:3000/ && cypress open", "cy:run": "npm-run-all --parallel dev cy:open" }, diff --git a/src/app/(auth)/components/InputField.test.tsx b/src/app/(auth)/components/InputField.test.tsx new file mode 100644 index 0000000..5009199 --- /dev/null +++ b/src/app/(auth)/components/InputField.test.tsx @@ -0,0 +1,63 @@ +import { describe, expect, vi } from "vitest"; +import { useForm } from "react-hook-form"; +import { fireEvent, render, screen } from "@testing-library/react"; + +import InputField from "./InputField"; + +type TestFormValues = { + nickname: string; +}; + +describe("InputField Component", () => { + const TestInputField = () => { + const { + register, + trigger, + watch, + formState: { errors }, + } = useForm(); + + return ( + + ); + }; + + it("InputField 렌더링 합니다.", () => { + render(); + expect(screen.getByLabelText("Nickname")).toBeInTheDocument(); + }); + + it("입력이 유효하지 않으면 오류 메시지를 표시합니다.", async () => { + const mockTrigger = vi.fn().mockResolvedValue(false); + render(); + + const input = screen.getByLabelText("Nickname") as HTMLInputElement; + fireEvent.blur(input); + + await mockTrigger("nickname"); + expect(mockTrigger).toHaveBeenCalledWith("nickname"); + + const errorElement = screen.getByRole("alert"); + expect(errorElement).toBeInTheDocument(); + }); + + it("blur() 되었을 때, InputField 유효성을 검사합니다.", async () => { + const mockTrigger = vi.fn().mockResolvedValue(true); + render(); + + const input = screen.getByLabelText("Nickname") as HTMLInputElement; + fireEvent.blur(input); + + await mockTrigger("nickname"); + expect(mockTrigger).toHaveBeenCalledWith("nickname"); + }); +}); diff --git a/src/app/(auth)/components/InputField.tsx b/src/app/(auth)/components/InputField.tsx index 912c26c..db1a4eb 100644 --- a/src/app/(auth)/components/InputField.tsx +++ b/src/app/(auth)/components/InputField.tsx @@ -74,7 +74,9 @@ export default function InputField({ aria-required="true" onBlur={handleBlur} // 입력 필드에서 포커스가 벗어날 때 handleBlur를 호출합니다. /> - {errorText} + + {errorText} + ); } diff --git a/src/app/(auth)/components/SignupForm.test.tsx b/src/app/(auth)/components/SignupForm.test.tsx new file mode 100644 index 0000000..1a83c5c --- /dev/null +++ b/src/app/(auth)/components/SignupForm.test.tsx @@ -0,0 +1,157 @@ +import { render, screen, waitFor } from "@testing-library/react"; +import { vi, Mock } from "vitest"; +import userEvent from "@testing-library/user-event"; + +import { useSignup } from "@/hook/useSignup"; + +import SignupForm from "./SignupForm"; + +// Mock modules +vi.mock("next/navigation", () => ({ + useRouter: () => ({ + push: vi.fn(), + }), +})); + +vi.mock("@/hook/useSignup", () => ({ + useSignup: vi.fn(), +})); + +vi.mock("react-toastify", () => ({ + toast: { + success: vi.fn(), + error: vi.fn(), + }, +})); + +describe("SignupForm", () => { + const mockMutate = vi.fn(); + + beforeEach(() => { + vi.clearAllMocks(); + (useSignup as Mock).mockReturnValue({ + mutate: mockMutate, + isLoading: false, + }); + }); + + describe("렌더링 테스트", () => { + it("모든 입력 필드와 제출 버튼이 렌더링되어야 한다", () => { + render(); + + expect(screen.getByLabelText("닉네임")).toBeInTheDocument(); + expect(screen.getByLabelText("아이디")).toBeInTheDocument(); + expect(screen.getByLabelText("비밀번호")).toBeInTheDocument(); + expect(screen.getByLabelText("비밀번호 확인")).toBeInTheDocument(); + expect(screen.getByText("회원가입하기")).toBeInTheDocument(); + }); + + it("페이지 로드시 닉네임 입력 필드에 포커스가 되어야 한다", () => { + render(); + + expect(screen.getByLabelText("닉네임")).toHaveFocus(); + }); + }); + + describe("유효성 검사", () => { + it("닉네임이 2자 미만일 경우 에러 메시지를 표시해야 한다", async () => { + render(); + const nicknameInput = screen.getByLabelText("닉네임"); + + await userEvent.type(nicknameInput, "a"); + await userEvent.tab(); + + expect(await screen.findByText("닉네임은 최소 2자 이상이어야 합니다.")).toBeInTheDocument(); + }); + + it("이메일 형식이 잘못된 경우 에러 메시지를 표시해야 한다", async () => { + render(); + const emailInput = screen.getByLabelText("아이디"); + + await userEvent.type(emailInput, "invalid-email"); + await userEvent.tab(); + + expect(await screen.findByText("유효한 이메일 주소를 입력해 주세요.")).toBeInTheDocument(); + }); + + it("비밀번호가 8자 미만일 경우 에러 메시지를 표시해야 한다", async () => { + render(); + const passwordInput = screen.getByLabelText("비밀번호"); + + await userEvent.type(passwordInput, "1234567"); + await userEvent.tab(); + + expect(await screen.findByText("비밀번호는 최소 8자 이상이어야 합니다.")).toBeInTheDocument(); + }); + + it("비밀번호와 비밀번호 확인이 일치하지 않을 경우 에러 메시지를 표시해야 한다", async () => { + render(); + const passwordInput = screen.getByLabelText("비밀번호"); + const passwordConfirmInput = screen.getByLabelText("비밀번호 확인"); + + await userEvent.type(passwordInput, "password123"); + await userEvent.type(passwordConfirmInput, "password456"); + await userEvent.tab(); + + expect(await screen.findByText("비밀번호가 일치하지 않습니다.")).toBeInTheDocument(); + }); + }); + + describe("폼 제출", () => { + const validFormData = { + nickname: "유저", + email: "test@example.com", + password: "password123", + passwordConfirm: "password123", + }; + + it("유효한 데이터로 폼 제출시 회원가입 mutation이 호출되어야 한다", async () => { + render(); + + await userEvent.type(screen.getByLabelText("닉네임"), validFormData.nickname); + await userEvent.type(screen.getByLabelText("아이디"), validFormData.email); + await userEvent.type(screen.getByLabelText("비밀번호"), validFormData.password); + await userEvent.type(screen.getByLabelText("비밀번호 확인"), validFormData.passwordConfirm); + + const submitButton = screen.getByText("회원가입하기"); + await userEvent.click(submitButton); + + await waitFor(() => { + expect(mockMutate).toHaveBeenCalledWith( + expect.objectContaining({ + nickname: validFormData.nickname, + email: validFormData.email, + password: validFormData.password, + passwordConfirm: validFormData.passwordConfirm, + }), + expect.any(Object), + ); + }); + }); + + it("이미 사용 중인 이메일로 가입 시도시 에러 메시지를 표시해야 한다", async () => { + const mockError = { + isAxiosError: true, + response: { + status: 409, + }, + }; + + mockMutate.mockImplementation((_, options) => { + options.onError(mockError); + }); + + render(); + + await userEvent.type(screen.getByLabelText("닉네임"), validFormData.nickname); + await userEvent.type(screen.getByLabelText("아이디"), validFormData.email); + await userEvent.type(screen.getByLabelText("비밀번호"), validFormData.password); + await userEvent.type(screen.getByLabelText("비밀번호 확인"), validFormData.passwordConfirm); + + const submitButton = screen.getByText("회원가입하기"); + await userEvent.click(submitButton); + + expect(await screen.findByText("이미 사용 중인 이메일입니다.")).toBeInTheDocument(); + }); + }); +}); diff --git a/src/hook/useSignup.tsx b/src/hook/useSignup.tsx index 0c85f9e..49642a3 100644 --- a/src/hook/useSignup.tsx +++ b/src/hook/useSignup.tsx @@ -7,7 +7,6 @@ import { ErrorType } from "@/api/goalAPI"; export const useSignup = () => { const router = useRouter(); - console.log("1"); return useMutation({ mutationFn: signup,