diff --git a/src/App.jsx b/src/App.jsx
index 8b0d9b1..c269fbe 100644
--- a/src/App.jsx
+++ b/src/App.jsx
@@ -11,13 +11,15 @@ import { setUser } from './reducers/authSlice';
import { lightTheme, darkTheme } from './styles/theme';
+import ErrorBoundary from './ErrorBoundary';
+
import MainPage from './pages/MainPage';
import WritePage from './pages/WritePage';
-import GlobalStyles from './styles/GlobalStyles';
-import IntroducePage from './pages/IntroducePage';
import LoginPage from './pages/LoginPage';
import RegisterPage from './pages/RegisterPage';
-import HeaderContainer from './containers/base/HeaderContainer';
+import NotFoundPage from './pages/NotFoundPage';
+import GlobalStyles from './styles/GlobalStyles';
+import IntroducePage from './pages/IntroducePage';
const App = () => {
const dispatch = useDispatch();
@@ -34,17 +36,19 @@ const App = () => {
}, [dispatch, user]);
return (
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
);
};
diff --git a/src/App.test.jsx b/src/App.test.jsx
index d02a33b..9e6dcaf 100644
--- a/src/App.test.jsx
+++ b/src/App.test.jsx
@@ -58,6 +58,7 @@ describe('App', () => {
},
commonReducer: {
theme: given.theme,
+ errorType: null,
},
}));
});
@@ -101,6 +102,16 @@ describe('App', () => {
});
});
+ context('with Not Found path', () => {
+ given('group', () => ([]));
+
+ it('renders the study introduce page', () => {
+ const { container } = renderApp({ path: '/some-not-found' });
+
+ expect(container).toHaveTextContent('아무것도 없어요!');
+ });
+ });
+
context('with path /introduce', () => {
given('group', () => (STUDY_GROUP));
it('renders the study introduce page', () => {
diff --git a/src/ErrorBoundary.jsx b/src/ErrorBoundary.jsx
new file mode 100644
index 0000000..8e98fa5
--- /dev/null
+++ b/src/ErrorBoundary.jsx
@@ -0,0 +1,46 @@
+/* eslint-disable no-unused-vars */
+/* eslint-disable react/destructuring-assignment */
+import React from 'react';
+
+import useNotFound from './hooks/useNotFound';
+
+import NotFoundPage from './pages/NotFoundPage';
+
+function ErrorBoundaryWrapper({ children }) {
+ const { isNotFound } = useNotFound();
+
+ if (isNotFound) {
+ return ;
+ }
+
+ return <>{children}>;
+}
+
+class ErrorBoundary extends React.Component {
+ constructor(props) {
+ super(props);
+ this.state = { hasError: false };
+ }
+
+ static getDerivedStateFromError(error) {
+ return { hasError: true };
+ }
+
+ componentDidCatch(error, errorInfo) {
+ // logErrorToMyService(error, errorInfo);
+ }
+
+ render() {
+ if (this.state.hasError) {
+ return
앗! 알 수 없는 오류가 발생했어요!
;
+ }
+
+ return (
+
+ {this.props.children}
+
+ );
+ }
+}
+
+export default ErrorBoundary;
diff --git a/src/ErrorBoundary.test.jsx b/src/ErrorBoundary.test.jsx
new file mode 100644
index 0000000..691c9bc
--- /dev/null
+++ b/src/ErrorBoundary.test.jsx
@@ -0,0 +1,83 @@
+import './util/__mocks__/matchMedia';
+
+import React from 'react';
+
+import { useSelector, useDispatch } from 'react-redux';
+
+import { render } from '@testing-library/react';
+
+import ErrorBoundary from './ErrorBoundary';
+
+jest.mock('react-redux');
+
+describe('ErrorBoundary', () => {
+ const dispatch = jest.fn();
+
+ beforeEach(() => {
+ dispatch.mockClear();
+
+ useDispatch.mockImplementation(() => dispatch);
+
+ useSelector.mockImplementation((selector) => selector({
+ commonReducer: {
+ errorType: given.errorType,
+ },
+ }));
+ });
+
+ const renderErrorBoundary = (ui) => render(ui);
+
+ context('Has Unknown Error', () => {
+ given('errorType', () => null);
+
+ const MockComponent = () => {
+ throw new Error('error');
+ };
+
+ it('should be renders "앗! 알 수 없는 오류가 발생했어요!" Error Message', () => {
+ const { container } = renderErrorBoundary((
+
+
+
+ ));
+
+ expect(container).toHaveTextContent('앗! 알 수 없는 오류가 발생했어요!');
+ });
+ });
+
+ context("Hasn't Unknown Error", () => {
+ context('Without Not Found Error Type', () => {
+ given('errorType', () => null);
+ const MockComponent = () => (
+ 정상입니다!
+ );
+
+ it('should be Success render component', () => {
+ const { container } = renderErrorBoundary((
+
+
+
+ ));
+
+ expect(container).toHaveTextContent('정상입니다!');
+ });
+ });
+
+ context('With Not Found Error Type', () => {
+ given('errorType', () => 'NOT_FOUND');
+ const MockComponent = () => (
+ 정상입니다!
+ );
+
+ it('should be Success render component', () => {
+ const { container } = renderErrorBoundary((
+
+
+
+ ));
+
+ expect(container).toHaveTextContent('아무것도 없어요!');
+ });
+ });
+ });
+});
diff --git a/src/assets/icons/404.svg b/src/assets/icons/404.svg
new file mode 100644
index 0000000..7433c10
--- /dev/null
+++ b/src/assets/icons/404.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/src/components/common/ErrorScreenTemplate.jsx b/src/components/common/ErrorScreenTemplate.jsx
new file mode 100644
index 0000000..72a1b63
--- /dev/null
+++ b/src/components/common/ErrorScreenTemplate.jsx
@@ -0,0 +1,72 @@
+import React from 'react';
+
+import styled from '@emotion/styled';
+
+import mq from '../../styles/responsive';
+import palette from '../../styles/palette';
+
+import Button from '../../styles/Button';
+
+const ErrorScreenTemplateBlock = styled.div`
+ top: -40px;
+ left: 0px;
+ position: fixed;
+ width: 100%;
+ height: 100%;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+`;
+
+const ErrorScreenWrapper = styled.div`
+ display: flex;
+ width: 100%;
+ height: auto;
+ align-items: center;
+ justify-content: center;
+ flex-direction: column;
+ -webkit-box-align: center;
+ -webkit-box-pack: center;
+
+ .message {
+ ${mq({ fontSize: ['2rem', '3rem'] })};
+
+ text-align: center;
+ line-height: 1.5;
+ margin-bottom: 2rem;
+ }
+`;
+
+const StyledButton = styled(Button)`
+ ${mq({
+ fontSize: ['1.2rem', '1.4rem'],
+ padding: ['0.4rem 1rem', '0.6rem 1.2rem'],
+
+ })};
+
+ transition: none;
+
+ &:hover {
+ background: ${palette.teal[4]};
+ color: white;
+ border: 2px solid ${palette.teal[4]};
+ }
+`;
+
+const ErrorScreenTemplate = ({
+ message, buttonText, onClick, children,
+}) => (
+
+
+ {children}
+
+ {message}
+
+
+ {buttonText}
+
+
+
+);
+
+export default ErrorScreenTemplate;
diff --git a/src/components/common/ErrorScreenTemplate.test.jsx b/src/components/common/ErrorScreenTemplate.test.jsx
new file mode 100644
index 0000000..2286ea0
--- /dev/null
+++ b/src/components/common/ErrorScreenTemplate.test.jsx
@@ -0,0 +1,42 @@
+import React from 'react';
+
+import { render, fireEvent } from '@testing-library/react';
+
+import ErrorScreenTemplate from './ErrorScreenTemplate';
+
+describe('ErrorScreenTemplate', () => {
+ const handleClick = jest.fn();
+
+ beforeEach(() => {
+ handleClick.mockClear();
+ });
+
+ const renderErrorScreenTemplate = ({ message, buttonText }) => render((
+
+ ));
+
+ it('should be renders error template contents', () => {
+ const { container } = renderErrorScreenTemplate({
+ message: '아무것도 없어요!',
+ buttonText: '홈으로',
+ });
+
+ expect(container).toHaveTextContent('아무것도 없어요!');
+ expect(container).toHaveTextContent('홈으로');
+ });
+
+ it('handle Click event', () => {
+ const { getByText } = renderErrorScreenTemplate({
+ message: '아무것도 없어요!',
+ buttonText: '홈으로',
+ });
+
+ fireEvent.click(getByText('홈으로'));
+
+ expect(handleClick).toBeCalled();
+ });
+});
diff --git a/src/containers/error/NotFoundContainer.jsx b/src/containers/error/NotFoundContainer.jsx
new file mode 100644
index 0000000..1cc7974
--- /dev/null
+++ b/src/containers/error/NotFoundContainer.jsx
@@ -0,0 +1,44 @@
+import React from 'react';
+
+import { useHistory } from 'react-router-dom';
+
+import styled from '@emotion/styled';
+
+import mq from '../../styles/responsive';
+
+import useNotFound from '../../hooks/useNotFound';
+
+import NotFoundSvg from '../../assets/icons/404.svg';
+import ErrorScreenTemplate from '../../components/common/ErrorScreenTemplate';
+
+const NotFoundImage = styled(NotFoundSvg)`
+ ${mq({
+ width: ['500px', '700px', '800px'],
+ })};
+
+ height: auto;
+`;
+
+const NotFoundContainer = () => {
+ const history = useHistory();
+ const { reset } = useNotFound();
+
+ const onClick = () => {
+ history.push('/');
+ reset();
+ };
+
+ return (
+
+
+
+ );
+};
+
+export default NotFoundContainer;
diff --git a/src/containers/error/NotFoundContainer.test.jsx b/src/containers/error/NotFoundContainer.test.jsx
new file mode 100644
index 0000000..a178b44
--- /dev/null
+++ b/src/containers/error/NotFoundContainer.test.jsx
@@ -0,0 +1,58 @@
+import '../../util/__mocks__/matchMedia';
+
+import React from 'react';
+
+import { useDispatch } from 'react-redux';
+
+import { render, fireEvent } from '@testing-library/react';
+
+import NotFoundContainer from './NotFoundContainer';
+
+const mockPush = jest.fn();
+
+jest.mock('react-router-dom', () => ({
+ ...jest.requireActual('react-router-dom'),
+ useHistory() {
+ return {
+ push: mockPush,
+ };
+ },
+}));
+
+describe('NotFoundContainer', () => {
+ const dispatch = jest.fn();
+
+ beforeEach(() => {
+ dispatch.mockClear();
+
+ useDispatch.mockImplementation(() => dispatch);
+ });
+
+ const renderNotFoundContainer = () => render((
+
+ ));
+
+ it('should be renders error template contents', () => {
+ const { container } = renderNotFoundContainer({
+ message: '아무것도 없어요!',
+ buttonText: '홈으로',
+ });
+
+ expect(container).toHaveTextContent('아무것도 없어요!');
+ expect(container).toHaveTextContent('홈으로');
+ });
+
+ it('handle Click event', () => {
+ const { getByText } = renderNotFoundContainer({
+ message: '아무것도 없어요!',
+ buttonText: '홈으로',
+ });
+
+ fireEvent.click(getByText('홈으로'));
+
+ expect(mockPush).toBeCalledWith('/');
+ expect(dispatch).toBeCalledWith({
+ type: 'common/resetError',
+ });
+ });
+});
diff --git a/src/containers/introduce/IntroduceFormContainer.test.jsx b/src/containers/introduce/IntroduceFormContainer.test.jsx
index 3fc9647..17db40f 100644
--- a/src/containers/introduce/IntroduceFormContainer.test.jsx
+++ b/src/containers/introduce/IntroduceFormContainer.test.jsx
@@ -1,3 +1,5 @@
+import '../../util/__mocks__/matchMedia';
+
import React from 'react';
import { MemoryRouter } from 'react-router-dom';
diff --git a/src/containers/introduce/IntroduceHeaderContainer.test.jsx b/src/containers/introduce/IntroduceHeaderContainer.test.jsx
index a81dbca..0e424e0 100644
--- a/src/containers/introduce/IntroduceHeaderContainer.test.jsx
+++ b/src/containers/introduce/IntroduceHeaderContainer.test.jsx
@@ -1,3 +1,5 @@
+import '../../util/__mocks__/matchMedia';
+
import React from 'react';
import { MemoryRouter } from 'react-router-dom';
diff --git a/src/containers/introduce/ReviewContainer.test.jsx b/src/containers/introduce/ReviewContainer.test.jsx
index f93ae8e..85344c1 100644
--- a/src/containers/introduce/ReviewContainer.test.jsx
+++ b/src/containers/introduce/ReviewContainer.test.jsx
@@ -1,3 +1,5 @@
+import '../../util/__mocks__/matchMedia';
+
import React from 'react';
import { useSelector, useDispatch } from 'react-redux';
diff --git a/src/containers/write/TagsFormContainer.test.jsx b/src/containers/write/TagsFormContainer.test.jsx
index 7dfe281..f0e5b7d 100644
--- a/src/containers/write/TagsFormContainer.test.jsx
+++ b/src/containers/write/TagsFormContainer.test.jsx
@@ -1,3 +1,5 @@
+import '../../util/__mocks__/matchMedia';
+
import React from 'react';
import { fireEvent, render } from '@testing-library/react';
diff --git a/src/containers/write/WriteButtonsContainer.test.jsx b/src/containers/write/WriteButtonsContainer.test.jsx
index c4791f7..bf0bd3f 100644
--- a/src/containers/write/WriteButtonsContainer.test.jsx
+++ b/src/containers/write/WriteButtonsContainer.test.jsx
@@ -1,3 +1,5 @@
+import '../../util/__mocks__/matchMedia';
+
import React from 'react';
import { useDispatch, useSelector } from 'react-redux';
diff --git a/src/containers/write/WriteEditorContainer.test.jsx b/src/containers/write/WriteEditorContainer.test.jsx
index 2b7a411..d9b1f6b 100644
--- a/src/containers/write/WriteEditorContainer.test.jsx
+++ b/src/containers/write/WriteEditorContainer.test.jsx
@@ -1,3 +1,5 @@
+import '../../util/__mocks__/matchMedia';
+
import React from 'react';
import { useDispatch, useSelector } from 'react-redux';
diff --git a/src/containers/write/WriteFormContainer.test.jsx b/src/containers/write/WriteFormContainer.test.jsx
index 41392ba..c617768 100644
--- a/src/containers/write/WriteFormContainer.test.jsx
+++ b/src/containers/write/WriteFormContainer.test.jsx
@@ -1,3 +1,5 @@
+import '../../util/__mocks__/matchMedia';
+
import React from 'react';
import { useDispatch, useSelector } from 'react-redux';
diff --git a/src/hooks/useNotFound.js b/src/hooks/useNotFound.js
new file mode 100644
index 0000000..6df70fe
--- /dev/null
+++ b/src/hooks/useNotFound.js
@@ -0,0 +1,24 @@
+import { useCallback } from 'react';
+
+import { useDispatch, useSelector } from 'react-redux';
+
+import { getCommon } from '../util/utils';
+import { setNotFound, resetError } from '../reducers/commonSlice';
+
+function useNotFound() {
+ const dispatch = useDispatch();
+
+ const errorType = useSelector(getCommon('errorType'));
+
+ const showNotFound = useCallback(() => dispatch(setNotFound()), [dispatch]);
+
+ const reset = useCallback(() => dispatch(resetError()), [dispatch]);
+
+ return {
+ reset,
+ showNotFound,
+ isNotFound: errorType === 'NOT_FOUND',
+ };
+}
+
+export default useNotFound;
diff --git a/src/pages/IntroducePage.jsx b/src/pages/IntroducePage.jsx
index 2dd823c..67d83e2 100644
--- a/src/pages/IntroducePage.jsx
+++ b/src/pages/IntroducePage.jsx
@@ -6,6 +6,7 @@ import { useParams } from 'react-router-dom';
import { loadStudyGroup } from '../reducers/groupSlice';
import GlobalBlock from '../styles/GlobalBlock';
+import HeaderContainer from '../containers/base/HeaderContainer';
import ReviewContainer from '../containers/introduce/ReviewContainer';
import ThemeToggleContainer from '../containers/base/ThemeToggleContainer';
import IntroduceFormContainer from '../containers/introduce/IntroduceFormContainer';
@@ -21,12 +22,15 @@ const IntroducePage = ({ params }) => {
}, [dispatch, id]);
return (
-
-
-
-
-
-
+ <>
+
+
+
+
+
+
+
+ >
);
};
export default React.memo(IntroducePage);
diff --git a/src/pages/LoginPage.jsx b/src/pages/LoginPage.jsx
index f7c4a54..3e076aa 100644
--- a/src/pages/LoginPage.jsx
+++ b/src/pages/LoginPage.jsx
@@ -4,6 +4,7 @@ import { Helmet } from 'react-helmet';
import { LOGIN } from '../util/constants/constants';
+import HeaderContainer from '../containers/base/HeaderContainer';
import LoginFormContainer from '../containers/auth/LoginFormContainer';
const LoginPage = () => (
@@ -11,6 +12,7 @@ const LoginPage = () => (
{`ConStu | ${LOGIN}`}
+
>
);
diff --git a/src/pages/MainPage.jsx b/src/pages/MainPage.jsx
index 8700f27..8d37281 100644
--- a/src/pages/MainPage.jsx
+++ b/src/pages/MainPage.jsx
@@ -4,6 +4,7 @@ import { Helmet } from 'react-helmet';
import GlobalBlock from '../styles/GlobalBlock';
+import HeaderContainer from '../containers/base/HeaderContainer';
import ThemeToggleContainer from '../containers/base/ThemeToggleContainer';
import StudyGroupsContainer from '../containers/groups/StudyGroupsContainer';
@@ -12,6 +13,7 @@ const MainPage = () => (
ConStu
+
diff --git a/src/pages/NotFoundPage.jsx b/src/pages/NotFoundPage.jsx
new file mode 100644
index 0000000..3ad1c66
--- /dev/null
+++ b/src/pages/NotFoundPage.jsx
@@ -0,0 +1,9 @@
+import React from 'react';
+
+import NotFoundContainer from '../containers/error/NotFoundContainer';
+
+const NotFoundPage = () => (
+
+);
+
+export default NotFoundPage;
diff --git a/src/pages/NotFoundPage.test.jsx b/src/pages/NotFoundPage.test.jsx
new file mode 100644
index 0000000..05f7a1a
--- /dev/null
+++ b/src/pages/NotFoundPage.test.jsx
@@ -0,0 +1,23 @@
+import '../util/__mocks__/matchMedia';
+
+import React from 'react';
+
+import { render } from '@testing-library/react';
+
+import NotFoundPage from './NotFoundPage';
+
+describe('NotFoundPage', () => {
+ const renderNotFoundPage = () => render((
+
+ ));
+
+ describe('Renders NotFound(404) Contents', () => {
+ it('should be renders 404 Image and Message Text', () => {
+ const { container, getByTestId } = renderNotFoundPage();
+
+ expect(container).toHaveTextContent('아무것도 없어요!');
+ expect(container).toHaveTextContent('홈으로');
+ expect(getByTestId('not-found-image')).not.toBeNull();
+ });
+ });
+});
diff --git a/src/pages/RegisterPage.jsx b/src/pages/RegisterPage.jsx
index fb098bb..4a1b6ba 100644
--- a/src/pages/RegisterPage.jsx
+++ b/src/pages/RegisterPage.jsx
@@ -4,6 +4,7 @@ import { Helmet } from 'react-helmet';
import { REGISTER } from '../util/constants/constants';
+import HeaderContainer from '../containers/base/HeaderContainer';
import RegisterFormContainer from '../containers/auth/RegisterFormContainer';
const RegisterPage = () => (
@@ -11,6 +12,7 @@ const RegisterPage = () => (
{`ConStu | ${REGISTER}`}
+
>
);
diff --git a/src/pages/RegisterPage.test.jsx b/src/pages/RegisterPage.test.jsx
index 5bdea16..57fdfe9 100644
--- a/src/pages/RegisterPage.test.jsx
+++ b/src/pages/RegisterPage.test.jsx
@@ -4,6 +4,7 @@ import { useDispatch, useSelector } from 'react-redux';
import { render } from '@testing-library/react';
+import { MemoryRouter } from 'react-router-dom';
import RegisterPage from './RegisterPage';
import MockTheme from '../components/common/test/MockTheme';
@@ -28,7 +29,9 @@ describe('RegisterPage', () => {
const renderRegisterPage = () => render((
-
+
+
+
));
diff --git a/src/pages/WritePage.jsx b/src/pages/WritePage.jsx
index daf9a08..7e2d215 100644
--- a/src/pages/WritePage.jsx
+++ b/src/pages/WritePage.jsx
@@ -4,6 +4,7 @@ import { Helmet } from 'react-helmet';
import GlobalBlock from '../styles/GlobalBlock';
+import HeaderContainer from '../containers/base/HeaderContainer';
import TagFormContainer from '../containers/write/TagsFormContainer';
import WriteFormContainer from '../containers/write/WriteFormContainer';
import ThemeToggleContainer from '../containers/base/ThemeToggleContainer';
@@ -15,6 +16,7 @@ const WritePage = () => (
ConStu | 스터디 개설하기
+
diff --git a/src/pages/WritePage.test.jsx b/src/pages/WritePage.test.jsx
index 9175d00..4214012 100644
--- a/src/pages/WritePage.test.jsx
+++ b/src/pages/WritePage.test.jsx
@@ -6,6 +6,7 @@ import { useDispatch, useSelector } from 'react-redux';
import { render } from '@testing-library/react';
+import { MemoryRouter } from 'react-router-dom';
import WritePage from './WritePage';
import MockTheme from '../components/common/test/MockTheme';
@@ -34,7 +35,9 @@ describe('WritePage', () => {
const renderWritePage = () => render((
-
+
+
+
));
diff --git a/src/reducers/commonSlice.js b/src/reducers/commonSlice.js
index 621f2fd..7b33305 100644
--- a/src/reducers/commonSlice.js
+++ b/src/reducers/commonSlice.js
@@ -7,6 +7,7 @@ const { actions, reducer } = createSlice({
name: 'common',
initialState: {
theme: getInitTheme(),
+ errorType: null,
},
reducers: {
@@ -18,11 +19,25 @@ const { actions, reducer } = createSlice({
theme: !state.theme,
};
},
+ setNotFound(state) {
+ return {
+ ...state,
+ errorType: 'NOT_FOUND',
+ };
+ },
+ resetError(state) {
+ return {
+ ...state,
+ errorType: null,
+ };
+ },
},
});
export const {
changeTheme,
+ setNotFound,
+ resetError,
} = actions;
export default reducer;
diff --git a/src/reducers/commonSlice.test.js b/src/reducers/commonSlice.test.js
index 62e8251..0e0e938 100644
--- a/src/reducers/commonSlice.test.js
+++ b/src/reducers/commonSlice.test.js
@@ -1,13 +1,13 @@
-import '../util/__mocks__/matchMedia';
-
import reducer, {
- changeTheme,
+ changeTheme, setNotFound, resetError,
} from './commonSlice';
+jest.mock('../util/utils');
describe('reducer', () => {
context('when previous state is undefined', () => {
const initialState = {
- theme: false,
+ theme: undefined,
+ errorType: null,
};
it('returns initialState', () => {
@@ -24,4 +24,20 @@ describe('reducer', () => {
expect(state.theme).toBe(true);
});
});
+
+ describe('setNotFound', () => {
+ it('should be set "NOT_FOUND" error type', () => {
+ const state = reducer({ errorType: null }, setNotFound());
+
+ expect(state.errorType).toBe('NOT_FOUND');
+ });
+ });
+
+ describe('resetError', () => {
+ it('should be reset error type', () => {
+ const state = reducer({ errorType: 'NOT_FOUND' }, resetError());
+
+ expect(state.errorType).toBeNull();
+ });
+ });
});
diff --git a/src/reducers/groupSlice.js b/src/reducers/groupSlice.js
index b0831bd..a10ce31 100644
--- a/src/reducers/groupSlice.js
+++ b/src/reducers/groupSlice.js
@@ -15,6 +15,8 @@ import {
deletePostReview,
} from '../services/api';
+import { setNotFound } from './commonSlice';
+
import { formatGroup } from '../util/utils';
const writeInitialState = {
@@ -138,9 +140,17 @@ export const loadStudyGroups = (tag) => async (dispatch) => {
export const loadStudyGroup = (id) => async (dispatch) => {
dispatch(setStudyGroup(null));
- const response = await getStudyGroup(id);
+ try {
+ const response = await getStudyGroup(id);
+
+ if (!response) {
+ dispatch(setNotFound());
+ }
- dispatch(setStudyGroup(formatGroup(response)));
+ dispatch(setStudyGroup(formatGroup(response)));
+ } catch (error) {
+ dispatch(setGroupError(error.code));
+ }
};
export const writeStudyGroup = () => async (dispatch, getState) => {
diff --git a/src/reducers/groupSlice.test.js b/src/reducers/groupSlice.test.js
index 7d55e79..54525e2 100644
--- a/src/reducers/groupSlice.test.js
+++ b/src/reducers/groupSlice.test.js
@@ -1,4 +1,4 @@
-// eslint-disable-next-line import/no-extraneous-dependencies
+/* eslint-disable import/no-extraneous-dependencies */
import thunk from 'redux-thunk';
import configureStore from 'redux-mock-store';
@@ -268,27 +268,64 @@ describe('async actions', () => {
applyEndDate: new Date(),
reviews: [],
};
+ const data = jest.fn();
beforeEach(() => {
- const data = jest.fn().mockReturnValueOnce(settings);
+ data.mockReturnValueOnce(settings);
store = mockStore({});
- getStudyGroup.mockReturnValueOnce({
- data,
- id: '1',
+ });
+
+ context('without getStudyGroup error', () => {
+ context('without response', () => {
+ getStudyGroup.mockReturnValueOnce({
+ data,
+ id: '1',
+ });
+
+ it('dispatch calls setNotFound event', async () => {
+ await store.dispatch(loadStudyGroup(1));
+
+ const actions = store.getActions();
+
+ expect(actions[0]).toEqual(setStudyGroup(null));
+ expect(actions[1]).toEqual(setStudyGroup({
+ ...settings,
+ id: '1',
+ }));
+ });
+ });
+
+ context('with response', () => {
+ getStudyGroup.mockReturnValueOnce(null);
+
+ it('load study group detail', async () => {
+ await store.dispatch(loadStudyGroup(1));
+
+ const actions = store.getActions();
+
+ expect(actions[0]).toEqual(setStudyGroup(null));
+ expect(actions[1]).toEqual({
+ type: 'common/setNotFound',
+ });
+ });
});
});
- it('load study group detail', async () => {
- await store.dispatch(loadStudyGroup(1));
+ context('with getStudyGroup error', () => {
+ getStudyGroup.mockImplementationOnce(() => {
+ throw new Error('error');
+ });
- const actions = store.getActions();
+ it('dispatches loadStudyGroup action failure to return error', async () => {
+ try {
+ await store.dispatch(loadStudyGroup(1));
+ } catch (error) {
+ const actions = store.getActions();
- expect(actions[0]).toEqual(setStudyGroup(null));
- expect(actions[1]).toEqual(setStudyGroup({
- ...settings,
- id: '1',
- }));
+ expect(actions[0]).toEqual(setGroupError(error));
+ }
+ });
});
});
diff --git a/src/util/__mocks__/utils.js b/src/util/__mocks__/utils.js
index bc859d8..38db53d 100644
--- a/src/util/__mocks__/utils.js
+++ b/src/util/__mocks__/utils.js
@@ -18,3 +18,5 @@ export const formatGroup = (group) => {
reviews: reviews && [...formatReviewDate(reviews)],
};
};
+
+export const getInitTheme = jest.fn();
diff --git a/tests/error/error_test.js b/tests/error/error_test.js
new file mode 100644
index 0000000..961fb41
--- /dev/null
+++ b/tests/error/error_test.js
@@ -0,0 +1,30 @@
+Feature('사용자는 에러 상태에 대해서 경험할 수 있다.');
+
+const step = codeceptjs.container.plugins('commentStep');
+
+const Given = (given) => step`${given}`;
+const When = (when) => step`${when}`;
+const Then = (then) => step`${then}`;
+
+Scenario('존재하지 않는 페이지로 이동한 경우', ({ I }) => {
+ Given('메인 페이지에서');
+ I.amOnPage('/');
+
+ When('존재하지 않는 페이지로 이동하면');
+ I.amOnPage('/not-found');
+
+ Then('404 페이지로 이동한다');
+ I.see('아무것도 없어요!');
+ I.see('홈으로');
+});
+
+Scenario('존재하지 않는 페이지에서 "홈으로" 버튼을 클릭한 경우', ({ I }) => {
+ Given('존재하지 않는 페이지에서');
+ I.amOnPage('/not-found');
+
+ When('"홈으로" 버튼을 클릭하면');
+ I.click('홈으로');
+
+ Then('메인 페이지로 이동한다.');
+ I.seeCurrentUrlEquals('/');
+});