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();
});
});
});