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('/'); +});