Files
zellyy-finance/src/components/__tests__/LoginForm.test.tsx
hansoo e72f9e8d26 feat: React 성능 최적화 및 Vercel 배포 시스템 구축 완료
🚀 성능 최적화 (Task 8):
- React.lazy를 활용한 코드 스플리팅 구현
- React.memo, useMemo, useCallback을 통한 메모이제이션 최적화
- 초기 번들 크기 87% 감소 (470kB → 62kB)
- 백그라운드 동기화 간격 최적화 (5분 → 30초)

📦 Vercel 배포 인프라 구축 (Task 9):
- vercel.json 배포 설정 및 보안 헤더 구성
- GitHub Actions 자동 배포 워크플로우 설정
- 환경별 배포 및 미리보기 시스템 구현
- 자동화된 배포 스크립트 및 환경 변수 관리
- 포괄적인 배포 가이드 및 체크리스트 작성

🔧 코드 품질 개선:
- ESLint 주요 오류 수정 (사용하지 않는 변수/import 정리)
- 테스트 커버리지 확장 (229개 테스트 통과)
- TypeScript 타입 안전성 강화
- Prettier 코드 포맷팅 적용

⚠️ 참고: 테스트 파일의 any 타입 및 일부 경고는 향후 개선 예정

🛠️ Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-07-12 20:52:04 +09:00

446 lines
15 KiB
TypeScript

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) => (
<a href={to} className={className} data-testid="forgot-password-link">
{children}
</a>
),
};
});
// Wrapper component for Router context
const Wrapper = ({ children }: { children: React.ReactNode }) => (
<BrowserRouter>{children}</BrowserRouter>
);
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(<LoginForm {...defaultProps} />, { 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(<LoginForm {...defaultProps} />, { 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(<LoginForm {...defaultProps} />, { 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(<LoginForm {...defaultProps} />, { 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(<LoginForm {...defaultProps} email="test@example.com" />, {
wrapper: Wrapper,
});
expect(screen.getByDisplayValue("test@example.com")).toBeInTheDocument();
});
it("displays current password value", () => {
render(<LoginForm {...defaultProps} password="mypassword" />, {
wrapper: Wrapper,
});
expect(screen.getByDisplayValue("mypassword")).toBeInTheDocument();
});
it("calls setEmail when email input changes", () => {
render(<LoginForm {...defaultProps} />, { 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(<LoginForm {...defaultProps} />, { 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(<LoginForm {...defaultProps} />, { wrapper: Wrapper });
const passwordInput = screen.getByLabelText("비밀번호");
expect(passwordInput).toHaveAttribute("type", "password");
});
it("shows password as text when showPassword is true", () => {
render(<LoginForm {...defaultProps} showPassword={true} />, {
wrapper: Wrapper,
});
const passwordInput = screen.getByLabelText("비밀번호");
expect(passwordInput).toHaveAttribute("type", "text");
});
it("calls setShowPassword when visibility toggle is clicked", () => {
render(<LoginForm {...defaultProps} />, { 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(<LoginForm {...defaultProps} showPassword={true} />, {
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(<LoginForm {...defaultProps} />, { 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(<LoginForm {...defaultProps} isLoading={true} />, {
wrapper: Wrapper,
});
const loginButton = screen.getByText("로그인 중...");
expect(loginButton).toBeDisabled();
});
it("does not submit when form is disabled during table setup", () => {
render(<LoginForm {...defaultProps} isSettingUpTables={true} />, {
wrapper: Wrapper,
});
const loginButton = screen.getByText("데이터베이스 설정 중...");
expect(loginButton).toBeDisabled();
});
});
describe("loading states", () => {
it("shows loading text when isLoading is true", () => {
render(<LoginForm {...defaultProps} isLoading={true} />, {
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(<LoginForm {...defaultProps} isSettingUpTables={true} />, {
wrapper: Wrapper,
});
expect(screen.getByText("데이터베이스 설정 중...")).toBeInTheDocument();
const submitButton = screen
.getByText("데이터베이스 설정 중...")
.closest("button");
expect(submitButton).toBeDisabled();
});
it("shows normal text when not loading", () => {
render(<LoginForm {...defaultProps} />, { wrapper: Wrapper });
expect(screen.getByText("로그인")).toBeInTheDocument();
const submitButton = screen.getByText("로그인").closest("button");
expect(submitButton).not.toBeDisabled();
});
it("isLoading takes precedence over isSettingUpTables", () => {
render(
<LoginForm
{...defaultProps}
isLoading={true}
isSettingUpTables={true}
/>,
{ wrapper: Wrapper }
);
expect(screen.getByText("로그인 중...")).toBeInTheDocument();
});
});
describe("error handling", () => {
it("does not show error message when loginError is null", () => {
render(<LoginForm {...defaultProps} />, { wrapper: Wrapper });
expect(screen.queryByText(/에러/)).not.toBeInTheDocument();
});
it("shows regular error message for standard errors", () => {
const errorMessage = "잘못된 이메일 또는 비밀번호입니다.";
render(<LoginForm {...defaultProps} loginError={errorMessage} />, {
wrapper: Wrapper,
});
expect(screen.getByText(errorMessage)).toBeInTheDocument();
});
it("shows CORS/JSON error with special styling and suggestions", () => {
const corsError = "CORS 정책에 의해 차단되었습니다.";
render(<LoginForm {...defaultProps} loginError={corsError} />, {
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(<LoginForm {...defaultProps} loginError={jsonError} />, {
wrapper: Wrapper,
});
expect(screen.getByText(jsonError)).toBeInTheDocument();
expect(
screen.getByText(/다른 CORS 프록시 유형을 시도해 보세요/)
).toBeInTheDocument();
});
it("detects network 404 errors correctly", () => {
const networkError = "404 Not Found 오류입니다.";
render(<LoginForm {...defaultProps} loginError={networkError} />, {
wrapper: Wrapper,
});
expect(screen.getByText(networkError)).toBeInTheDocument();
expect(
screen.getByText(/네트워크 연결 상태를 확인하세요/)
).toBeInTheDocument();
});
it("detects proxy errors correctly", () => {
const proxyError = "프록시 서버 응답 오류입니다.";
render(<LoginForm {...defaultProps} loginError={proxyError} />, {
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(<LoginForm {...defaultProps} />, { 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(<LoginForm {...defaultProps} />, { wrapper: Wrapper });
const emailInput = screen.getByLabelText("이메일");
expect(emailInput).toHaveClass("pl-10", "neuro-pressed");
});
it("applies correct CSS classes to password input", () => {
render(<LoginForm {...defaultProps} />, { wrapper: Wrapper });
const passwordInput = screen.getByLabelText("비밀번호");
expect(passwordInput).toHaveClass("pl-10", "neuro-pressed");
});
it("applies correct CSS classes to submit button", () => {
render(<LoginForm {...defaultProps} />, { 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(<LoginForm {...defaultProps} />, { wrapper: Wrapper });
expect(screen.getByLabelText("이메일")).toBeInTheDocument();
expect(screen.getByLabelText("비밀번호")).toBeInTheDocument();
});
it("has proper input IDs matching labels", () => {
render(<LoginForm {...defaultProps} />, { 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(<LoginForm {...defaultProps} />, { 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(<LoginForm {...defaultProps} />, { wrapper: Wrapper });
const submitButton = screen.getByText("로그인").closest("button");
expect(submitButton).toHaveAttribute("type", "submit");
});
});
describe("edge cases", () => {
it("handles empty email and password values", () => {
render(<LoginForm {...defaultProps} email="" password="" />, {
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(<LoginForm {...defaultProps} loginError={longError} />, {
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(
<LoginForm
{...defaultProps}
email={specialEmail}
password={specialPassword}
/>,
{ wrapper: Wrapper }
);
expect(screen.getByDisplayValue(specialEmail)).toBeInTheDocument();
expect(screen.getByDisplayValue(specialPassword)).toBeInTheDocument();
});
it("maintains form functionality during rapid state changes", () => {
const { rerender } = render(<LoginForm {...defaultProps} />, {
wrapper: Wrapper,
});
// Rapid state changes
rerender(<LoginForm {...defaultProps} isLoading={true} />);
rerender(<LoginForm {...defaultProps} isSettingUpTables={true} />);
rerender(<LoginForm {...defaultProps} loginError="Error" />);
rerender(<LoginForm {...defaultProps} />);
// Form should still be functional
expect(screen.getByTestId("login-form")).toBeInTheDocument();
expect(screen.getByLabelText("이메일")).toBeInTheDocument();
expect(screen.getByLabelText("비밀번호")).toBeInTheDocument();
});
});
});