import { render, screen, fireEvent } from "@testing-library/react"; import { describe, expect, it, vi, beforeEach } from "vitest"; import { BrowserRouter } from "react-router-dom"; import LoginForm from "../auth/LoginForm"; // Mock react-router-dom Link component vi.mock("react-router-dom", async () => { const actual = await vi.importActual("react-router-dom"); return { ...actual, Link: ({ to, children, className }: any) => ( {children} ), }; }); // Wrapper component for Router context const Wrapper = ({ children }: { children: React.ReactNode }) => ( {children} ); describe("LoginForm", () => { const mockSetEmail = vi.fn(); const mockSetPassword = vi.fn(); const mockSetShowPassword = vi.fn(); const mockHandleLogin = vi.fn(); const defaultProps = { email: "", setEmail: mockSetEmail, password: "", setPassword: mockSetPassword, showPassword: false, setShowPassword: mockSetShowPassword, isLoading: false, isSettingUpTables: false, loginError: null, handleLogin: mockHandleLogin, }; beforeEach(() => { vi.clearAllMocks(); }); describe("rendering", () => { it("renders the login form with all fields", () => { render(, { wrapper: Wrapper }); expect(screen.getByTestId("login-form")).toBeInTheDocument(); expect(screen.getByLabelText("이메일")).toBeInTheDocument(); expect(screen.getByLabelText("비밀번호")).toBeInTheDocument(); expect(screen.getByText("로그인")).toBeInTheDocument(); expect(screen.getByTestId("forgot-password-link")).toBeInTheDocument(); }); it("renders email field with correct attributes", () => { render(, { wrapper: Wrapper }); const emailInput = screen.getByLabelText("이메일"); expect(emailInput).toHaveAttribute("type", "email"); expect(emailInput).toHaveAttribute("id", "email"); expect(emailInput).toHaveAttribute("placeholder", "your@email.com"); }); it("renders password field with correct attributes", () => { render(, { wrapper: Wrapper }); const passwordInput = screen.getByLabelText("비밀번호"); expect(passwordInput).toHaveAttribute("type", "password"); expect(passwordInput).toHaveAttribute("id", "password"); expect(passwordInput).toHaveAttribute("placeholder", "••••••••"); }); it("renders forgot password link", () => { render(, { wrapper: Wrapper }); const forgotLink = screen.getByTestId("forgot-password-link"); expect(forgotLink).toHaveAttribute("href", "/forgot-password"); expect(forgotLink).toHaveTextContent("비밀번호를 잊으셨나요?"); }); }); describe("form values", () => { it("displays current email value", () => { render(, { wrapper: Wrapper, }); expect(screen.getByDisplayValue("test@example.com")).toBeInTheDocument(); }); it("displays current password value", () => { render(, { wrapper: Wrapper, }); expect(screen.getByDisplayValue("mypassword")).toBeInTheDocument(); }); it("calls setEmail when email input changes", () => { render(, { wrapper: Wrapper }); const emailInput = screen.getByLabelText("이메일"); fireEvent.change(emailInput, { target: { value: "test@example.com" } }); expect(mockSetEmail).toHaveBeenCalledWith("test@example.com"); }); it("calls setPassword when password input changes", () => { render(, { wrapper: Wrapper }); const passwordInput = screen.getByLabelText("비밀번호"); fireEvent.change(passwordInput, { target: { value: "newpassword" } }); expect(mockSetPassword).toHaveBeenCalledWith("newpassword"); }); }); describe("password visibility toggle", () => { it("shows password as hidden by default", () => { render(, { wrapper: Wrapper }); const passwordInput = screen.getByLabelText("비밀번호"); expect(passwordInput).toHaveAttribute("type", "password"); }); it("shows password as text when showPassword is true", () => { render(, { wrapper: Wrapper, }); const passwordInput = screen.getByLabelText("비밀번호"); expect(passwordInput).toHaveAttribute("type", "text"); }); it("calls setShowPassword when visibility toggle is clicked", () => { render(, { wrapper: Wrapper }); // Find the password toggle button (the one that's not the submit button) const buttons = screen.getAllByRole("button"); const toggleButton = buttons.find( (btn) => btn.getAttribute("type") === "button" ); fireEvent.click(toggleButton!); expect(mockSetShowPassword).toHaveBeenCalledWith(true); }); it("calls setShowPassword with opposite value", () => { render(, { wrapper: Wrapper, }); // Find the password toggle button (the one that's not the submit button) const buttons = screen.getAllByRole("button"); const toggleButton = buttons.find( (btn) => btn.getAttribute("type") === "button" ); fireEvent.click(toggleButton!); expect(mockSetShowPassword).toHaveBeenCalledWith(false); }); }); describe("form submission", () => { it("calls handleLogin when form is submitted", () => { render(, { wrapper: Wrapper }); const form = screen.getByTestId("login-form"); fireEvent.submit(form); expect(mockHandleLogin).toHaveBeenCalledTimes(1); }); it("does not submit when form is disabled during loading", () => { render(, { wrapper: Wrapper, }); const loginButton = screen.getByText("로그인 중..."); expect(loginButton).toBeDisabled(); }); it("does not submit when form is disabled during table setup", () => { render(, { wrapper: Wrapper, }); const loginButton = screen.getByText("데이터베이스 설정 중..."); expect(loginButton).toBeDisabled(); }); }); describe("loading states", () => { it("shows loading text when isLoading is true", () => { render(, { wrapper: Wrapper, }); expect(screen.getByText("로그인 중...")).toBeInTheDocument(); const submitButton = screen.getByText("로그인 중...").closest("button"); expect(submitButton).toBeDisabled(); }); it("shows table setup text when isSettingUpTables is true", () => { render(, { wrapper: Wrapper, }); expect(screen.getByText("데이터베이스 설정 중...")).toBeInTheDocument(); const submitButton = screen .getByText("데이터베이스 설정 중...") .closest("button"); expect(submitButton).toBeDisabled(); }); it("shows normal text when not loading", () => { render(, { wrapper: Wrapper }); expect(screen.getByText("로그인")).toBeInTheDocument(); const submitButton = screen.getByText("로그인").closest("button"); expect(submitButton).not.toBeDisabled(); }); it("isLoading takes precedence over isSettingUpTables", () => { render( , { wrapper: Wrapper } ); expect(screen.getByText("로그인 중...")).toBeInTheDocument(); }); }); describe("error handling", () => { it("does not show error message when loginError is null", () => { render(, { wrapper: Wrapper }); expect(screen.queryByText(/에러/)).not.toBeInTheDocument(); }); it("shows regular error message for standard errors", () => { const errorMessage = "잘못된 이메일 또는 비밀번호입니다."; render(, { wrapper: Wrapper, }); expect(screen.getByText(errorMessage)).toBeInTheDocument(); }); it("shows CORS/JSON error with special styling and suggestions", () => { const corsError = "CORS 정책에 의해 차단되었습니다."; render(, { wrapper: Wrapper, }); expect(screen.getByText(corsError)).toBeInTheDocument(); expect( screen.getByText(/다른 CORS 프록시 유형을 시도해 보세요/) ).toBeInTheDocument(); expect( screen.getByText(/HTTPS URL을 사용하는 Supabase 인스턴스로 변경/) ).toBeInTheDocument(); expect( screen.getByText(/네트워크 연결 상태를 확인하세요/) ).toBeInTheDocument(); }); it("detects JSON errors correctly", () => { const jsonError = "JSON 파싱 오류가 발생했습니다."; render(, { wrapper: Wrapper, }); expect(screen.getByText(jsonError)).toBeInTheDocument(); expect( screen.getByText(/다른 CORS 프록시 유형을 시도해 보세요/) ).toBeInTheDocument(); }); it("detects network 404 errors correctly", () => { const networkError = "404 Not Found 오류입니다."; render(, { wrapper: Wrapper, }); expect(screen.getByText(networkError)).toBeInTheDocument(); expect( screen.getByText(/네트워크 연결 상태를 확인하세요/) ).toBeInTheDocument(); }); it("detects proxy errors correctly", () => { const proxyError = "프록시 서버 응답 오류입니다."; render(, { wrapper: Wrapper, }); expect(screen.getByText(proxyError)).toBeInTheDocument(); expect( screen.getByText(/다른 CORS 프록시 유형을 시도해 보세요/) ).toBeInTheDocument(); }); }); describe("CSS classes and styling", () => { it("applies correct CSS classes to form container", () => { render(, { wrapper: Wrapper }); const container = screen.getByTestId("login-form").parentElement; expect(container).toHaveClass("neuro-flat", "p-8", "mb-6"); }); it("applies correct CSS classes to email input", () => { render(, { wrapper: Wrapper }); const emailInput = screen.getByLabelText("이메일"); expect(emailInput).toHaveClass("pl-10", "neuro-pressed"); }); it("applies correct CSS classes to password input", () => { render(, { wrapper: Wrapper }); const passwordInput = screen.getByLabelText("비밀번호"); expect(passwordInput).toHaveClass("pl-10", "neuro-pressed"); }); it("applies correct CSS classes to submit button", () => { render(, { wrapper: Wrapper }); const submitButton = screen.getByRole("button", { name: /로그인/ }); expect(submitButton).toHaveClass( "w-full", "hover:bg-neuro-income/80", "text-white", "h-auto", "bg-neuro-income", "text-lg" ); }); }); describe("accessibility", () => { it("has proper form labels", () => { render(, { wrapper: Wrapper }); expect(screen.getByLabelText("이메일")).toBeInTheDocument(); expect(screen.getByLabelText("비밀번호")).toBeInTheDocument(); }); it("has proper input IDs matching labels", () => { render(, { wrapper: Wrapper }); const emailInput = screen.getByLabelText("이메일"); const passwordInput = screen.getByLabelText("비밀번호"); expect(emailInput).toHaveAttribute("id", "email"); expect(passwordInput).toHaveAttribute("id", "password"); }); it("password toggle button has correct type", () => { render(, { wrapper: Wrapper }); // Find the eye icon button (the one that's not the submit button) const buttons = screen.getAllByRole("button"); const toggleButton = buttons.find( (button) => button.getAttribute("type") === "button" ); expect(toggleButton).toHaveAttribute("type", "button"); }); it("submit button has correct type", () => { render(, { wrapper: Wrapper }); const submitButton = screen.getByText("로그인").closest("button"); expect(submitButton).toHaveAttribute("type", "submit"); }); }); describe("edge cases", () => { it("handles empty email and password values", () => { render(, { wrapper: Wrapper, }); const emailInput = screen.getByLabelText("이메일"); const passwordInput = screen.getByLabelText("비밀번호"); expect(emailInput).toHaveValue(""); expect(passwordInput).toHaveValue(""); }); it("handles very long error messages", () => { const longError = "A".repeat(1000); render(, { wrapper: Wrapper, }); expect(screen.getByText(longError)).toBeInTheDocument(); }); it("handles special characters in email and password", () => { const specialEmail = "test+tag@example-domain.co.uk"; const specialPassword = "P@ssw0rd!#$%"; render( , { wrapper: Wrapper } ); expect(screen.getByDisplayValue(specialEmail)).toBeInTheDocument(); expect(screen.getByDisplayValue(specialPassword)).toBeInTheDocument(); }); it("maintains form functionality during rapid state changes", () => { const { rerender } = render(, { wrapper: Wrapper, }); // Rapid state changes rerender(); rerender(); rerender(); rerender(); // Form should still be functional expect(screen.getByTestId("login-form")).toBeInTheDocument(); expect(screen.getByLabelText("이메일")).toBeInTheDocument(); expect(screen.getByLabelText("비밀번호")).toBeInTheDocument(); }); }); });