From 49840a7696aad05f468ecc68136dfb9c97241ecf Mon Sep 17 00:00:00 2001 From: celine Date: Fri, 8 May 2026 15:57:27 +0900 Subject: [PATCH 1/8] =?UTF-8?q?test(GNB):=20=EC=83=81=EB=8B=A8=20GNB=20?= =?UTF-8?q?=ED=85=8C=EC=8A=A4=ED=8A=B8=EC=BD=94=EB=93=9C=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Resolves:#360 --- components/layout/Header/index.test.tsx | 140 ++++++++++++++++++++++++ 1 file changed, 140 insertions(+) create mode 100644 components/layout/Header/index.test.tsx diff --git a/components/layout/Header/index.test.tsx b/components/layout/Header/index.test.tsx new file mode 100644 index 00000000..40516938 --- /dev/null +++ b/components/layout/Header/index.test.tsx @@ -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 {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 ; + }; +}); + +jest.mock("@/components/ui/Dropdowns/ActionDropdown", () => { + return function MockActionDropdown({ + items, + }: { + items: { label: string; onClick: () => void; disabled?: boolean }[]; + }) { + return ( +
+ + {items.map((item) => ( + + ))} +
+ ); + }; +}); + +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(
); + + 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); + }); +}); From c03a56546b435de8735a4e1913614ff18b9874e3 Mon Sep 17 00:00:00 2001 From: celine Date: Fri, 8 May 2026 15:58:22 +0900 Subject: [PATCH 2/8] =?UTF-8?q?test(mypage/profile):=20profile=20=EC=98=81?= =?UTF-8?q?=EC=97=AD=20=EB=A0=8C=EB=8D=94=EB=A7=81=20=ED=85=8C=EC=8A=A4?= =?UTF-8?q?=ED=8A=B8=20=EC=BD=94=EB=93=9C=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Resolves:#360 --- features/mypage/MyProfile/index.test.tsx | 85 ++++++++++++++++++++++++ 1 file changed, 85 insertions(+) create mode 100644 features/mypage/MyProfile/index.test.tsx diff --git a/features/mypage/MyProfile/index.test.tsx b/features/mypage/MyProfile/index.test.tsx new file mode 100644 index 00000000..3a27862c --- /dev/null +++ b/features/mypage/MyProfile/index.test.tsx @@ -0,0 +1,85 @@ +import { render, screen } from "@testing-library/react"; +import userEvent from "@testing-library/user-event"; +import MyProfileContainer from "."; +import { mockUserProfile } from "../mockData"; + +jest.mock("react-loading-skeleton", () => { + return function MockSkeleton() { + return
; + }; +}); + +jest.mock("@/components/ui/Avatar", () => { + return function MockAvatar({ src }: { src: string | null }) { + return 프로필 이미지; + }; +}); + +jest.mock("../components/ProfileModal", () => { + return function MockProfileModal({ isOpen, user }: { isOpen: boolean; user: { name: string } }) { + return isOpen ?
{user.name} 프로필 수정 모달
: null; + }; +}); + +jest.mock("@/hooks/useUser", () => ({ + useUser: jest.fn(), +})); + +const { useUser } = jest.requireMock("@/hooks/useUser"); + +function renderMyProfile() { + const user = userEvent.setup(); + const view = render(); + + return { + user, + ...view, + }; +} + +describe("MyProfile", () => { + afterEach(() => { + jest.clearAllMocks(); + }); + + test("유저 정보가 로딩 중이면 프로필 스켈레톤을 렌더링한다", () => { + useUser.mockReturnValue({ user: null, isPending: true }); + + renderMyProfile(); + + const skeletons = screen.getAllByTestId("profile-skeleton"); + expect(skeletons).toHaveLength(3); + }); + + test("유저 정보가 있으면 이름과 이메일을 렌더링한다", () => { + useUser.mockReturnValue({ user: mockUserProfile, isPending: false }); + + renderMyProfile(); + + const name = screen.getByText("홍길동"); + const email = screen.getByText("test@example.com"); + + expect(name).toBeInTheDocument(); + expect(email).toBeInTheDocument(); + }); + + test("프로필 수정 버튼을 클릭하면 프로필 모달을 연다", async () => { + useUser.mockReturnValue({ user: mockUserProfile, isPending: false }); + + const { user } = renderMyProfile(); + + const editButton = screen.getByRole("button", { name: "프로필 수정" }); + await user.click(editButton); + + const modal = screen.getByRole("dialog"); + expect(modal).toHaveTextContent("홍길동 프로필 수정 모달"); + }); + + test("유저 정보가 없으면 아무것도 렌더링하지 않는다", () => { + useUser.mockReturnValue({ user: null, isPending: false }); + + const { container } = renderMyProfile(); + + expect(container).toBeEmptyDOMElement(); + }); +}); From 0272751a8ac697d7aee8fe10db246065b3af3c6b Mon Sep 17 00:00:00 2001 From: celine Date: Fri, 8 May 2026 16:00:19 +0900 Subject: [PATCH 3/8] =?UTF-8?q?test(mypage/TabWrapper):=20=EB=A7=88?= =?UTF-8?q?=EC=9D=B4=ED=8E=98=EC=9D=B4=EC=A7=80=20=ED=83=AD=20=EC=98=81?= =?UTF-8?q?=EC=97=AD=20=EB=A0=8C=EB=8D=94=EB=A7=81=20=ED=85=8C=EC=8A=A4?= =?UTF-8?q?=ED=8A=B8=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Resolves: #360 --- .../mypage/MyTab/TabWrapper/index.test.tsx | 99 +++++++++++++++++++ 1 file changed, 99 insertions(+) create mode 100644 features/mypage/MyTab/TabWrapper/index.test.tsx diff --git a/features/mypage/MyTab/TabWrapper/index.test.tsx b/features/mypage/MyTab/TabWrapper/index.test.tsx new file mode 100644 index 00000000..488d5843 --- /dev/null +++ b/features/mypage/MyTab/TabWrapper/index.test.tsx @@ -0,0 +1,99 @@ +import { render, screen, waitFor } from "@testing-library/react"; +import userEvent from "@testing-library/user-event"; +import TabWrapper from "."; + +const querySet = jest.fn(); +let tabQuery: string | null = null; + +jest.mock("@/hooks/useQueryParams", () => ({ + useQueryParams: () => ({ + get: () => tabQuery, + set: querySet, + }), +})); + +jest.mock("../../JoinedMeetingList", () => { + return function MockJoinedMeetingList() { + return
참여 모임 콘텐츠
; + }; +}); + +jest.mock("../../CreatedMeetingList", () => { + return function MockCreatedMeetingList() { + return
개설 모임 콘텐츠
; + }; +}); + +jest.mock("../../AvailableReviewList", () => { + return function MockAvailableReviewList() { + return
리뷰 작성 콘텐츠
; + }; +}); + +jest.mock("../../WrittenReviewList", () => { + return function MockWrittenReviewList() { + return
리뷰 목록 콘텐츠
; + }; +}); + +function renderTabWrapper() { + const user = userEvent.setup(); + + render(); + + return { + user, + }; +} + +describe("MyTab", () => { + beforeEach(() => { + tabQuery = null; + querySet.mockReset(); + querySet.mockImplementation(({ tab }: { tab: string | null }) => { + tabQuery = tab; + }); + }); + + test("기본 탭으로 참여 모임 콘텐츠를 렌더링한다", () => { + renderTabWrapper(); + + const joinedTab = screen.getByRole("tab", { name: "참여 모임" }); + const joinedContent = screen.getByText("참여 모임 콘텐츠"); + + expect(joinedTab).toBeInTheDocument(); + expect(joinedContent).toBeInTheDocument(); + }); + + test("탭을 클릭하면 선택한 콘텐츠로 변경하고 query 값을 갱신한다", async () => { + const { user } = renderTabWrapper(); + + const createdTab = screen.getByRole("tab", { name: "개설 모임" }); + await user.click(createdTab); + + await waitFor(() => { + const createdContent = screen.getByText("개설 모임 콘텐츠"); + expect(createdContent).toBeInTheDocument(); + }); + expect(querySet).toHaveBeenCalledWith({ tab: "CreatedMeetingList" }); + }); + + test("URL query의 tab 값이 있으면 해당 탭 콘텐츠를 기본으로 렌더링한다", () => { + tabQuery = "WrittenReviewList"; + + renderTabWrapper(); + + const writtenReviewContent = screen.getByText("리뷰 목록 콘텐츠"); + expect(writtenReviewContent).toBeInTheDocument(); + }); + + test("잘못된 tab query 값은 제거한다", async () => { + tabQuery = "UnknownTab"; + + renderTabWrapper(); + + await waitFor(() => { + expect(querySet).toHaveBeenCalledWith({ tab: null }); + }); + }); +}); From 95a59664cfacbf5e6d2bddbf71d9ee008ab8e724 Mon Sep 17 00:00:00 2001 From: celine Date: Fri, 8 May 2026 16:02:29 +0900 Subject: [PATCH 4/8] =?UTF-8?q?test(mypage/Tabitem):=20=EB=A7=88=EC=9D=B4?= =?UTF-8?q?=ED=8E=98=EC=9D=B4=EC=A7=80=20=EC=B0=B8=EC=97=AC=EB=AA=A8?= =?UTF-8?q?=EC=9E=84=20=ED=83=AD=20=EB=A0=8C=EB=8D=94=EB=A7=81=20=ED=85=8C?= =?UTF-8?q?=EC=8A=A4=ED=8A=B8=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Resolves: #360 --- .../mypage/JoinedMeetingList/index.test.tsx | 189 ++++++++++++++++++ 1 file changed, 189 insertions(+) create mode 100644 features/mypage/JoinedMeetingList/index.test.tsx diff --git a/features/mypage/JoinedMeetingList/index.test.tsx b/features/mypage/JoinedMeetingList/index.test.tsx new file mode 100644 index 00000000..7d76dfaa --- /dev/null +++ b/features/mypage/JoinedMeetingList/index.test.tsx @@ -0,0 +1,189 @@ +import { render, screen } from "@testing-library/react"; +import userEvent from "@testing-library/user-event"; +import JoinedMeetingListWrapper from "."; +import { mockMeMeetingApiRes } from "../mockData"; + +const handleWishToggle = jest.fn(); +const deleteMeetingsJoin = jest.fn(); + +jest.mock("@/components/ui/Empty", () => { + return function MockEmpty({ children }: { children: React.ReactNode }) { + return
{children}
; + }; +}); + +jest.mock("@/components/ui/Loading", () => { + return function MockLoading() { + return
목록 로딩 중
; + }; +}); + +jest.mock("../components/DetailCard", () => { + return function MockDetailCard({ + item, + actions = [], + wishAction, + }: { + item: { name: string }; + actions?: { label: string; handleCardButtonClick: () => void }[]; + wishAction?: { handleWishClick: () => void }; + }) { + return ( +
  • + {item.name} + {actions.map((action) => ( + + ))} + +
  • + ); + }; +}); + +jest.mock("@/components/ui/Modals/AlertModal", () => { + return function MockAlert({ + isOpen, + children, + handleConfirmButton, + }: { + isOpen: boolean; + children: React.ReactNode; + handleConfirmButton: () => void; + }) { + return isOpen ? ( +
    + {children} + +
    + ) : null; + }; +}); + +jest.mock("@/features/shared/components/ReviewModal", () => ({ + __esModule: true, + default: function MockReviewModal({ isOpen }: { isOpen: boolean }) { + return isOpen ?
    리뷰 모달
    : null; + }, +})); + +jest.mock("@/hooks/useMeetingFavorite", () => ({ + __esModule: true, + default: () => ({ handleWishToggle }), +})); + +jest.mock("../queries", () => ({ + useMyJoinedInfinite: jest.fn(), +})); + +jest.mock("../mutations", () => ({ + usePatchMeetingsStatus: () => ({ mutate: jest.fn(), isPending: false }), + useDeleteMeetings: () => ({ mutate: jest.fn(), isPending: false }), + useDeleteMeetingsJoin: () => ({ mutate: deleteMeetingsJoin, isPending: false }), + usePostMeetingsReviews: () => ({ mutate: jest.fn(), isPending: false }), +})); + +const { useMyJoinedInfinite } = jest.requireMock("../queries"); + +function setJoinedMeetings(data = [mockMeMeetingApiRes], isFetchingNextPage = false) { + useMyJoinedInfinite.mockReturnValue({ + data: { pages: [{ data }] }, + hasNextPage: false, + isFetchingNextPage, + }); +} + +function renderJoinedMeetingList() { + const user = userEvent.setup(); + + render(); + + return { + user, + }; +} + +describe("JoinedMeetingList", () => { + const consoleError = jest.spyOn(console, "error").mockImplementation(() => {}); + + beforeEach(() => { + jest.clearAllMocks(); + setJoinedMeetings([{ ...mockMeMeetingApiRes, role: "participant", isCompleted: false }]); + }); + + afterAll(() => { + consoleError.mockRestore(); + }); + + test("내가 참여한 모임을 렌더링한다", () => { + renderJoinedMeetingList(); + + const name = screen.getByText("코딩 스터디"); + const cancelButton = screen.getByRole("button", { name: "참여 취소하기" }); + + expect(name).toBeInTheDocument(); + expect(cancelButton).toBeInTheDocument(); + }); + + test("데이터가 없으면 Empty UI를 렌더링한다", () => { + setJoinedMeetings([]); + + renderJoinedMeetingList(); + + const empty = screen.getByText("아직 참여한 모임이 없어요"); + expect(empty).toBeInTheDocument(); + }); + + test("다음 페이지를 불러오는 중이면 Loading UI를 렌더링한다", () => { + setJoinedMeetings([{ ...mockMeMeetingApiRes, role: "participant", isCompleted: false }], true); + + renderJoinedMeetingList(); + + const loading = screen.getByText("목록 로딩 중"); + expect(loading).toBeInTheDocument(); + }); + + test("참여 취소를 확인하면 참여 취소 mutation을 호출한다", async () => { + const { user } = renderJoinedMeetingList(); + + const cancelButton = screen.getByRole("button", { name: "참여 취소하기" }); + await user.click(cancelButton); + + const alertMessage = screen.getByText("모임 예약을 취소하시겠습니까?"); + const alertConfirmButton = screen.getByRole("button", { name: "확인" }); + + expect(alertMessage).toBeInTheDocument(); + + await user.click(alertConfirmButton); + + expect(deleteMeetingsJoin).toHaveBeenCalledWith( + { meetingId: 1000 }, + expect.objectContaining({ onSuccess: expect.any(Function), onError: expect.any(Function) }), + ); + }); + + test("찜 버튼을 클릭하면 찜 토글 핸들러를 호출한다", async () => { + const { user } = renderJoinedMeetingList(); + + const wishButton = screen.getByRole("button", { name: "찜 토글" }); + await user.click(wishButton); + + expect(handleWishToggle).toHaveBeenCalledWith(1000, false); + }); + + test("데이터 조회 중 에러가 발생하면 ErrorBoundary fallback UI를 렌더링한다", () => { + useMyJoinedInfinite.mockImplementation(() => { + throw new Error("query error"); + }); + + renderJoinedMeetingList(); + + const errorMessage = screen.getByText("나의 모임을 불러오지 못했습니다."); + expect(errorMessage).toBeInTheDocument(); + }); +}); From d9f38abb866b76043e37684c532766de6de886be Mon Sep 17 00:00:00 2001 From: celine Date: Fri, 8 May 2026 16:03:07 +0900 Subject: [PATCH 5/8] =?UTF-8?q?test(mypage/Tabitem):=20=EB=A7=88=EC=9D=B4?= =?UTF-8?q?=ED=8E=98=EC=9D=B4=EC=A7=80=20=EA=B0=9C=EC=84=A4=EB=AA=A8?= =?UTF-8?q?=EC=9E=84=20=ED=83=AD=20=EC=98=81=EC=97=AD=20=EB=A0=8C=EB=8D=94?= =?UTF-8?q?=EB=A7=81=20=ED=85=8C=EC=8A=A4=ED=8A=B8=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Resolves: #360 --- .../mypage/CreatedMeetingList/index.test.tsx | 209 ++++++++++++++++++ 1 file changed, 209 insertions(+) create mode 100644 features/mypage/CreatedMeetingList/index.test.tsx diff --git a/features/mypage/CreatedMeetingList/index.test.tsx b/features/mypage/CreatedMeetingList/index.test.tsx new file mode 100644 index 00000000..d93e8cbd --- /dev/null +++ b/features/mypage/CreatedMeetingList/index.test.tsx @@ -0,0 +1,209 @@ +import { render, screen } from "@testing-library/react"; +import userEvent from "@testing-library/user-event"; +import CreatedMeetingListWrapper from "."; +import { mockMeMeetingApiRes } from "../mockData"; + +const patchMeetingsStatus = jest.fn(); +const postMeetingReview = jest.fn(); +const reviewFormValues = { score: 5, comment: "좋은 모임이었어요" }; + +jest.mock("@/components/ui/Empty", () => { + return function MockEmpty({ children }: { children: React.ReactNode }) { + return
    {children}
    ; + }; +}); + +jest.mock("@/components/ui/Loading", () => { + return function MockLoading() { + return
    목록 로딩 중
    ; + }; +}); + +jest.mock("../components/DetailCard", () => { + return function MockDetailCard({ + item, + actions = [], + wishAction, + }: { + item: { name: string }; + actions?: { label: string; handleCardButtonClick: () => void }[]; + wishAction?: { handleWishClick: () => void }; + }) { + return ( +
  • + {item.name} + {actions.map((action) => ( + + ))} + +
  • + ); + }; +}); + +jest.mock("@/components/ui/Modals/AlertModal", () => { + return function MockAlert({ + isOpen, + children, + handleConfirmButton, + }: { + isOpen: boolean; + children: React.ReactNode; + handleConfirmButton: () => void; + }) { + return isOpen ? ( +
    + {children} + +
    + ) : null; + }; +}); + +jest.mock("@/features/shared/components/ReviewModal", () => ({ + __esModule: true, + default: function MockReviewModal({ + isOpen, + handleFormSubmit, + }: { + isOpen: boolean; + handleFormSubmit: (values: typeof reviewFormValues) => void; + }) { + return isOpen ? ( +
    + +
    + ) : null; + }, +})); + +jest.mock("@/hooks/useMeetingFavorite", () => ({ + __esModule: true, + default: () => ({ handleWishToggle: jest.fn() }), +})); + +jest.mock("../queries", () => ({ + useMyCreatedInfinite: jest.fn(), +})); + +jest.mock("../mutations", () => ({ + usePatchMeetingsStatus: () => ({ mutate: patchMeetingsStatus, isPending: false }), + useDeleteMeetings: () => ({ mutate: jest.fn(), isPending: false }), + usePostMeetingsReviews: () => ({ mutate: postMeetingReview, isPending: false }), +})); + +const { useMyCreatedInfinite } = jest.requireMock("../queries"); + +function setCreatedMeetings(data = [mockMeMeetingApiRes], isFetchingNextPage = false) { + useMyCreatedInfinite.mockReturnValue({ + data: { pages: [{ data }] }, + hasNextPage: false, + isFetchingNextPage, + }); +} + +function renderCreatedMeetingList() { + const user = userEvent.setup(); + + render(); + + return { + user, + }; +} + +describe("CreatedMeetingList", () => { + const consoleError = jest.spyOn(console, "error").mockImplementation(() => {}); + + beforeEach(() => { + jest.clearAllMocks(); + setCreatedMeetings([{ ...mockMeMeetingApiRes, isCompleted: false, isReviewed: false }]); + }); + + afterAll(() => { + consoleError.mockRestore(); + }); + + test("내가 만든 모임을 렌더링한다", () => { + renderCreatedMeetingList(); + + const name = screen.getByText("코딩 스터디"); + const confirmButton = screen.getByRole("button", { name: "모임 확정하기" }); + + expect(name).toBeInTheDocument(); + expect(confirmButton).toBeInTheDocument(); + }); + + test("데이터가 없으면 Empty UI를 렌더링한다", () => { + setCreatedMeetings([]); + + renderCreatedMeetingList(); + + const empty = screen.getByText("아직 내가 만든 모임이 없어요"); + expect(empty).toBeInTheDocument(); + }); + + test("다음 페이지를 불러오는 중이면 Loading UI를 렌더링한다", () => { + setCreatedMeetings([{ ...mockMeMeetingApiRes, isCompleted: false, isReviewed: false }], true); + + renderCreatedMeetingList(); + + const loading = screen.getByText("목록 로딩 중"); + expect(loading).toBeInTheDocument(); + }); + + test("모임 확정을 확인하면 상태 변경 mutation을 호출한다", async () => { + const { user } = renderCreatedMeetingList(); + + const confirmButton = screen.getByRole("button", { name: "모임 확정하기" }); + await user.click(confirmButton); + + const alertMessage = screen.getByText("모임을 확정하시겠습니까?"); + const alertConfirmButton = screen.getByRole("button", { name: "확인" }); + + expect(alertMessage).toBeInTheDocument(); + + await user.click(alertConfirmButton); + + expect(patchMeetingsStatus).toHaveBeenCalledWith( + { meetingId: 1000, status: "CONFIRMED" }, + expect.objectContaining({ onSuccess: expect.any(Function), onError: expect.any(Function) }), + ); + }); + + test("완료된 미작성 모임에서 리뷰를 제출하면 리뷰 작성 mutation을 호출한다", async () => { + setCreatedMeetings([{ ...mockMeMeetingApiRes, isCompleted: true, isReviewed: false }]); + + const { user } = renderCreatedMeetingList(); + + 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를 렌더링한다", () => { + useMyCreatedInfinite.mockImplementation(() => { + throw new Error("query error"); + }); + + renderCreatedMeetingList(); + + const errorMessage = screen.getByText("내가 만든 모임을 불러오지 못했습니다."); + expect(errorMessage).toBeInTheDocument(); + }); +}); From b4a60d37cc15d4f7fcf1c13ca594cab830747d81 Mon Sep 17 00:00:00 2001 From: celine Date: Fri, 8 May 2026 16:03:50 +0900 Subject: [PATCH 6/8] =?UTF-8?q?test(mypage/Tabitem):=20=EB=A7=88=EC=9D=B4?= =?UTF-8?q?=ED=8E=98=EC=9D=B4=EC=A7=80=20=EB=A6=AC=EB=B7=B0=EC=9E=91?= =?UTF-8?q?=EC=84=B1=20=ED=83=AD=20=EB=A0=8C=EB=8D=94=EB=A7=81=20=ED=85=8C?= =?UTF-8?q?=EC=8A=A4=ED=8A=B8=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Resolves:#360 --- .../mypage/AvailableReviewList/index.test.tsx | 177 ++++++++++++++++++ 1 file changed, 177 insertions(+) create mode 100644 features/mypage/AvailableReviewList/index.test.tsx diff --git a/features/mypage/AvailableReviewList/index.test.tsx b/features/mypage/AvailableReviewList/index.test.tsx new file mode 100644 index 00000000..673ccb07 --- /dev/null +++ b/features/mypage/AvailableReviewList/index.test.tsx @@ -0,0 +1,177 @@ +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
    {children}
    ; + }; +}); + +jest.mock("@/components/ui/Loading", () => { + return function MockLoading() { + return
    목록 로딩 중
    ; + }; +}); + +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 ( +
  • + {item.name} + {actions.map((action) => ( + + ))} + +
  • + ); + }; +}); + +jest.mock("@/features/shared/components/ReviewModal", () => ({ + __esModule: true, + default: function MockReviewModal({ + isOpen, + handleFormSubmit, + }: { + isOpen: boolean; + handleFormSubmit: (values: typeof reviewFormValues) => void; + }) { + return isOpen ? ( +
    + +
    + ) : 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(); + + 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 () => { + const { user } = renderAvailableReviewList(); + + const wishButton = screen.getByRole("button", { name: "찜 토글" }); + await user.click(wishButton); + + expect(handleWishToggle).toHaveBeenCalledWith(1000, false); + }); + + 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(); + }); +}); From 054c549112ab8157be939bede1145cc086a07425 Mon Sep 17 00:00:00 2001 From: celine Date: Fri, 8 May 2026 16:04:33 +0900 Subject: [PATCH 7/8] =?UTF-8?q?test(mypage/Tabitem):=20=EB=A7=88=EC=9D=B4?= =?UTF-8?q?=ED=8E=98=EC=9D=B4=EC=A7=80=20=EB=A6=AC=EB=B7=B0=EB=AA=A9?= =?UTF-8?q?=EB=A1=9D=20=ED=83=AD=20=EC=98=81=EC=97=AD=20=EB=A0=8C=EB=8D=94?= =?UTF-8?q?=EB=A7=81=20=ED=85=8C=EC=8A=A4=ED=8A=B8=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Resolves:#360 --- .../mypage/WrittenReviewList/index.test.tsx | 234 ++++++++++++++++++ 1 file changed, 234 insertions(+) create mode 100644 features/mypage/WrittenReviewList/index.test.tsx diff --git a/features/mypage/WrittenReviewList/index.test.tsx b/features/mypage/WrittenReviewList/index.test.tsx new file mode 100644 index 00000000..28e68cd1 --- /dev/null +++ b/features/mypage/WrittenReviewList/index.test.tsx @@ -0,0 +1,234 @@ +import { render, screen } from "@testing-library/react"; +import userEvent from "@testing-library/user-event"; +import WrittenReviewListWrapper from "."; +import { mockUserProfile } from "../mockData"; +import { ReviewCardItem } from "../types"; + +const patchReviews = jest.fn(); +const deleteReviews = jest.fn(); +const reviewFormValues = { score: 4, comment: "수정한 리뷰입니다" }; + +const mockReviewItem: ReviewCardItem = { + id: 123, + score: 5, + comment: "함께 공부해서 좋았어요", + meetingId: 1000, + meetingType: "자기계발", + meetingName: "코딩 스터디", + meetingImage: "https://example.com/image.jpg", + meetingDateTime: "2026-04-02T16:05:00.000Z", + createdAt: "2026-04-08T06:22:04.892Z", +}; + +jest.mock("@/components/ui/Empty", () => { + return function MockEmpty({ children }: { children: React.ReactNode }) { + return
    {children}
    ; + }; +}); + +jest.mock("@/components/ui/Loading", () => { + return function MockLoading() { + return
    목록 로딩 중
    ; + }; +}); + +jest.mock("../components/ReviewCard", () => { + return function MockReviewCard({ + item, + handleEdit, + handleDelete, + }: { + item: { meetingName: string; comment: string }; + handleEdit: () => void; + handleDelete: () => void; + }) { + return ( +
  • + {item.meetingName} + {item.comment} + + +
  • + ); + }; +}); + +jest.mock("@/components/ui/Modals/AlertModal", () => { + return function MockAlert({ + isOpen, + children, + handleConfirmButton, + }: { + isOpen: boolean; + children: React.ReactNode; + handleConfirmButton: () => void; + }) { + return isOpen ? ( +
    + {children} + +
    + ) : null; + }; +}); + +jest.mock("@/features/shared/components/ReviewModal", () => ({ + __esModule: true, + default: function MockReviewModal({ + isOpen, + initialValue, + handleFormSubmit, + }: { + isOpen: boolean; + initialValue?: { score?: number; comment?: string }; + handleFormSubmit: (values: typeof reviewFormValues) => void; + }) { + return isOpen ? ( +
    + 초기 리뷰: {initialValue?.comment} + +
    + ) : null; + }, +})); + +jest.mock("@/hooks/useUser", () => ({ + useUser: jest.fn(), +})); + +jest.mock("../queries", () => ({ + useMyReviewInfinite: jest.fn(), +})); + +jest.mock("../mutations", () => ({ + usePatchReviews: () => ({ mutate: patchReviews, isPending: false }), + useDeleteReviews: () => ({ mutate: deleteReviews, isPending: false }), +})); + +const { useUser } = jest.requireMock("@/hooks/useUser"); +const { useMyReviewInfinite } = jest.requireMock("../queries"); + +function setWrittenReviews(data = [mockReviewItem], isFetchingNextPage = false) { + useMyReviewInfinite.mockReturnValue({ + data: { pages: [{ data }] }, + hasNextPage: false, + isFetchingNextPage, + }); +} + +function renderWrittenReviewList() { + const user = userEvent.setup(); + const view = render(); + + return { + user, + ...view, + }; +} + +describe("WrittenReviewList", () => { + const consoleError = jest.spyOn(console, "error").mockImplementation(() => {}); + + beforeEach(() => { + jest.clearAllMocks(); + useUser.mockReturnValue({ user: mockUserProfile }); + setWrittenReviews(); + }); + + afterAll(() => { + consoleError.mockRestore(); + }); + + test("작성한 리뷰를 렌더링한다", () => { + renderWrittenReviewList(); + + const meetingName = screen.getByText("코딩 스터디"); + const comment = screen.getByText("함께 공부해서 좋았어요"); + + expect(meetingName).toBeInTheDocument(); + expect(comment).toBeInTheDocument(); + }); + + test("유저 정보가 없으면 아무것도 렌더링하지 않는다", () => { + useUser.mockReturnValue({ user: null }); + + const { container } = renderWrittenReviewList(); + + expect(container).toBeEmptyDOMElement(); + }); + + test("데이터가 없으면 Empty UI를 렌더링한다", () => { + setWrittenReviews([]); + + renderWrittenReviewList(); + + const empty = screen.getByText("아직 작성한 리뷰가 없어요"); + expect(empty).toBeInTheDocument(); + }); + + test("다음 페이지를 불러오는 중이면 Loading UI를 렌더링한다", () => { + setWrittenReviews([mockReviewItem], true); + + renderWrittenReviewList(); + + const loading = screen.getByText("목록 로딩 중"); + expect(loading).toBeInTheDocument(); + }); + + test("리뷰 수정 모달에서 제출하면 리뷰 수정 mutation을 호출한다", async () => { + const { user } = renderWrittenReviewList(); + + const editButton = screen.getByRole("button", { name: "수정하기" }); + await user.click(editButton); + + const initialReview = screen.getByText("초기 리뷰: 함께 공부해서 좋았어요"); + const submitButton = screen.getByRole("button", { name: "리뷰 수정 제출" }); + + expect(initialReview).toBeInTheDocument(); + + await user.click(submitButton); + + expect(patchReviews).toHaveBeenCalledWith( + { reviewId: 123, reviewFormValues }, + expect.objectContaining({ onSuccess: expect.any(Function), onError: expect.any(Function) }), + ); + }); + + test("리뷰 삭제를 확인하면 리뷰 삭제 mutation을 호출한다", async () => { + const { user } = renderWrittenReviewList(); + + const deleteButton = screen.getByRole("button", { name: "삭제하기" }); + await user.click(deleteButton); + + const alertMessage = screen.getByText("리뷰를 삭제하시겠습니까?"); + const alertConfirmButton = screen.getByRole("button", { name: "확인" }); + + expect(alertMessage).toBeInTheDocument(); + + await user.click(alertConfirmButton); + + expect(deleteReviews).toHaveBeenCalledWith( + { reviewId: 123 }, + expect.objectContaining({ onSuccess: expect.any(Function), onError: expect.any(Function) }), + ); + }); + + test("데이터 조회 중 에러가 발생하면 ErrorBoundary fallback UI를 렌더링한다", () => { + useMyReviewInfinite.mockImplementation(() => { + throw new Error("query error"); + }); + + renderWrittenReviewList(); + + const errorMessage = screen.getByText("작성한 리뷰를 불러오지 못했습니다."); + expect(errorMessage).toBeInTheDocument(); + }); +}); From 533d01c36a1f6d61007e257767a2c7c3651dcf70 Mon Sep 17 00:00:00 2001 From: celine Date: Fri, 8 May 2026 16:18:01 +0900 Subject: [PATCH 8/8] =?UTF-8?q?test(mypage/Tabitem):=20=EB=A7=88=EC=9D=B4?= =?UTF-8?q?=ED=8E=98=EC=9D=B4=EC=A7=80=20=ED=83=AD=EC=BB=A8=ED=85=90?= =?UTF-8?q?=EC=B8=A0=20=EC=B0=9C=20=ED=86=A0=EA=B8=80=20=EB=B0=8F=20?= =?UTF-8?q?=EB=8B=A4=EC=8B=9C=EC=8B=9C=EB=8F=84=20=ED=85=8C=EC=8A=A4?= =?UTF-8?q?=ED=8A=B8=EC=BD=94=EB=93=9C=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Resolves:#360 --- .../mypage/AvailableReviewList/index.test.tsx | 5 +++-- .../mypage/CreatedMeetingList/index.test.tsx | 17 ++++++++++++++++- .../mypage/JoinedMeetingList/index.test.tsx | 7 +++++-- .../mypage/WrittenReviewList/index.test.tsx | 3 +++ 4 files changed, 27 insertions(+), 5 deletions(-) diff --git a/features/mypage/AvailableReviewList/index.test.tsx b/features/mypage/AvailableReviewList/index.test.tsx index 673ccb07..9957fe37 100644 --- a/features/mypage/AvailableReviewList/index.test.tsx +++ b/features/mypage/AvailableReviewList/index.test.tsx @@ -138,12 +138,13 @@ describe("AvailableReviewList", () => { }); test("찜 버튼을 클릭하면 찜 토글 핸들러를 호출한다", async () => { - const { user } = renderAvailableReviewList(); + setAvailableReviews([{ ...mockMeMeetingApiRes, isFavorited: false }]); + const { user } = renderAvailableReviewList(); const wishButton = screen.getByRole("button", { name: "찜 토글" }); await user.click(wishButton); - expect(handleWishToggle).toHaveBeenCalledWith(1000, false); + expect(handleWishToggle).toHaveBeenCalledWith(mockMeMeetingApiRes.id, false); }); test("리뷰 작성 모달에서 제출하면 리뷰 작성 mutation을 호출한다", async () => { diff --git a/features/mypage/CreatedMeetingList/index.test.tsx b/features/mypage/CreatedMeetingList/index.test.tsx index d93e8cbd..420314bf 100644 --- a/features/mypage/CreatedMeetingList/index.test.tsx +++ b/features/mypage/CreatedMeetingList/index.test.tsx @@ -5,6 +5,7 @@ import { mockMeMeetingApiRes } from "../mockData"; const patchMeetingsStatus = jest.fn(); const postMeetingReview = jest.fn(); +const handleWishToggle = jest.fn(); const reviewFormValues = { score: 5, comment: "좋은 모임이었어요" }; jest.mock("@/components/ui/Empty", () => { @@ -87,7 +88,7 @@ jest.mock("@/features/shared/components/ReviewModal", () => ({ jest.mock("@/hooks/useMeetingFavorite", () => ({ __esModule: true, - default: () => ({ handleWishToggle: jest.fn() }), + default: () => ({ handleWishToggle }), })); jest.mock("../queries", () => ({ @@ -196,6 +197,17 @@ describe("CreatedMeetingList", () => { ); }); + test("찜 버튼을 클릭하면 찜 토글 핸들러를 호출한다", async () => { + setCreatedMeetings([{ ...mockMeMeetingApiRes, isFavorited: false }]); + + const { user } = renderCreatedMeetingList(); + + const wishButton = screen.getByRole("button", { name: "찜 토글" }); + await user.click(wishButton); + + expect(handleWishToggle).toHaveBeenCalledWith(mockMeMeetingApiRes.id, false); + }); + test("데이터 조회 중 에러가 발생하면 ErrorBoundary fallback UI를 렌더링한다", () => { useMyCreatedInfinite.mockImplementation(() => { throw new Error("query error"); @@ -204,6 +216,9 @@ describe("CreatedMeetingList", () => { renderCreatedMeetingList(); const errorMessage = screen.getByText("내가 만든 모임을 불러오지 못했습니다."); + const retryButton = screen.getByRole("button", { name: "다시 시도" }); + expect(errorMessage).toBeInTheDocument(); + expect(retryButton).toBeInTheDocument(); }); }); diff --git a/features/mypage/JoinedMeetingList/index.test.tsx b/features/mypage/JoinedMeetingList/index.test.tsx index 7d76dfaa..d81939bf 100644 --- a/features/mypage/JoinedMeetingList/index.test.tsx +++ b/features/mypage/JoinedMeetingList/index.test.tsx @@ -169,11 +169,11 @@ describe("JoinedMeetingList", () => { test("찜 버튼을 클릭하면 찜 토글 핸들러를 호출한다", async () => { const { user } = renderJoinedMeetingList(); - + setJoinedMeetings([{ ...mockMeMeetingApiRes, isFavorited: false }]); const wishButton = screen.getByRole("button", { name: "찜 토글" }); await user.click(wishButton); - expect(handleWishToggle).toHaveBeenCalledWith(1000, false); + expect(handleWishToggle).toHaveBeenCalledWith(mockMeMeetingApiRes.id, false); }); test("데이터 조회 중 에러가 발생하면 ErrorBoundary fallback UI를 렌더링한다", () => { @@ -184,6 +184,9 @@ describe("JoinedMeetingList", () => { renderJoinedMeetingList(); const errorMessage = screen.getByText("나의 모임을 불러오지 못했습니다."); + const retryButton = screen.getByRole("button", { name: "다시 시도" }); + expect(errorMessage).toBeInTheDocument(); + expect(retryButton).toBeInTheDocument(); }); }); diff --git a/features/mypage/WrittenReviewList/index.test.tsx b/features/mypage/WrittenReviewList/index.test.tsx index 28e68cd1..3feafe78 100644 --- a/features/mypage/WrittenReviewList/index.test.tsx +++ b/features/mypage/WrittenReviewList/index.test.tsx @@ -229,6 +229,9 @@ describe("WrittenReviewList", () => { renderWrittenReviewList(); const errorMessage = screen.getByText("작성한 리뷰를 불러오지 못했습니다."); + const retryButton = screen.getByRole("button", { name: "다시 시도" }); + expect(errorMessage).toBeInTheDocument(); + expect(retryButton).toBeInTheDocument(); }); });