Skip to content
Merged
140 changes: 140 additions & 0 deletions components/layout/Header/index.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
import { render, screen } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import Header from ".";

const push = jest.fn();
const logout = jest.fn();
const openLogin = jest.fn();

jest.mock("next/image", () => {
return function MockImage(props: { alt: string }) {
return <img alt={props.alt} />;
};
});

jest.mock("next/navigation", () => ({
usePathname: jest.fn(),
useRouter: () => ({ push }),
}));

jest.mock("@/hooks/useUser", () => ({
useUser: jest.fn(),
}));

jest.mock("@/features/header/queries", () => ({
useGetFavoritesCount: jest.fn(),
}));

jest.mock("@/features/header/components/Notification", () => {
return function MockNotification() {
return <button type="button">알림 버튼</button>;
};
});

jest.mock("@/components/ui/Dropdowns/ActionDropdown", () => {
return function MockActionDropdown({
items,
}: {
items: { label: string; onClick: () => void; disabled?: boolean }[];
}) {
return (
<div>
<button type="button">프로필 메뉴 열기</button>
{items.map((item) => (
<button key={item.label} type="button" onClick={item.onClick} disabled={item.disabled}>
{item.label}
</button>
))}
</div>
);
};
});

jest.mock("@/store/modal.store", () => ({
useModalStore: () => ({ openLogin }),
}));

jest.mock("@/features/auth/mutations", () => ({
useLogout: () => ({ mutate: logout, isPending: false }),
}));

jest.mock("@/features/auth/components/LoginModal", () => ({
LoginModal: () => null,
}));

jest.mock("@/features/auth/components/SignUpModal", () => ({
SignUpModal: () => null,
}));

const { usePathname } = jest.requireMock("next/navigation");
const { useUser } = jest.requireMock("@/hooks/useUser");
const { useGetFavoritesCount } = jest.requireMock("@/features/header/queries");

function renderHeader() {
const user = userEvent.setup();

render(<Header />);

return {
user,
};
}

describe("Header", () => {
beforeEach(() => {
jest.clearAllMocks();
usePathname.mockReturnValue("/meetup/list");
useGetFavoritesCount.mockReturnValue({ data: undefined });
});

test("로그아웃 상태에서는 로그인 버튼이 보이고 프로필 메뉴와 알림 버튼은 보이지 않는다", async () => {
useUser.mockReturnValue({ user: null, isLoggedIn: false, isPending: false });

const { user } = renderHeader();

const loginButton = screen.getByRole("button", { name: "로그인" });
const profileMenuButton = screen.queryByRole("button", { name: "프로필 메뉴 열기" });
const notificationButton = screen.queryByRole("button", { name: "알림 버튼" });

expect(loginButton).toBeInTheDocument();
expect(profileMenuButton).not.toBeInTheDocument();
expect(notificationButton).not.toBeInTheDocument();

await user.click(loginButton);
expect(openLogin).toHaveBeenCalledTimes(1);
});

test("로그인 상태에서는 알림 버튼, 프로필 메뉴, 찜한 모임 개수가 보인다", async () => {
useUser.mockReturnValue({
user: {
id: 1,
name: "홍길동",
email: "test@example.com",
image: "https://example.com/me.jpg",
},
isLoggedIn: true,
isPending: false,
});
useGetFavoritesCount.mockReturnValue({ data: { count: 3 } });

const { user } = renderHeader();

const notificationButton = screen.getByRole("button", { name: "알림 버튼" });
const profileMenuButton = screen.getByRole("button", { name: "프로필 메뉴 열기" });
const favoritesCount = screen.getByText("3");
const loginButton = screen.queryByRole("button", { name: "로그인" });

expect(notificationButton).toBeInTheDocument();
expect(profileMenuButton).toBeInTheDocument();
expect(favoritesCount).toBeInTheDocument();
expect(loginButton).not.toBeInTheDocument();

const mypageButton = screen.getByRole("button", { name: "마이페이지" });
await user.click(mypageButton);
expect(push).toHaveBeenCalledWith("/mypage");

const logoutButton = screen.getByRole("button", { name: "로그아웃" });
await user.click(logoutButton);
expect(logout).toHaveBeenCalledTimes(1);
});
});
178 changes: 178 additions & 0 deletions features/mypage/AvailableReviewList/index.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,178 @@
import { render, screen } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import AvailableReviewListWrapper from ".";
import { mockMeMeetingApiRes } from "../mockData";

const handleWishToggle = jest.fn();
const postMeetingReview = jest.fn();
const reviewFormValues = { score: 5, comment: "좋은 모임이었어요" };

jest.mock("@/components/ui/Empty", () => {
return function MockEmpty({ children }: { children: React.ReactNode }) {
return <div>{children}</div>;
};
});

jest.mock("@/components/ui/Loading", () => {
return function MockLoading() {
return <div>목록 로딩 중</div>;
};
});

jest.mock("../components/DetailCard", () => {
return function MockDetailCard({
item,
actions = [],
wishAction,
}: {
item: { id: number; name: string; isFavorited: boolean };
actions?: { label: string; handleCardButtonClick: () => void }[];
wishAction?: { handleWishClick: () => void };
}) {
return (
<li>
<span>{item.name}</span>
{actions.map((action) => (
<button key={action.label} type="button" onClick={action.handleCardButtonClick}>
{action.label}
</button>
))}
<button type="button" onClick={wishAction?.handleWishClick}>
찜 토글
</button>
</li>
);
};
});

jest.mock("@/features/shared/components/ReviewModal", () => ({
__esModule: true,
default: function MockReviewModal({
isOpen,
handleFormSubmit,
}: {
isOpen: boolean;
handleFormSubmit: (values: typeof reviewFormValues) => void;
}) {
return isOpen ? (
<div role="dialog">
<button type="button" onClick={() => handleFormSubmit(reviewFormValues)}>
리뷰 제출
</button>
</div>
) : null;
},
}));

jest.mock("@/hooks/useMeetingFavorite", () => ({
__esModule: true,
default: () => ({ handleWishToggle }),
}));

jest.mock("../queries", () => ({
useMyMeetupInfinite: jest.fn(),
}));

jest.mock("../mutations", () => ({
usePostMeetingsReviews: () => ({ mutate: postMeetingReview, isPending: false }),
}));

const { useMyMeetupInfinite } = jest.requireMock("../queries");

function setAvailableReviews(data = [mockMeMeetingApiRes], isFetchingNextPage = false) {
useMyMeetupInfinite.mockReturnValue({
data: { pages: [{ data }] },
hasNextPage: false,
isFetchingNextPage,
});
}

function renderAvailableReviewList() {
const user = userEvent.setup();

render(<AvailableReviewListWrapper />);

return {
user,
};
}

describe("AvailableReviewList", () => {
const consoleError = jest.spyOn(console, "error").mockImplementation(() => {});

beforeEach(() => {
jest.clearAllMocks();
setAvailableReviews();
});

afterAll(() => {
consoleError.mockRestore();
});

test("작성 가능한 리뷰 모임을 렌더링한다", () => {
renderAvailableReviewList();

const name = screen.getByText("코딩 스터디");
const reviewButton = screen.getByRole("button", { name: "리뷰 작성하기" });

expect(name).toBeInTheDocument();
expect(reviewButton).toBeInTheDocument();
});

test("데이터가 없으면 Empty UI를 렌더링한다", () => {
setAvailableReviews([]);

renderAvailableReviewList();

const empty = screen.getByText("작성 가능한 리뷰가 없어요");
expect(empty).toBeInTheDocument();
});

test("다음 페이지를 불러오는 중이면 Loading UI를 렌더링한다", () => {
setAvailableReviews([mockMeMeetingApiRes], true);

renderAvailableReviewList();

const loading = screen.getByText("목록 로딩 중");
expect(loading).toBeInTheDocument();
});

test("찜 버튼을 클릭하면 찜 토글 핸들러를 호출한다", async () => {
setAvailableReviews([{ ...mockMeMeetingApiRes, isFavorited: false }]);

const { user } = renderAvailableReviewList();
const wishButton = screen.getByRole("button", { name: "찜 토글" });
await user.click(wishButton);

expect(handleWishToggle).toHaveBeenCalledWith(mockMeMeetingApiRes.id, false);
});
Comment thread
sohyun0 marked this conversation as resolved.

test("리뷰 작성 모달에서 제출하면 리뷰 작성 mutation을 호출한다", async () => {
const { user } = renderAvailableReviewList();

const reviewButton = screen.getByRole("button", { name: "리뷰 작성하기" });
await user.click(reviewButton);

const submitButton = screen.getByRole("button", { name: "리뷰 제출" });
await user.click(submitButton);

expect(postMeetingReview).toHaveBeenCalledWith(
{ meetingId: 1000, reviewFormValues },
expect.objectContaining({ onSuccess: expect.any(Function), onError: expect.any(Function) }),
);
});

test("데이터 조회 중 에러가 발생하면 ErrorBoundary fallback UI를 렌더링한다", () => {
useMyMeetupInfinite.mockImplementation(() => {
throw new Error("query error");
});

renderAvailableReviewList();

const errorMessage = screen.getByText("작성 가능 한 리뷰를 불러오지 못했습니다.");
const retryButton = screen.getByRole("button", { name: "다시 시도" });

expect(errorMessage).toBeInTheDocument();
expect(retryButton).toBeInTheDocument();
});
});
Loading
Loading