Skip to content
Merged
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
43 changes: 43 additions & 0 deletions src/app/message/page.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import { render, screen } from '@testing-library/react';

import { ModalProvider } from '@/components/ui';
import { useInfiniteScroll } from '@/hooks/use-group/use-group-infinite-list';

import FollowingPage from './page';

jest.mock('@/hooks/use-group/use-group-infinite-list', () => ({
useInfiniteScroll: jest.fn(),
}));

jest.mock('next/navigation', () => ({
useSearchParams: () => ({
get: () => 'following',
}),
}));

jest.mock('js-cookie', () => ({
get: () => '1',
}));

describe('FollowingPage 테스트', () => {
beforeEach(() => {
(useInfiniteScroll as jest.Mock).mockReturnValue({
items: [],
error: null,
fetchNextPage: jest.fn(),
hasNextPage: false,
isFetchingNextPage: false,
completedMessage: '',
});
});

test('팔로잉이 없을 경우 FollowingNone을 보여준다', async () => {
render(
<ModalProvider>
<FollowingPage />
</ModalProvider>,
);

expect(await screen.findByText('아직 팔로우 한 사람이 없어요.')).toBeInTheDocument();
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { act, fireEvent, render, screen, waitFor } from '@testing-library/react';

import { ModalProvider } from '@/components/ui';
import { useAddFollowers } from '@/hooks/use-follower';

import { FollowingModal } from '.';

// Mock 설정
jest.mock('@/hooks/use-follower');

const createQueryClient = () =>
new QueryClient({
defaultOptions: {
queries: { retry: false },
mutations: { retry: false },
},
});

const renderWithQueryClient = async (component: React.ReactElement) => {
const testQueryClient = createQueryClient();
let renderResult;

await act(async () => {
renderResult = render(
<QueryClientProvider client={testQueryClient}>
<ModalProvider>{component}</ModalProvider>
</QueryClientProvider>,
);
});

return renderResult;
};

describe('FollowingModal 테스트', () => {
const mockMutate = jest.fn();
const mockUserId = 123;

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

// 기본 mock 설정
(useAddFollowers as jest.Mock).mockReturnValue({
mutate: mockMutate,
});
});

test('FollowingModal 렌더링 테스트', async () => {
await renderWithQueryClient(<FollowingModal userId={mockUserId} />);

expect(screen.getByText('팔로우 할 닉네임을 입력하세요')).toBeInTheDocument();
expect(screen.getByPlaceholderText('nickname')).toBeInTheDocument();
});

test('닉네임 입력이 정상적으로 동작한다', async () => {
await renderWithQueryClient(<FollowingModal userId={mockUserId} />);

const input = screen.getByPlaceholderText('nickname');
fireEvent.change(input, { target: { value: 'test' } });

expect(input).toHaveValue('test');
});

test('Enter 키 입력 시 폼이 제출된다', async () => {
mockMutate.mockImplementation((_data, options) => {
options?.onSuccess?.();
});

await renderWithQueryClient(<FollowingModal userId={mockUserId} />);

const input = screen.getByRole('textbox');
fireEvent.change(input, { target: { value: 'test' } });
fireEvent.keyDown(input, { key: 'Enter', code: 'Enter' });

await waitFor(() => {
expect(mockMutate).toHaveBeenCalledWith({ followNickname: 'test' }, expect.any(Object));
});
});
});
82 changes: 82 additions & 0 deletions src/components/pages/message/message-following-modal/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
import { useState } from 'react';

import { useForm } from '@tanstack/react-form';

import { Icon } from '@/components/icon';
import { Button, Input, ModalContent, ModalTitle, useModal } from '@/components/ui';
import { useAddFollowers } from '@/hooks/use-follower';

export const FollowingModal = ({ userId }: { userId: number }) => {
const { close } = useModal();
const [errorMessage, setErrorMessage] = useState<string | null>(null);
const { mutate: addFollower } = useAddFollowers({ userId });
const form = useForm({
defaultValues: {
nickname: '',
},
onSubmit: ({ value }) => {
const { nickname } = value;
setErrorMessage(null);

addFollower(
{
followNickname: nickname,
},
{
onSuccess: () => {
close();
},
onError: () => {
setErrorMessage('존재하지 않는 유저입니다.');
},
},
);
},
});
return (
<ModalContent className='mx-8'>
<ModalTitle className='mb-3'>팔로우 할 닉네임을 입력하세요</ModalTitle>
<form
onSubmit={(e) => {
e.preventDefault();
form.handleSubmit();
}}
>
<div className='mb-3'>
<form.Field
children={(field) => (
<Input
className='text-text-sm-medium w-full rounded-3xl bg-gray-100 px-4 py-2.5 text-gray-800'
iconButton={
<Icon id='search' className='absolute top-2.5 right-3 size-5 text-gray-500' />
}
placeholder='nickname'
value={field.state.value}
onChange={(e) => {
field.handleChange(e.target.value);
setErrorMessage(null);
}}
onKeyDown={(e) => {
if (e.key === 'Enter') {
e.preventDefault();
form.handleSubmit();
}
}}
/>
)}
name='nickname'
></form.Field>
</div>
{errorMessage && <p className='text-error-500 mb-2 ml-2 text-sm'>{errorMessage}</p>}
<div className='flex w-full flex-row gap-2'>
<Button size='sm' type='button' variant='tertiary' onClick={close}>
취소
</Button>
<Button size='sm' type='submit'>
팔로우
</Button>
</div>
</form>
</ModalContent>
);
};
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

message-following-search 컴포넌트를 지금봤는데, 이 파일 내부에서 모달과 팔로우 추가 버튼 둘다 관리하고 있네용

  1. 모달을 별도 파일로 분리하기(message-following-modal ? )
  2. 모달 내부에서 input값을 현재 직접 state로 제어하는 방식 => tanstack form 관리 방식으로 수정
  3. errorMessage를 zod로 제어하기

프로젝트 통일성을 위해 이 정도로 수정되면 좋을 것 같습니다!

Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { fireEvent, render, screen } from '@testing-library/react';
import { act, fireEvent, render, screen } from '@testing-library/react';

import { ModalProvider } from '@/components/ui';

Expand All @@ -8,39 +8,35 @@ import { FollowingSearch } from '.';
const createQueryClient = () =>
new QueryClient({
defaultOptions: {
queries: {
retry: false,
},
mutations: {
retry: false,
},
queries: { retry: false },
mutations: { retry: false },
},
});

describe('Following Search 테스트', () => {
const renderComponent = async () => {
const queryClient = createQueryClient();
test('Following Search 렌더링 테스트', () => {

await act(async () => {
render(
<QueryClientProvider client={queryClient}>
<ModalProvider>
<FollowingSearch userId={0} />
</ModalProvider>
</QueryClientProvider>,
);
});
};

describe('FollowingSearch', () => {
test('렌더링된다', async () => {
await renderComponent();
expect(screen.getByText('팔로우 추가')).toBeInTheDocument();
});

test('팔로우 추가 클릭 시 모달 생성', () => {
render(
<QueryClientProvider client={queryClient}>
<ModalProvider>
<FollowingSearch userId={0} />
</ModalProvider>
</QueryClientProvider>,
);
test('클릭 시 FollowingModal이 열린다', async () => {
await renderComponent();

expect(screen.queryByText('팔로우 할 닉네임을 입력하세요')).toBeNull();
expect(screen.queryByText('팔로우 할 닉네임을 입력하세요')).not.toBeInTheDocument();

fireEvent.click(screen.getByText('팔로우 추가'));

Expand Down
74 changes: 3 additions & 71 deletions src/components/pages/message/message-following-search/index.tsx
Original file line number Diff line number Diff line change
@@ -1,84 +1,16 @@
'use client';
import { useState } from 'react';

import { Icon } from '@/components/icon';
import { Button, Input, ModalContent, ModalTitle, useModal } from '@/components/ui';
import { useAddFollowers } from '@/hooks/use-follower';
import { useModal } from '@/components/ui';

const FollowerModal = ({ userId }: { userId: number }) => {
const { close } = useModal();
const [nickname, setNickname] = useState('');
const [errorMessage, setErrorMessage] = useState('');
const { mutate: addFollower } = useAddFollowers({ userId });

const handleConfirm = () => {
if (!nickname.trim()) {
setErrorMessage('닉네임을 입력해주세요.');
return;
}

setErrorMessage(''); // 에러 메세지 초기화.

addFollower(
{ followNickname: nickname },
{
onSuccess: () => {
close();
},
onError: () => {
setErrorMessage('존재하지 않는 유저입니다.');
},
},
);
};

const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const value = e.target.value;
setNickname(value);
if (errorMessage) {
setErrorMessage('');
}
};

// 모달 모양 바뀌면 적용하기!
return (
<ModalContent className='mx-8'>
<ModalTitle className='mb-3'>팔로우 할 닉네임을 입력하세요</ModalTitle>
<div className='mb-3'>
<Input
className='text-text-sm-medium w-full rounded-3xl bg-gray-100 px-4 py-2.5 text-gray-800'
iconButton={
<Icon id='search' className='absolute top-2.5 right-3 size-5 text-gray-500' />
}
placeholder='nickname'
value={nickname}
onChange={handleChange}
onKeyDown={(e) => {
if (e.key === 'Enter') {
handleConfirm();
}
}}
/>
{errorMessage && <p className='text-error-500 mt-2 text-sm'>{errorMessage}</p>}
</div>
<div className='flex w-full flex-row gap-2'>
<Button size='sm' variant='tertiary' onClick={close}>
취소
</Button>
<Button size='sm' onClick={handleConfirm}>
팔로우
</Button>
</div>
</ModalContent>
);
};
import { FollowingModal } from '../message-following-modal';

export const FollowingSearch = ({ userId }: { userId: number }) => {
const { open } = useModal();
return (
<div
className='flex items-center gap-5 px-5 py-4 transition-all hover:cursor-pointer hover:opacity-80'
onClick={() => open(<FollowerModal userId={userId} />)}
onClick={() => open(<FollowingModal userId={userId} />)}
>
<div className='rounded-full border-2 border-dashed border-gray-400 bg-gray-100 p-2'>
<Icon id='plus' className='size-6 text-gray-700' />
Expand Down