diff --git a/src/components/pages/pending/pending-members/index.test.tsx b/src/components/pages/pending/pending-members/index.test.tsx new file mode 100644 index 00000000..49c341ce --- /dev/null +++ b/src/components/pages/pending/pending-members/index.test.tsx @@ -0,0 +1,314 @@ +import { useRouter } from 'next/navigation'; + +import { render, screen } from '@testing-library/react'; + +import { API } from '@/api'; +import { GetJoinRequestsResponse, GroupUserV2Status } from '@/types/service/group'; + +import { GroupPendingMembers } from './index'; + +jest.mock('@tanstack/react-query', () => ({ + useQuery: jest.fn(), + useMutation: jest.fn(), + useQueryClient: jest.fn(), +})); + +jest.mock('next/navigation', () => ({ + useRouter: jest.fn(), +})); + +jest.mock('@/components/ui/toast/core', () => ({ + useToast: jest.fn(), +})); + +jest.mock('@/api', () => ({ + API: { + groupService: { + getJoinRequests: jest.fn(), + approveJoinRequest: jest.fn(), + rejectJoinRequest: jest.fn(), + }, + }, +})); + +jest.mock('./pending-member-card', () => ({ + PendingMemberCard: jest.fn(({ onApprove, onReject }) => ( +
+ + +
+ )), +})); + +jest.mock('./pending-members-loading', () => ({ + PendingMembersSkeleton: jest.fn(() =>
), +})); + +jest.mock('@/components/layout/empty-state', () => ({ + EmptyState: jest.fn(() =>
), +})); + +import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'; + +import { useToast } from '@/components/ui/toast/core'; + +const mockUseQuery = useQuery as jest.MockedFunction; +const mockUseMutation = useMutation as jest.MockedFunction; +const mockUseQueryClient = useQueryClient as jest.MockedFunction; +const mockUseRouter = useRouter as jest.MockedFunction; +const mockUseToast = useToast as jest.MockedFunction; + +interface MutationConfig { + mutationFn: (targetUserId: string) => Promise; + onSuccess?: () => Promise; +} + +describe('GroupPendingMembers', () => { + const mockGroupId = '1'; + const mockRouter = { + replace: jest.fn(), + }; + const mockQueryClient = { + invalidateQueries: jest.fn().mockResolvedValue(undefined), + }; + const mockRun = jest.fn(); + + const mockMembers: GetJoinRequestsResponse = { + groupId: 1, + groupTitle: '동탄에서 놀 사람 모이세요', + thumbnail100x100Url: null, + status: 'PENDING' as GroupUserV2Status, + count: 1, + items: [ + { + userId: 1, + groupUserId: 1, + nickName: '1등', + profileImage: null, + status: 'PENDING' as GroupUserV2Status, + joinedAt: '2026-01-23T00:00:00', + joinRequestMessage: '안녕하세요 이소망입니다', + }, + ], + serverTime: '2026-01-23T00:00:00', + }; + + beforeEach(() => { + jest.clearAllMocks(); + + mockUseRouter.mockReturnValue(mockRouter as unknown as ReturnType); + mockUseQueryClient.mockReturnValue( + mockQueryClient as unknown as ReturnType, + ); + mockUseToast.mockReturnValue({ run: mockRun } as ReturnType); + + mockUseQuery.mockReturnValue({ + data: mockMembers, + isLoading: false, + error: null, + } as unknown as ReturnType); + + mockUseMutation.mockReturnValue({ + mutate: jest.fn(), + } as unknown as ReturnType); + }); + + test('컴포넌트가 정상적으로 렌더링된다', () => { + render(); + + expect(screen.getByTestId('pending-member-card')).toBeInTheDocument(); + }); + + test('queryFn이 올바른 API를 호출한다', () => { + render(); + + const queryOptions = mockUseQuery.mock.calls[0][0] as unknown as { + queryFn: () => Promise; + }; + const queryFn = queryOptions.queryFn; + queryFn(); + + expect(API.groupService.getJoinRequests).toHaveBeenCalledWith( + { groupId: mockGroupId }, + 'PENDING', + ); + }); + + test('approveMutation이 올바른 API를 호출한다', async () => { + const mutationConfigs: MutationConfig[] = []; + + mockUseMutation.mockImplementation((config: unknown) => { + mutationConfigs.push(config as MutationConfig); + return { + mutate: jest.fn(), + } as unknown as ReturnType; + }); + + render(); + + const approveMutation = mutationConfigs.find((config) => + config.mutationFn.toString().includes('approveJoinRequest'), + ); + + expect(approveMutation).toBeDefined(); + if (approveMutation) { + const mutationFn = approveMutation.mutationFn; + await mutationFn('1'); + + expect(API.groupService.approveJoinRequest).toHaveBeenCalledWith({ + groupId: mockGroupId, + targetUserId: '1', + }); + + if (approveMutation.onSuccess) { + await approveMutation.onSuccess(); + expect(mockQueryClient.invalidateQueries).toHaveBeenCalled(); + expect(mockRun).toHaveBeenCalled(); + } + } + }); + + test('rejectMutation이 올바른 API를 호출한다', async () => { + const mutationConfigs: MutationConfig[] = []; + + mockUseMutation.mockImplementation((config: unknown) => { + mutationConfigs.push(config as MutationConfig); + return { + mutate: jest.fn(), + } as unknown as ReturnType; + }); + + render(); + + const rejectMutation = mutationConfigs.find((config) => + config.mutationFn.toString().includes('rejectJoinRequest'), + ); + + expect(rejectMutation).toBeDefined(); + if (rejectMutation) { + const mutationFn = rejectMutation.mutationFn; + await mutationFn('1'); + + expect(API.groupService.rejectJoinRequest).toHaveBeenCalledWith({ + groupId: mockGroupId, + targetUserId: '1', + }); + + if (rejectMutation.onSuccess) { + await rejectMutation.onSuccess(); + expect(mockQueryClient.invalidateQueries).toHaveBeenCalled(); + expect(mockRun).toHaveBeenCalled(); + } + } + }); + + test('isForbidden이 true일 때 router.replace가 호출된다', () => { + const error403 = { status: 403 }; + mockUseQuery.mockReturnValue({ + data: null, + isLoading: false, + error: error403, + } as unknown as ReturnType); + + render(); + + expect(mockRouter.replace).toHaveBeenCalledWith('/'); + }); + + test('로딩 중일 때 Skeleton을 표시한다', () => { + mockUseQuery.mockReturnValue({ + data: null, + isLoading: true, + error: null, + } as unknown as ReturnType); + + render(); + + expect(screen.getByTestId('skeleton')).toBeInTheDocument(); + }); + + test('에러가 발생하면 에러 메시지를 표시한다', () => { + const error500 = { status: 500 }; + mockUseQuery.mockReturnValue({ + data: null, + isLoading: false, + error: error500, + } as unknown as ReturnType); + + render(); + + expect(screen.getByText('데이터를 불러오는데 실패했습니다.')).toBeInTheDocument(); + }); + + test('데이터가 없거나 빈 배열일 때 EmptyState를 표시한다', () => { + const emptyData: GetJoinRequestsResponse = { + groupId: 1, + groupTitle: '미니랑 구정에 산책하실 분', + thumbnail100x100Url: null, + status: 'PENDING' as GroupUserV2Status, + count: 0, + items: [], + serverTime: '2026-01-23T00:00:00', + }; + + mockUseQuery.mockReturnValue({ + data: emptyData, + isLoading: false, + error: null, + } as unknown as ReturnType); + + render(); + + expect(screen.getByTestId('empty-state')).toBeInTheDocument(); + }); + + test('handleApprove가 approveMutation.mutate를 호출한다', () => { + const mockMutate = jest.fn(); + let approveMutationInstance: { mutate: jest.Mock } | null = null; + + mockUseMutation.mockImplementation((config: unknown) => { + const mutationConfig = config as MutationConfig; + if (mutationConfig.mutationFn.toString().includes('approveJoinRequest')) { + approveMutationInstance = { mutate: mockMutate }; + return approveMutationInstance as unknown as ReturnType; + } + return { + mutate: jest.fn(), + } as unknown as ReturnType; + }); + + render(); + + const approveButton = screen.getByTestId('approve-button'); + approveButton.click(); + + expect(mockMutate).toHaveBeenCalledWith('1'); + }); + + test('handleReject가 rejectMutation.mutate를 호출한다', () => { + const mockMutate = jest.fn(); + let rejectMutationInstance: { mutate: jest.Mock } | null = null; + + mockUseMutation.mockImplementation((config: unknown) => { + const mutationConfig = config as MutationConfig; + if (mutationConfig.mutationFn.toString().includes('rejectJoinRequest')) { + rejectMutationInstance = { mutate: mockMutate }; + return rejectMutationInstance as unknown as ReturnType; + } + return { + mutate: jest.fn(), + } as unknown as ReturnType; + }); + + render(); + + const rejectButton = screen.getByTestId('reject-button'); + rejectButton.click(); + + expect(mockMutate).toHaveBeenCalledWith('1'); + }); +});