Skip to content

✅ test: admin 컴포넌트 테스트 코드 추가 #29

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 7 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 13 additions & 14 deletions client/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions client/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
"@radix-ui/react-tooltip": "^1.1.3",
"@tanstack/react-query": "^5.59.20",
"@tanstack/react-query-devtools": "^5.59.20",
"@testing-library/user-event": "^14.6.0",
"avvvatars-react": "^0.4.2",
"axios": "^1.7.7",
"class-variance-authority": "^0.7.0",
Expand Down
24 changes: 24 additions & 0 deletions client/src/__tests__/__mocks__/components/ui/DropdownMenu.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
export const mockDropdownMenu = {
DropdownMenu: ({ children }: { children: React.ReactNode }) => {
return <div>{children}</div>;
},
DropdownMenuTrigger: ({ children }: { children: React.ReactNode; asChild?: boolean }) => {
return <div>{children}</div>;
},
DropdownMenuContent: ({ children }: { children: React.ReactNode }) => {
return <div>{children}</div>;
},
DropdownMenuItem: ({
children,
onClick,
className,
}: {
children: React.ReactNode;
onClick?: () => void;
className?: string;
}) => (
<div role="menuitem" className={className} data-testid="logout-button" onClick={onClick}>
{children}
</div>
),
};
6 changes: 6 additions & 0 deletions client/src/__tests__/__mocks__/external/lucide-react.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,4 +9,10 @@ export const mockLucideIcons = {
ChevronLeft: () => <div data-testid="chevron-left-icon">Mock Chevron Left Icon</div>,
ChevronRight: () => <div data-testid="chevron-right-icon">Mock Chevron Right Icon</div>,
MoreHorizontal: () => <div data-testid="more-horizontal-icon">Mock More Horizontal Icon</div>,
LogOut: () => <div data-testid="logout-icon">Mock Logout Icon</div>,
Eye: () => <div data-testid="eye-icon">Mock Eye Icon</div>,
EyeOff: () => <div data-testid="eyeoff-icon">Mock EyeOff Icon</div>,
CheckCircle: () => <div data-testid="check-circle-icon">Mock Check Circle Icon</div>,
XCircle: () => <div data-testid="x-circle-icon">Mock X Circle Icon</div>,
TriangleAlert: () => <div data-testid="triangle-alert-icon">Mock Triangle Alert Icon</div>,
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
import { render, screen, fireEvent } from "@testing-library/react";

import { AdminHeader } from "@/components/admin/layout/AdminHeader";
import userEvent from "@testing-library/user-event";
import { auth } from "@/api/services/admin/auth";

vi.mock("@/api/services/admin/auth", () => ({
auth: {
logout: vi.fn(),
},
}));

vi.mock("@/components/admin/layout/AdminNavigationMenu", () => ({
AdminNavigationMenu: ({ handleTap }: { handleTap: (tap: "RSS" | "MEMBER") => void }) => (
<div data-testid="admin-nav-menu">
<button onClick={() => handleTap("RSS")}>RSS</button>
<button onClick={() => handleTap("MEMBER")}>MEMBER</button>
</div>
),
}));

describe("AdminHeader 컴포넌트", () => {
const mockSetLogin = vi.fn();
const mockHandleTap = vi.fn();
const originalLocation = window.location;

beforeEach(() => {
vi.clearAllMocks();
Object.defineProperty(window, "location", {
configurable: true,
value: { reload: vi.fn() },
});
});

afterEach(() => {
Object.defineProperty(window, "location", {
configurable: true,
value: originalLocation,
});
});

it("로고와 네비게이션 메뉴가 정상적으로 렌더링되어야 한다", () => {
render(<AdminHeader setLogin={mockSetLogin} handleTap={mockHandleTap} />);

expect(screen.getByAltText("Logo")).toBeInTheDocument();
expect(screen.getByTestId("admin-nav-menu")).toBeInTheDocument();
expect(screen.getByTestId("user-icon")).toBeInTheDocument();
});

it("로고 클릭 시 페이지가 새로고침되어야 한다", () => {
render(<AdminHeader setLogin={mockSetLogin} handleTap={mockHandleTap} />);

const logo = screen.getByAltText("Logo");
fireEvent.click(logo);

expect(window.location.reload).toHaveBeenCalled();
});

it("네비게이션 메뉴 클릭 시 올바른 탭으로 전환되어야 한다", () => {
render(<AdminHeader setLogin={mockSetLogin} handleTap={mockHandleTap} />);

fireEvent.click(screen.getByText("RSS"));
expect(mockHandleTap).toHaveBeenCalledWith("RSS");

fireEvent.click(screen.getByText("MEMBER"));
expect(mockHandleTap).toHaveBeenCalledWith("MEMBER");
});


it("로그아웃 버튼 클릭 시 로그아웃 처리가 되어야 한다", async () => {
render(<AdminHeader setLogin={mockSetLogin} handleTap={mockHandleTap} />);

await userEvent.click(screen.getByTestId("user-menu-button"));

const logoutButton = await screen.findByTestId("logout-button");

await userEvent.click(logoutButton);

expect(auth.logout).toHaveBeenCalledTimes(1);
expect(mockSetLogin).toHaveBeenCalledTimes(1);
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
import { describe, it, expect, vi, beforeEach, Mock } from "vitest";
import { render, screen, fireEvent } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import AdminMember from "@/components/admin/layout/AdminMember";

vi.mock("@/hooks/queries/useAdminAuth", () => ({
useAdminRegister: (onSuccess: Mock, onError: Mock) => ({
mutate: (data: { loginId: string; password: string }) => {
if (data.loginId === "fail") {
onError({ response: { data: "아이디가 올바르지 않습니다." } });
} else {
onSuccess({ message: "생성완료" });
}
},
}),
}));

const mockAlert = vi.fn();
vi.stubGlobal("alert", mockAlert);

describe("AdminMember", () => {
const queryClient = new QueryClient();

beforeEach(() => {
vi.clearAllMocks();
});

const setup = () =>
render(
<QueryClientProvider client={queryClient}>
<AdminMember />
</QueryClientProvider>
);

it("컴포넌트가 초기 렌더링될 때 기본 요소가 표시되어야 한다", () => {
setup();
expect(screen.getByText("관리자 계정 생성")).toBeInTheDocument();
expect(screen.getByLabelText("ID")).toBeInTheDocument();
expect(screen.getByLabelText("Password")).toBeInTheDocument();
expect(screen.getByRole("button", { name: "가입" })).toBeInTheDocument();
});

it("Password Toggle 버튼 클릭 시 비밀번호 표시/숨기기가 전환되어야 한다", async () => {
setup();
const toggleButton = screen.getByRole("button", { name: "Toggle bold" });
const passwordInput = screen.getByLabelText("Password") as HTMLInputElement;

expect(passwordInput.type).toBe("password");
await userEvent.click(toggleButton);
expect(passwordInput.type).toBe("text");
await userEvent.click(toggleButton);
expect(passwordInput.type).toBe("password");
});

it("ID와 Password 입력 후 가입 시 onSuccess가 호출되어 폼이 초기화되어야 한다", async () => {
setup();
const idInput = screen.getByLabelText("ID") as HTMLInputElement;
const pwInput = screen.getByLabelText("Password") as HTMLInputElement;
const submitBtn = screen.getByRole("button", { name: "가입" });

await userEvent.type(idInput, "admin");
await userEvent.type(pwInput, "adminpw");
fireEvent.click(submitBtn);

expect(mockAlert).toHaveBeenCalledWith("관리자 등록 성공: 생성완료");
expect(idInput.value).toBe("");
expect(pwInput.value).toBe("");
});

it("ID가 'fail'일 때 가입 시 onError가 호출되어 에러 알림이 표시되어야 한다", async () => {
setup();
const idInput = screen.getByLabelText("ID");
const pwInput = screen.getByLabelText("Password");
const submitBtn = screen.getByRole("button", { name: "가입" });

await userEvent.type(idInput, "fail");
await userEvent.type(pwInput, "somepassword");
fireEvent.click(submitBtn);

expect(mockAlert).toHaveBeenCalledWith(
'관리자 등록 실패: "아이디가 올바르지 않습니다."'
);
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import { describe, it, expect, vi } from "vitest";
import { render, screen, fireEvent } from "@testing-library/react";
import { AdminNavigationMenu, TAB_TYPES } from "@/components/admin/layout/AdminNavigationMenu";

describe("AdminNavigationMenu", () => {
it("컴포넌트가 초기 렌더링될 때, 기본 요소가 표시되어야 한다", () => {
render(<AdminNavigationMenu handleTap={vi.fn()} />);
expect(screen.getByText("RSS 목록")).toBeInTheDocument();
expect(screen.getByText("회원 관리")).toBeInTheDocument();
});

it("RSS 목록 버튼 클릭 시 handleTap이 RSS로 호출되어야 한다", () => {
const handleTap = vi.fn();
render(<AdminNavigationMenu handleTap={handleTap} />);

const rssButton = screen.getByText("RSS 목록");
fireEvent.click(rssButton);

expect(handleTap).toHaveBeenCalledWith(TAB_TYPES.RSS);
});

it("회원 관리 버튼 클릭 시 handleTap이 MEMBER로 호출되어야 한다", () => {
const handleTap = vi.fn();
render(<AdminNavigationMenu handleTap={handleTap} />);

const memberButton = screen.getByText("회원 관리");
fireEvent.click(memberButton);

expect(handleTap).toHaveBeenCalledWith(TAB_TYPES.MEMBER);
});
});
Loading
Loading