-
Notifications
You must be signed in to change notification settings - Fork 1
Feature/#300: library 페이지에 대한 테스트 코드 추가 #195
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
base: develop
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,40 @@ | ||
| import { render, screen, fireEvent } from '@testing-library/react'; | ||
| import GenreBadge, { GENRES } from '@/app/library/_components/GenreBadge'; | ||
|
|
||
| describe('GenreBadge 테스트', () => { | ||
| const mockDispatch = jest.fn(); | ||
| let buttons: HTMLElement[]; | ||
|
|
||
| const setup = (selectedGenres: string[] = []) => { | ||
| render( | ||
| <GenreBadge | ||
| dispatchSelectedGenres={mockDispatch} | ||
| selectedGenres={selectedGenres} | ||
| /> | ||
| ); | ||
| buttons = screen.getAllByRole('button'); | ||
| }; | ||
|
|
||
| beforeEach(() => { | ||
| mockDispatch.mockClear(); | ||
| }); | ||
|
|
||
| it('모든 장르 버튼이 렌더링됨', () => { | ||
| setup(); | ||
| const buttonLabels = buttons.map((btn) => btn.textContent); | ||
| expect(buttonLabels).toEqual(GENRES); | ||
| }); | ||
|
|
||
| it('"판타지" 배지를 클릭하면 ["판타지"]로 dispatch', () => { | ||
| setup(); | ||
| const fantasyBadge = buttons.find((btn) => btn.textContent === '판타지')!; | ||
| fireEvent.click(fantasyBadge); | ||
| expect(mockDispatch).toHaveBeenCalledWith(['판타지']); | ||
| }); | ||
|
|
||
| it('선택된 장르는 aria-pressed=true', () => { | ||
| setup(['판타지']); | ||
| const fantasyBadge = screen.getByRole('button', { name: '판타지' }); | ||
| expect(fantasyBadge).toHaveAttribute('aria-pressed', 'true'); | ||
| }); | ||
| }); |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,111 @@ | ||
| import { render, screen } from '@testing-library/react'; | ||
| import LibraryListContainer from '@/app/library/_components/LibraryListContainer'; | ||
| import { LibraryListContainerProps } from '@/app/library/_components/type'; | ||
|
|
||
| interface Story { | ||
| id: number; | ||
| title: string; | ||
| } | ||
|
|
||
| jest.mock('@/hooks/api/library/useInfiniteStories', () => ({ | ||
| useInfiniteStories: jest.fn(), | ||
| })); | ||
| jest.mock('@/hooks/useCurrentViewPort', () => ({ | ||
| __esModule: true, | ||
| default: jest.fn(), | ||
| })); | ||
| jest.mock('@/app/library/_components/LibraryListGrid', () => ({ | ||
| __esModule: true, | ||
| default: ({ stories }: { stories: Story[] }) => ( | ||
| <div data-testid="library-list-grid"> | ||
| {stories.map((s) => ( | ||
| <div key={s.id}>{s.title}</div> | ||
| ))} | ||
| </div> | ||
| ), | ||
| })); | ||
| jest.mock('@/app/library/_components/LibraryListSkeleton', () => ({ | ||
| __esModule: true, | ||
| default: () => <div data-testid="skeleton">로딩중...</div>, | ||
| })); | ||
| jest.mock('@/components/common/Observer/Observer', () => ({ | ||
| __esModule: true, | ||
| default: ({ enabled }: { enabled: boolean }) => ( | ||
| <div data-testid="observer">{enabled ? 'enabled' : 'disabled'}</div> | ||
| ), | ||
| })); | ||
|
|
||
| import { useInfiniteStories } from '@/hooks/api/library/useInfiniteStories'; | ||
| import useCurrentViewPort from '@/hooks/useCurrentViewPort'; | ||
|
Comment on lines
+38
to
+39
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 파일 중간에 import 문을 선언해주신 이유가 있을까요?
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. jest.mock()이 적용된 모듈의 mock 버전을 가져오기 위해 import를 중간에 배치한다고 합니다. 일반 ESM은 파일 최상단 import만 허용하지만, Jest는 mock 적용을 위해 변환 시 순서를 재조정하기 떄문에 위 방법이 가능하다고 합니다! |
||
| import { VIEWPORT_BREAK_POINT } from '@/constants/viewportBreakPoint'; | ||
|
|
||
| describe('LibraryListContainer', () => { | ||
| const mockUseInfiniteStories = useInfiniteStories as jest.Mock; | ||
| const mockUseCurrentViewPort = useCurrentViewPort as jest.Mock; | ||
|
|
||
| const defaultProps: LibraryListContainerProps = { | ||
| keyword: '', | ||
| searchType: '제목', | ||
| genres: [], | ||
| }; | ||
|
|
||
| beforeEach(() => { | ||
| jest.clearAllMocks(); | ||
| mockUseCurrentViewPort.mockReturnValue({ | ||
| viewportWidth: (VIEWPORT_BREAK_POINT.LG + VIEWPORT_BREAK_POINT.XL) / 2, | ||
| }); | ||
| }); | ||
|
|
||
| const setup = ( | ||
| storyData?: Story[], | ||
| options?: Partial<ReturnType<typeof useInfiniteStories>> | ||
| ) => { | ||
| mockUseInfiniteStories.mockReturnValue({ | ||
| data: storyData ? { pages: [storyData] } : { pages: [[]] }, | ||
| fetchNextPage: jest.fn(), | ||
| hasNextPage: false, | ||
| isFetchingNextPage: false, | ||
| isLoading: !storyData, | ||
| ...options, | ||
| }); | ||
|
|
||
| return render(<LibraryListContainer {...defaultProps} />); | ||
| }; | ||
|
|
||
| it('로딩 상태일 때 Skeleton을 렌더링한다', () => { | ||
| setup(undefined, { isLoading: true }); | ||
| expect(screen.getByTestId('skeleton')).toBeInTheDocument(); | ||
| }); | ||
|
|
||
| it('스토리가 없고 keyword가 빈 문자열이면 "아직 스토리가 없어요" 메시지를 표시한다', () => { | ||
| setup([]); | ||
| expect(screen.getByText('아직 스토리가 없어요')).toBeInTheDocument(); | ||
| }); | ||
|
|
||
| it('스토리가 없고 keyword가 있으면 "검색된 스토리가 없어요" 메시지를 표시한다', () => { | ||
| render( | ||
| <LibraryListContainer keyword="테스트" searchType="제목" genres={[]} /> | ||
| ); | ||
| mockUseInfiniteStories.mockReturnValue({ | ||
| data: { pages: [[]] }, | ||
| fetchNextPage: jest.fn(), | ||
| hasNextPage: false, | ||
| isFetchingNextPage: false, | ||
| isLoading: false, | ||
| }); | ||
|
|
||
| expect(screen.getByText('검색된 스토리가 없어요')).toBeInTheDocument(); | ||
| }); | ||
|
|
||
| it('스토리가 있으면 LibraryListGrid와 Observer를 렌더링한다', () => { | ||
| setup([{ id: 1, title: 'Story 1' }], { hasNextPage: true }); | ||
| expect(screen.getByTestId('library-list-grid')).toBeInTheDocument(); | ||
| expect(screen.getByTestId('observer')).toHaveTextContent('enabled'); | ||
| expect(screen.getByText('Story 1')).toBeInTheDocument(); | ||
| }); | ||
|
|
||
| it('hasNextPage가 false이면 Observer가 disabled 상태로 렌더링된다', () => { | ||
| setup([{ id: 1, title: 'Story 1' }], { hasNextPage: false }); | ||
| expect(screen.getByTestId('observer')).toHaveTextContent('disabled'); | ||
| }); | ||
| }); | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,78 @@ | ||
| import { render, screen, fireEvent } from '@testing-library/react'; | ||
| import SearchInput from '@/app/library/_components/SearchInput'; | ||
|
|
||
| describe('SearchInput 테스트', () => { | ||
| const mockSetKeyword = jest.fn(); | ||
| const mockOnSearch = jest.fn(); | ||
|
|
||
| const renderSearchInput = (keyword = '') => | ||
| render( | ||
| <SearchInput | ||
| keyword={keyword} | ||
| setKeyword={mockSetKeyword} | ||
| onSearch={mockOnSearch} | ||
| /> | ||
| ); | ||
|
|
||
| beforeEach(() => { | ||
| jest.clearAllMocks(); | ||
| }); | ||
|
|
||
| it('초기 렌더링 시 입력창에 keyword 값이 반영된다', () => { | ||
| renderSearchInput('초기값'); | ||
| const input = screen.getByLabelText( | ||
| '스토리 제목으로 검색' | ||
| ) as HTMLInputElement; | ||
| expect(input.value).toBe('초기값'); | ||
| }); | ||
|
|
||
| it('검색어를 입력하면 setKeyword가 호출된다', () => { | ||
| renderSearchInput(); | ||
| fireEvent.change(screen.getByLabelText('스토리 제목으로 검색'), { | ||
| target: { value: '천마재림' }, | ||
| }); | ||
| expect(mockSetKeyword).toHaveBeenCalledWith('천마재림'); | ||
| }); | ||
|
|
||
| it('검색어 입력 시 초기화 버튼이 나타난다', () => { | ||
| renderSearchInput(); | ||
| fireEvent.change(screen.getByLabelText('스토리 제목으로 검색'), { | ||
| target: { value: '천마재림' }, | ||
| }); | ||
| expect(screen.getByLabelText('검색어 초기화')).toBeInTheDocument(); | ||
| }); | ||
|
|
||
| it('검색어를 입력하고 엔터키를 눌렀을 때 onSearch가 호출된다', () => { | ||
| renderSearchInput(); | ||
| const input = screen.getByLabelText('스토리 제목으로 검색'); | ||
| fireEvent.change(input, { target: { value: '천마재림' } }); | ||
| fireEvent.submit(input.closest('form')!); | ||
| expect(mockOnSearch).toHaveBeenCalled(); | ||
| }); | ||
|
|
||
| it('검색어를 입력하고 검색 버튼을 클릭했을 때 onSearch가 호출된다', () => { | ||
| renderSearchInput(); | ||
| fireEvent.change(screen.getByLabelText('스토리 제목으로 검색'), { | ||
| target: { value: '천마재림' }, | ||
| }); | ||
| fireEvent.click(screen.getByLabelText('검색')); | ||
| expect(mockOnSearch).toHaveBeenCalled(); | ||
| }); | ||
|
|
||
| it('초기화 버튼 클릭 시 setKeyword가 빈 문자열로 호출되고 입력값이 비워진다', () => { | ||
| renderSearchInput('천마재림'); | ||
| fireEvent.click(screen.getByLabelText('검색어 초기화')); | ||
| expect(mockSetKeyword).toHaveBeenCalledWith(''); | ||
| expect( | ||
| (screen.getByLabelText('스토리 제목으로 검색') as HTMLInputElement).value | ||
| ).toBe(''); | ||
| }); | ||
|
|
||
| it('검색어가 비워질 때 onSearch가 자동 호출된다', () => { | ||
| renderSearchInput('천마재림'); | ||
| fireEvent.change(screen.getByLabelText('스토리 제목으로 검색'), { | ||
| target: { value: '' }, | ||
| }); | ||
| expect(mockOnSearch).toHaveBeenCalled(); | ||
| }); | ||
| }); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
여기서는 __esModule 옵션이 없는게 export default로 설정돼서 그런건가요?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
저도 정확히 몰라서 gpt한테 물어보니, const로 export 하는 경우에는 __esModules가 필요없다고 하네요 ㅎㅎ