Skip to content
Open
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
2 changes: 2 additions & 0 deletions .eslintrc.js
Original file line number Diff line number Diff line change
Expand Up @@ -52,5 +52,7 @@ module.exports = {

'react/prop-types': 'off',
'react/react-in-jsx-scope': 'off',

'import/no-unresolved': 'off',
},
};
3 changes: 3 additions & 0 deletions __mocks__/react-redux.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export const useDispatch = jest.fn((dispatch) => dispatch);

export const useSelector = jest.fn((selector) => selector({}));
4 changes: 4 additions & 0 deletions fixtures/data.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
export const regions = [{ id: 1, name: '서울' }, { id: 2, name: '부산' }];
export const categories = [{ id: 1, name: '한식' }, { id: 2, name: '양식' }];
export const restaurants = [{ id: 1, name: '코코식당' }, { id: 2, name: '네네식당' }];
export const selectedRegionAndCategory = { selectedRegion: { id: 1, name: '서울' }, selectedCategory: { id: 1, name: '한식' } };
11 changes: 10 additions & 1 deletion src/App.jsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,14 @@
import RegionContainer from './RegionContainer';
import CategoriesContainer from './CategoriesContainer';
import RestaurantsContainer from './RestaurantsContainer';

export default function App() {
return (
<></>
<>
<h1>Restaurants</h1>
<RegionContainer />
<CategoriesContainer />
<RestaurantsContainer />
</>
);
}
78 changes: 78 additions & 0 deletions src/App.test.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
import { fireEvent, render } from '@testing-library/react';
import { useDispatch, useSelector } from 'react-redux';
import App from './App';
import {
regions, categories, restaurants, selectedRegionAndCategory,
} from '../fixtures/data';

describe('App', () => {
jest.mock('react-redux');
jest.mock('./services/api');

const dispatch = jest.fn();

useDispatch.mockImplementation(() => dispatch);
useSelector.mockImplementation(() => ({
regions,
categories,
restaurants,
selectedRegionAndCategory,
}));

const renderApp = () => render(<App />);

describe('RegionContainer가 렌더링 된다.', () => {
it('지역정보들이 input으로 보인다.', () => {
const { getByText } = renderApp();
regions.forEach((region) => expect(getByText(new RegExp(`[${region.name}]`))).not.toBeNull());
});

it('input 클릭시 상태가 업데이트된다.', () => {
const { getByText } = renderApp();
fireEvent.click(getByText(/서울/));
expect(dispatch).toBeCalled();
});

it('선택된 input은 v가 표시된다.', () => {
const { getByText } = renderApp();
expect(getByText('서울 v')).not.toBeNull();
});
});

describe('CategoryContainer가 렌더링 된다.', () => {
it('카테고리 목록이 input으로 보인다.', () => {
const { getByText } = renderApp();
expect(getByText(/한식/)).not.toBeNull();
expect(getByText(/양식/)).not.toBeNull();
});

it('카테고리 input 클릭시 상태가 업데이트된다.', () => {
const { getByText } = renderApp();
fireEvent.click(getByText(/한식/));
expect(dispatch).toBeCalled();
});

it('선택된 input은 v가 표시된다.', () => {
const { getByText } = renderApp();
expect(getByText('한식 v')).not.toBeNull();
});
});

describe('RestaurantContainer가 렌더링 된다.', () => {
it('불러온 레스토랑 데이터들이 보인다.', () => {
const { getByText } = renderApp();
expect(getByText(/코코식당/)).not.toBeNull();
expect(getByText(/네네식당/)).not.toBeNull();
});
it('선택된 식당이 바뀌면 ', () => {
useSelector.mockImplementation(() => ({
restaurants: [
{ id: 1, name: '바뀐식당' }, { id: 2, name: '무슨식당' },
],
}));
const { getByText } = renderApp();
expect(getByText('바뀐식당')).not.toBeNull();
expect(getByText('무슨식당')).not.toBeNull();
});
});
});
31 changes: 31 additions & 0 deletions src/CategoriesContainer.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import { useEffect } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { loadCategories, setSelectedRegionAndCategory } from './action';
import Category from './Category';

export default function CategoriesContainer() {
const dispatch = useDispatch();
const { categories, selectedRegionAndCategory } = useSelector((state) => ({
categories: state.categories,
selectedRegionAndCategory: state.selectedRegionAndCategory,
}));

useEffect(() => {
Copy link
Contributor

Choose a reason for hiding this comment

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

여기 위에 빈 줄 하나를 추가해서 구분해주면 좋을 것 같아요

Copy link
Author

Choose a reason for hiding this comment

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

넵 반영하겠습니다 :)

dispatch(loadCategories());
}, []);

function handleClickCategory(category) {
dispatch(setSelectedRegionAndCategory({
selectedRegion: selectedRegionAndCategory?.selectedRegion,
selectedCategory: category,
}));
}

return (
<Category
categories={categories}
selectedRegionAndCategory={selectedRegionAndCategory}
onClick={handleClickCategory}
/>
);
}
45 changes: 45 additions & 0 deletions src/CategoriesContainer.test.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import { render, fireEvent } from '@testing-library/react';
import { useDispatch, useSelector } from 'react-redux';
import { categories } from '../fixtures/data';
import { setSelectedRegionAndCategory } from './action';
import CategoriesContainer from './CategoriesContainer';

describe('CategoriesContainer', () => {
const dispatch = jest.fn();
const renderCategoriesContainer = () => render(<CategoriesContainer />);

jest.mock('react-redux');
jest.mock('./services/api');
Comment on lines +11 to +12
Copy link
Contributor

Choose a reason for hiding this comment

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

이건 describe위에 선언해도 좋을 것 같아요. 이게 여기 안에 있으면 테스트에 따라서 다르게 바뀔수도 있다고 오해가 생길 것 같습니다

Copy link
Author

Choose a reason for hiding this comment

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

넵 알겠습니닷


useSelector.mockImplementation(() => ({ categories }));

useDispatch.mockImplementation(() => dispatch);

it('데이터 받기', () => {
const { getByText } = renderCategoriesContainer();
expect(getByText(categories[0].name)).not.toBeNull();
});

describe('button 클릭이 되면', () => {
it('클릭 된 버튼의 데이터를 state에 저장한다', () => {
const { getByText } = renderCategoriesContainer();
// expect(dispatch).not.toBeCalled();
fireEvent.click(getByText(categories[0].name));
expect(dispatch).toBeCalledWith(setSelectedRegionAndCategory({
selectedRegion: undefined,
selectedCategory: categories[0],
}));
});
});

// it('dispatch가 실행된다', () => {
// const category = categories[0];
// renderCategoriesContainer(category);

// expect(dispatch).not.toBeCalled();

// fireEvent.click(screen.getByText(category.name));

// expect(dispatch).toBeCalledWith(selectCategory(category.id));
// });
});
15 changes: 15 additions & 0 deletions src/Category.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
export default function Category({ categories, selectedRegionAndCategory, onClick }) {
return (
<>
{categories?.map((category) => (
<button
type="button"
key={category.id}
onClick={() => onClick(category)}
>
{selectedRegionAndCategory?.selectedCategory?.name === category.name ? `${category.name} v` : category.name}
</button>
))}
</>
);
}
29 changes: 29 additions & 0 deletions src/Category.test.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import { render, fireEvent } from '@testing-library/react';
import Category from './Category';
import { categories, selectedRegionAndCategory } from '../fixtures/data';

describe('Region', () => {
const onClick = jest.fn();

const renderCategory = () => render(
<Category
categories={categories}
selectedRegionAndCategory={selectedRegionAndCategory}
onClick={onClick}
/>,
);

it('지역 input이 보인다.', () => {
const { getByText } = renderCategory();
expect(getByText('양식')).not.toBeNull();
});
it('지역 input을 클릭하면 함수를 호출한다.', () => {
const { getByText } = renderCategory();
fireEvent.click(getByText('양식'));
expect(onClick).toBeCalledWith({ id: 2, name: '양식' });
});
it('선택된 input 옆에는 체크표시가 된다.', () => {
const { getByText } = renderCategory();
expect(getByText('한식 v')).not.toBeNull();
});
});
15 changes: 15 additions & 0 deletions src/Region.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
export default function Region({ regions, selectedRegionAndCategory, onClick }) {
return (
<div>
{regions?.map((item) => (
<button
type="button"
key={item.id}
onClick={() => onClick(item)}
>
{selectedRegionAndCategory?.selectedRegion?.name === item.name ? `${item.name} v` : item.name}
</button>
))}
</div>
);
}
29 changes: 29 additions & 0 deletions src/Region.test.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import { render, fireEvent } from '@testing-library/react';
import Region from './Region';
import { regions, selectedRegionAndCategory } from '../fixtures/data';

describe('Region', () => {
const onClick = jest.fn();

const renderRegion = () => render(
<Region
regions={regions}
selectedRegionAndCategory={selectedRegionAndCategory}
onClick={onClick}
/>,
);

it('지역 input이 보인다.', () => {
const { getByText } = renderRegion();
expect(getByText('부산')).not.toBeNull();
});
it('지역 input을 클릭하면 함수를 호출한다.', () => {
const { getByText } = renderRegion();
fireEvent.click(getByText('부산'));
expect(onClick).toBeCalledWith({ id: 2, name: '부산' });
});
it('선택된 input 옆에는 체크표시가 된다.', () => {
const { getByText } = renderRegion();
expect(getByText('서울 v')).not.toBeNull();
});
});
32 changes: 32 additions & 0 deletions src/RegionContainer.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import { useEffect } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { setSelectedRegionAndCategory, loadRegions } from './action';
import Region from './Region';

export default function RegionContainer() {
// 데이터 페칭하기
const dispatch = useDispatch();
const { regions, selectedRegionAndCategory } = useSelector((state) => ({
regions: state.regions,
selectedRegionAndCategory: state.selectedRegionAndCategory,
}));

useEffect(() => {
dispatch(loadRegions());
}, []);

function handleClickButton(item) {
dispatch(setSelectedRegionAndCategory({
selectedCategory: selectedRegionAndCategory?.selectedCategory,
selectedRegion: item,
}));
}

return (
<Region
regions={regions}
selectedRegionAndCategory={selectedRegionAndCategory}
onClick={handleClickButton}
/>
);
}
22 changes: 22 additions & 0 deletions src/RegionContainer.test.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { fireEvent, render } from '@testing-library/react';
import { useDispatch, useSelector } from 'react-redux';
import { regions } from '../fixtures/data';
import RegionContainer from './RegionContainer';

describe('RegionContainer', () => {
const dispatch = jest.fn();
jest.mock('react-redux');
jest.mock('./services/api');
useDispatch.mockImplementation(() => dispatch);
useSelector.mockImplementation(() => ({ regions }));

const renderRegionContainer = () => render(<RegionContainer />);

it('데이터 받기', () => {
const { getByText } = renderRegionContainer();

expect(getByText('서울')).not.toBeNull();
fireEvent.click(getByText('서울'));
expect(dispatch).toBeCalled();
});
});
7 changes: 7 additions & 0 deletions src/Restaurants.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
export default function Restaurants({ restaurants }) {
return (
<>
{restaurants?.map((restaurant) => <li key={restaurant.id}>{restaurant.name}</li>)}
</>
);
}
13 changes: 13 additions & 0 deletions src/Restaurants.test.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { render } from '@testing-library/react';
import Restaurants from './Restaurants';
import { restaurants } from '../fixtures/data';

describe('RestaurantsContainer', () => {
const renderRestaurants = () => render(<Restaurants restaurants={restaurants} />);

it('식당 리스트가 보인다', () => {
const { getByText } = renderRestaurants();
expect(getByText('코코식당')).not.toBeNull();
expect(getByText('네네식당')).not.toBeNull();
});
});
29 changes: 29 additions & 0 deletions src/RestaurantsContainer.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import { useEffect } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { loadRestaurants } from './action';
import Restaurants from './Restaurants';

export default function RestaurantsContainer() {
const { restaurants, selectedRegion, selectedCategory } = useSelector((state) => ({
restaurants: state.restaurants,
selectedRegion: state.selectedRegionAndCategory.selectedRegion,
selectedCategory: state.selectedRegionAndCategory.selectedCategory,
}));

const dispatch = useDispatch();

useEffect(() => {
const isEmptyObject = (object) => Object.keys(object || {}).length === 0;

if (isEmptyObject(selectedRegion) || isEmptyObject(selectedCategory)) {
return;
}
dispatch(loadRestaurants(selectedRegion, selectedCategory));
}, [selectedRegion, selectedCategory]);

return (
<ul>
<Restaurants restaurants={restaurants} />
</ul>
);
}
Loading