diff --git a/frontend/.storybook/preview.tsx b/frontend/.storybook/preview.tsx index 61f9e69fc..d05d7ab32 100644 --- a/frontend/.storybook/preview.tsx +++ b/frontend/.storybook/preview.tsx @@ -9,6 +9,7 @@ import { Theme } from '../src/styles/Theme'; import { initialize, mswLoader } from 'msw-storybook-addon'; import { handlers } from '../src/mocks/handlers'; import ToastProvider from '../src/providers/ToastProvider/ToastProvider'; +import ModalProvider from '../src/providers/ModalProvider/ModalProvider'; initialize({ serviceWorker: { @@ -39,7 +40,9 @@ const preview: Preview = { - + + + diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx deleted file mode 100644 index 0cb7a9ae6..000000000 --- a/frontend/src/App.tsx +++ /dev/null @@ -1,9 +0,0 @@ -import { RouterProvider } from 'react-router-dom'; - -import { router } from './router'; - -const App = () => { - return ; -}; - -export default App; diff --git a/frontend/src/components/GameResult/GameResult.hook.ts b/frontend/src/components/GameResult/GameResult.hook.ts index b717fb571..6f77e4cef 100644 --- a/frontend/src/components/GameResult/GameResult.hook.ts +++ b/frontend/src/components/GameResult/GameResult.hook.ts @@ -2,11 +2,15 @@ import { useMutation, useQuery, UseQueryResult } from '@tanstack/react-query'; import { useParams } from 'react-router-dom'; import { useRecoilValue } from 'recoil'; +import AlertModal from '../common/AlertModal/AlertModal'; + import { fetchMatchingResult } from '@/apis/balanceContent'; import { resetRoom } from '@/apis/room'; import { QUERY_KEYS } from '@/constants/queryKeys'; +import useModal from '@/hooks/useModal'; import { memberInfoState } from '@/recoil/atom'; import { MatchingResult, MemberMatchingInfo } from '@/types/balanceContent'; +import { CustomError } from '@/utils/error'; type MatchingResultQueryResponse = UseQueryResult & { matchedMembers?: MemberMatchingInfo[]; @@ -34,11 +38,13 @@ export const useMatchingResultQuery = (): MatchingResultQueryResponse => { }; }; -export const useResetRoomMutation = (roomId: number, showModal: () => void) => { +export const useResetRoomMutation = (roomId: number) => { + const { show: showModal } = useModal(); + return useMutation({ mutationFn: async () => await resetRoom(roomId), - onError: () => { - showModal(); + onError: (error: CustomError) => { + showModal(AlertModal, { title: '방 초기화 에러', message: error.message }); }, }); }; diff --git a/frontend/src/components/GameResult/GameResult.tsx b/frontend/src/components/GameResult/GameResult.tsx index ff5a77d00..9feb629b1 100644 --- a/frontend/src/components/GameResult/GameResult.tsx +++ b/frontend/src/components/GameResult/GameResult.tsx @@ -7,16 +7,13 @@ import { noMatchingImg, noMatchingText, } from './GameResult.styled'; -import AlertModal from '../common/AlertModal/AlertModal'; import FinalButton from '../common/FinalButton/FinalButton'; import Spinner from '../common/Spinner/Spinner'; import GameResultItem from '../GameResultItem/GameResultItem'; import SadDdangKong from '@/assets/images/sadDdangkong.png'; -import useModal from '@/hooks/useModal'; const GameResult = () => { - const { isOpen, show, close } = useModal(); const { matchedMembers, existMatching, isLoading } = useMatchingResultQuery(); return ( @@ -47,13 +44,7 @@ const GameResult = () => { )} - - + ); }; diff --git a/frontend/src/components/ReadyMembersContainer/ReadyMembersContainer.test.tsx b/frontend/src/components/ReadyMembersContainer/ReadyMembersContainer.test.tsx new file mode 100644 index 000000000..6d911f8c1 --- /dev/null +++ b/frontend/src/components/ReadyMembersContainer/ReadyMembersContainer.test.tsx @@ -0,0 +1,21 @@ +import { screen, waitFor } from '@testing-library/react'; +import { userEvent } from '@testing-library/user-event'; + +import ReadyMembersContainer from './ReadyMembersContainer'; + +import { customRender } from '@/utils/test-utils'; + +describe('ReadyMembersContainer 테스트', () => { + it('초대하기 버튼을 클릭했을 때, 초대 모달이 뜬다.', async () => { + const user = userEvent.setup(); + customRender(); + + const inviteButton = await screen.findByText('초대하기'); + await user.click(inviteButton); + + await waitFor(() => { + const copyText = screen.getByText('초대 링크 복사'); + expect(copyText).toBeInTheDocument(); + }); + }); +}); diff --git a/frontend/src/components/ReadyMembersContainer/ReadyMembersContainer.tsx b/frontend/src/components/ReadyMembersContainer/ReadyMembersContainer.tsx index fcb2a5a68..ea173b999 100644 --- a/frontend/src/components/ReadyMembersContainer/ReadyMembersContainer.tsx +++ b/frontend/src/components/ReadyMembersContainer/ReadyMembersContainer.tsx @@ -23,9 +23,13 @@ import { memberInfoState } from '@/recoil/atom'; const ReadyMembersContainer = () => { const { members, master } = useGetRoomInfo(); - const { isOpen, show, close } = useModal(); + const { show } = useModal(); const [memberInfo, setMemberInfo] = useRecoilState(memberInfoState); + const handleClickInvite = () => { + show(InviteModal); + }; + // 원래 방장이 아니다 + 방장의 memberId와 내 memberId가 같다 -> 방장으로 변경 useEffect(() => { if (!memberInfo.isMaster && master.memberId === memberInfo.memberId) { @@ -39,7 +43,7 @@ const ReadyMembersContainer = () => {
  • -
- ); }; diff --git a/frontend/src/components/SelectContainer/SelectContainer.test.tsx b/frontend/src/components/SelectContainer/SelectContainer.test.tsx index 074f6616e..accacd872 100644 --- a/frontend/src/components/SelectContainer/SelectContainer.test.tsx +++ b/frontend/src/components/SelectContainer/SelectContainer.test.tsx @@ -4,6 +4,7 @@ import { http, HttpResponse } from 'msw'; import SelectContainer from './SelectContainer'; +import { ERROR_MESSAGE } from '@/constants/message'; import { MOCK_API_URL } from '@/constants/url'; import BALANCE_CONTENT from '@/mocks/data/balanceContent.json'; import { server } from '@/mocks/server'; @@ -15,7 +16,10 @@ describe('SelectContainer', () => { const user = userEvent.setup(); server.use( http.post(MOCK_API_URL.vote, () => { - return new HttpResponse(JSON.stringify({ errorCode: '', message: '' }), { status: 400 }); + return new HttpResponse( + JSON.stringify({ errorCode: 'ALREADY_VOTED', message: ERROR_MESSAGE.ALREADY_VOTED }), + { status: 400 }, + ); }), ); diff --git a/frontend/src/components/SelectContainer/SelectContainer.tsx b/frontend/src/components/SelectContainer/SelectContainer.tsx index 7a1352a7a..ff0a9bdc3 100644 --- a/frontend/src/components/SelectContainer/SelectContainer.tsx +++ b/frontend/src/components/SelectContainer/SelectContainer.tsx @@ -3,18 +3,15 @@ import { useParams } from 'react-router-dom'; import useSelectOption from './hooks/useSelectOption'; import { selectContainerLayout, selectSection } from './SelectContainer.styled'; import Timer from './Timer/Timer'; -import AlertModal from '../common/AlertModal/AlertModal'; import SelectButton from '../common/SelectButton/SelectButton'; import SelectOption from '@/components/SelectOption/SelectOption'; import useBalanceContentQuery from '@/hooks/useBalanceContentQuery'; -import useModal from '@/hooks/useModal'; const SelectContainer = () => { const { roomId } = useParams(); const { balanceContent } = useBalanceContentQuery(Number(roomId)); const { selectedOption, handleClickOption, completeSelection } = useSelectOption(); - const { isOpen, show, close } = useModal(); return (
@@ -22,7 +19,6 @@ const SelectContainer = () => { selectedId={selectedOption.id} isVoted={selectedOption.isCompleted} completeSelection={completeSelection} - showModal={show} />
{ contentId={balanceContent.contentId} selectedId={selectedOption.id} completeSelection={completeSelection} - showModal={show} - /> -
); diff --git a/frontend/src/components/SelectContainer/Timer/Timer.tsx b/frontend/src/components/SelectContainer/Timer/Timer.tsx index 91e3d94d1..29bb88ac9 100644 --- a/frontend/src/components/SelectContainer/Timer/Timer.tsx +++ b/frontend/src/components/SelectContainer/Timer/Timer.tsx @@ -19,10 +19,9 @@ interface TimerProps { selectedId: number; isVoted: boolean; completeSelection: () => void; - showModal: () => void; } -const Timer = ({ selectedId, isVoted, completeSelection, showModal }: TimerProps) => { +const Timer = ({ selectedId, isVoted, completeSelection }: TimerProps) => { const { roomId } = useParams(); const { balanceContent, isFetching } = useBalanceContentQuery(Number(roomId)); const { barWidthPercent, leftRoundTime, isAlmostFinished } = useVoteTimer({ @@ -30,7 +29,6 @@ const Timer = ({ selectedId, isVoted, completeSelection, showModal }: TimerProps selectedId, isVoted, completeSelection, - showModal, }); useVoteIsFinished({ diff --git a/frontend/src/components/SelectContainer/Timer/hooks/useVoteTimer.ts b/frontend/src/components/SelectContainer/Timer/hooks/useVoteTimer.ts index 96c94fbcc..71bf760a7 100644 --- a/frontend/src/components/SelectContainer/Timer/hooks/useVoteTimer.ts +++ b/frontend/src/components/SelectContainer/Timer/hooks/useVoteTimer.ts @@ -10,16 +10,9 @@ interface UseVoteTimerProps { selectedId: number; isVoted: boolean; completeSelection: () => void; - showModal: () => void; } -const useVoteTimer = ({ - roomId, - selectedId, - isVoted, - completeSelection, - showModal, -}: UseVoteTimerProps) => { +const useVoteTimer = ({ roomId, selectedId, isVoted, completeSelection }: UseVoteTimerProps) => { const { balanceContent } = useBalanceContentQuery(roomId); const timeLimit = balanceContent.timeLimit || DEFAULT_TIME_LIMIT_MSEC; @@ -27,7 +20,6 @@ const useVoteTimer = ({ selectedId, contentId: balanceContent.contentId, completeSelection, - showModal, }); const { leftRoundTime, barWidthPercent, isAlmostFinished } = useTimer({ diff --git a/frontend/src/components/StartButtonContainer/StartButton/StartButton.test.tsx b/frontend/src/components/StartButtonContainer/StartButton/StartButton.test.tsx new file mode 100644 index 000000000..723ca81c5 --- /dev/null +++ b/frontend/src/components/StartButtonContainer/StartButton/StartButton.test.tsx @@ -0,0 +1,37 @@ +import { screen, waitFor } from '@testing-library/react'; +import { userEvent } from '@testing-library/user-event'; +import { http, HttpResponse } from 'msw'; + +import StartButton from './StartButton'; + +import { ERROR_MESSAGE } from '@/constants/message'; +import { MOCK_API_URL } from '@/constants/url'; +import { server } from '@/mocks/server'; +import { customRenderWithIsMaster } from '@/utils/test-utils'; + +describe('StartButton 테스트', () => { + it('시작 버튼을 클릭했을 때, 게임 시작 API에서 에러가 발생하면 알림 모달이 뜬다.', async () => { + const user = userEvent.setup(); + server.use( + http.patch(MOCK_API_URL.startGame, async () => { + return HttpResponse.json( + { + errorCode: 'NOT_READY_ROOM', + message: ERROR_MESSAGE.NOT_READY_ROOM, + }, + { status: 400 }, + ); + }), + ); + + customRenderWithIsMaster(, true); + + const startButton = await screen.findByRole('button', { name: '시작' }); + await user.click(startButton); + + await waitFor(() => { + const closeIcon = screen.getByAltText('닫기 버튼'); + expect(closeIcon).toBeInTheDocument(); + }); + }); +}); diff --git a/frontend/src/components/StartButtonContainer/StartButton/StartButton.tsx b/frontend/src/components/StartButtonContainer/StartButton/StartButton.tsx index 958a07a40..0311cb213 100644 --- a/frontend/src/components/StartButtonContainer/StartButton/StartButton.tsx +++ b/frontend/src/components/StartButtonContainer/StartButton/StartButton.tsx @@ -2,12 +2,8 @@ import { useGameStart } from './hooks/useGameStart'; import Button from '@/components/common/Button/Button'; -interface StartButtonProps { - show: () => void; -} - -const StartButton = ({ show }: StartButtonProps) => { - const { memberInfo, handleGameStart } = useGameStart({ showModal: show }); +const StartButton = () => { + const { memberInfo, handleGameStart } = useGameStart(); return (

{title}

{memberInfo.isMaster ? ( - ) : ( )} - {isOpen && } ); }; diff --git a/frontend/src/hooks/useModal.ts b/frontend/src/hooks/useModal.ts index 0529c40d4..24228ec7b 100644 --- a/frontend/src/hooks/useModal.ts +++ b/frontend/src/hooks/useModal.ts @@ -1,16 +1,13 @@ -import { useState } from 'react'; +import { useContext } from 'react'; -const useModal = () => { - const [isOpen, setIsOpen] = useState(false); - - const show = () => { - setIsOpen(true); - }; +import { ModalDispatchContext } from '@/providers/ModalProvider/ModalProvider'; - const close = () => { - setIsOpen(false); - }; +const useModal = () => { + const dispatch = useContext(ModalDispatchContext); - return { isOpen, show, close }; + if (dispatch === null) { + throw new Error('ModalDispatchContext가 존재하지 않습니다.'); + } + return dispatch; }; export default useModal; diff --git a/frontend/src/index.tsx b/frontend/src/index.tsx index af3f59a00..7ba7c7146 100644 --- a/frontend/src/index.tsx +++ b/frontend/src/index.tsx @@ -3,10 +3,11 @@ import * as Sentry from '@sentry/react'; import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; import { ReactQueryDevtools } from '@tanstack/react-query-devtools'; import ReactDOM from 'react-dom/client'; +import { RouterProvider } from 'react-router-dom'; import { RecoilRoot } from 'recoil'; -import App from './App'; import ToastProvider from './providers/ToastProvider/ToastProvider'; +import { router } from './router'; import GlobalStyle from './styles/GlobalStyle'; import { Theme } from './styles/Theme'; @@ -35,7 +36,7 @@ enableMocking().then(() => { - + diff --git a/frontend/src/pages/NicknamePage/NicknamePage.tsx b/frontend/src/pages/NicknamePage/NicknamePage.tsx index 98fe45c7d..770a5e02a 100644 --- a/frontend/src/pages/NicknamePage/NicknamePage.tsx +++ b/frontend/src/pages/NicknamePage/NicknamePage.tsx @@ -1,7 +1,7 @@ import { useQuery } from '@tanstack/react-query'; import { useEffect } from 'react'; import { useParams } from 'react-router-dom'; -import { useRecoilValue, useSetRecoilState } from 'recoil'; +import { useSetRecoilState } from 'recoil'; import NicknameInput from './NicknameInput/NicknameInput'; import { @@ -18,16 +18,12 @@ import useMakeOrEnterRoom from './useMakeOrEnterRoom'; import { isJoinableRoom } from '@/apis/room'; import AngryDdangkong from '@/assets/images/angryDdangkong.png'; import SillyDdangkong from '@/assets/images/sillyDdangkong.png'; -import AlertModal from '@/components/common/AlertModal/AlertModal'; import Button from '@/components/common/Button/Button'; import Content from '@/components/layout/Content/Content'; -import useModal from '@/hooks/useModal'; -import { memberInfoState, roomUuidState } from '@/recoil/atom'; +import { roomUuidState } from '@/recoil/atom'; const NicknamePage = () => { - const { isOpen, show, close } = useModal(); - const { nicknameInputRef, handleMakeOrEnterRoom, isLoading } = useMakeOrEnterRoom(show); - const { isMaster } = useRecoilValue(memberInfoState); + const { nicknameInputRef, handleMakeOrEnterRoom, isLoading } = useMakeOrEnterRoom(); const { roomUuid } = useParams(); const setRoomUuidState = useSetRecoilState(roomUuidState); @@ -69,12 +65,6 @@ const NicknamePage = () => { text={isLoading ? '접속 중...' : '확인'} bottom /> - ); }; diff --git a/frontend/src/pages/NicknamePage/useMakeOrEnterRoom.ts b/frontend/src/pages/NicknamePage/useMakeOrEnterRoom.ts index a6bda8bb2..b0f05034d 100644 --- a/frontend/src/pages/NicknamePage/useMakeOrEnterRoom.ts +++ b/frontend/src/pages/NicknamePage/useMakeOrEnterRoom.ts @@ -4,19 +4,23 @@ import { useNavigate, useParams } from 'react-router-dom'; import { useRecoilState } from 'recoil'; import { enterRoom, createRoom } from '@/apis/room'; +import AlertModal from '@/components/common/AlertModal/AlertModal'; import { ROUTES } from '@/constants/routes'; +import useModal from '@/hooks/useModal'; import { memberInfoState, roomUuidState } from '@/recoil/atom'; import { CreateOrEnterRoomResponse } from '@/types/room'; +import { CustomError } from '@/utils/error'; -const useMakeOrEnterRoom = (showModal: () => void) => { +const useMakeOrEnterRoom = () => { const nicknameInputRef = useRef(null); const navigate = useNavigate(); const [{ isMaster }, setMemberInfo] = useRecoilState(memberInfoState); const [, setRoomUuidState] = useRecoilState(roomUuidState); const { roomUuid } = useParams(); + const { show: showModal } = useModal(); - const createRoomMutation = useMutation({ + const createRoomMutation = useMutation({ mutationFn: createRoom, onSuccess: (data) => { setMemberInfo((prev) => ({ @@ -26,14 +30,14 @@ const useMakeOrEnterRoom = (showModal: () => void) => { setRoomUuidState(data.roomUuid || ''); navigate(ROUTES.ready(Number(data.roomId)), { replace: true }); }, - onError: () => { - showModal(); + onError: (error) => { + showModal(AlertModal, { title: '방 생성 에러', message: error.message }); }, }); const enterRoomMutation = useMutation< CreateOrEnterRoomResponse, - Error, + CustomError, { nickname: string; roomUuid: string } >({ mutationFn: ({ nickname, roomUuid }) => enterRoom(roomUuid, nickname), @@ -42,8 +46,8 @@ const useMakeOrEnterRoom = (showModal: () => void) => { setRoomUuidState(data.roomUuid || ''); navigate(ROUTES.ready(Number(data.roomId)), { replace: true }); }, - onError: () => { - showModal(); + onError: (error: CustomError) => { + showModal(AlertModal, { title: '방 참여 에러', message: error.message }); }, }); diff --git a/frontend/src/pages/RoundResultPage/RoundResultPage.tsx b/frontend/src/pages/RoundResultPage/RoundResultPage.tsx index 1d4719593..eef185169 100644 --- a/frontend/src/pages/RoundResultPage/RoundResultPage.tsx +++ b/frontend/src/pages/RoundResultPage/RoundResultPage.tsx @@ -1,32 +1,14 @@ -import { useParams } from 'react-router-dom'; - -import createRandomNextRoundMessage from './createRandomNextRoundMessage'; - -import InfoModal from '@/components/common/InfoModal/InfoModal'; import NextRoundButton from '@/components/common/NextRoundButton/NextRoundButton'; -import useMoveNextRoundMutation from '@/components/common/NextRoundButton/NextRoundButton.hook'; import Content from '@/components/layout/Content/Content'; import RoundVoteContainer from '@/components/RoundVoteContainer/RoundVoteContainer'; import TopicContainer from '@/components/TopicContainer/TopicContainer'; -import useModal from '@/hooks/useModal'; const RoundResultPage = () => { - const { roomId } = useParams(); - const { isOpen, show, close } = useModal(); - const { mutate: moveNextRound } = useMoveNextRoundMutation(Number(roomId)); - const randomRoundNextMessage = createRandomNextRoundMessage(); - return ( - - + ); }; diff --git a/frontend/src/providers/ModalProvider/ModalProvider.tsx b/frontend/src/providers/ModalProvider/ModalProvider.tsx new file mode 100644 index 000000000..38dcbf19e --- /dev/null +++ b/frontend/src/providers/ModalProvider/ModalProvider.tsx @@ -0,0 +1,63 @@ +import { createContext, PropsWithChildren, useMemo, useState } from 'react'; + +interface ModalProps { + title?: string; + message?: string; + onConfirm?: () => void; +} + +interface ModalState extends ModalProps { + isOpen: boolean; + onClose: () => void; +} + +interface Modal extends ModalProps { + Component: React.FC | null; + isOpen: boolean; +} + +interface ModalDispatchContextProps { + show: (Component: React.FC | null, props?: ModalProps) => void; + close: () => void; +} + +export const ModalDispatchContext = createContext(null); + +const ModalProvider = ({ children }: PropsWithChildren) => { + const [modal, setModal] = useState({ + Component: null, + isOpen: false, + title: '', + message: '', + onConfirm: () => {}, + }); + + const show = (Component: React.FC | null, props?: ModalProps) => { + setModal({ + Component, + title: props?.title, + message: props?.message, + onConfirm: props?.onConfirm, + isOpen: true, + }); + }; + + const close = () => { + setModal((prev) => ({ + ...prev, + Component: null, + isOpen: false, + })); + }; + + const dispatch = useMemo(() => ({ show, close }), []); + + return ( + + {children} + {modal.isOpen && modal.Component && } + + ); +}; + +export default ModalProvider; diff --git a/frontend/src/router/HeaderLayout.tsx b/frontend/src/router/HeaderLayout.tsx new file mode 100644 index 000000000..84066738c --- /dev/null +++ b/frontend/src/router/HeaderLayout.tsx @@ -0,0 +1,14 @@ +import { Outlet } from 'react-router-dom'; + +import Header from '@/components/layout/Header/Header'; + +const HeaderLayout = () => { + return ( + <> +
+ + + ); +}; + +export default HeaderLayout; diff --git a/frontend/src/router/layout.tsx b/frontend/src/router/MainLayout.tsx similarity index 51% rename from frontend/src/router/layout.tsx rename to frontend/src/router/MainLayout.tsx index 7cc3160ad..47b51ce90 100644 --- a/frontend/src/router/layout.tsx +++ b/frontend/src/router/MainLayout.tsx @@ -1,13 +1,16 @@ import { Outlet } from 'react-router-dom'; import RootErrorBoundary from '@/components/common/ErrorBoundary/RootErrorBoundary'; -import Header from '@/components/layout/Header/Header'; +import ModalProvider from '@/providers/ModalProvider/ModalProvider'; -export const Layout = () => { +const MainLayout = () => { return ( -
- + + + ); }; + +export default MainLayout; diff --git a/frontend/src/router/index.tsx b/frontend/src/router/index.tsx index 2f0ffa453..0f7c8a8eb 100644 --- a/frontend/src/router/index.tsx +++ b/frontend/src/router/index.tsx @@ -1,6 +1,7 @@ import { createBrowserRouter } from 'react-router-dom'; -import { Layout } from './layout'; +import HeaderLayout from './HeaderLayout'; +import MainLayout from './MainLayout'; import RouterErrorFallback from '@/components/common/ErrorFallback/RouterErrorFallback/RouterErrorFallback'; import GamePage from '@/pages/GamePage/GamePage'; @@ -14,38 +15,43 @@ import VoteStatusPage from '@/pages/VoteStatusPage/VoteStatusPage'; export const router = createBrowserRouter([ { path: '/', - element: , + element: , errorElement: , - }, - { - path: ':roomId/game', - element: , - }, - { - path: '/', - element: , children: [ { - path: 'nickname/:roomUuid?', - element: , + path: '/', + element: , }, { - path: ':roomId/ready', - element: , + path: ':roomId/game', + element: , }, { - path: ':roomId/round/result', - element: , - }, - { - path: ':roomId/round/result/status', - element: , - }, - { - path: ':roomId/game/result', - element: , + path: '/', + element: , + children: [ + { + path: 'nickname/:roomUuid?', + element: , + }, + { + path: ':roomId/ready', + element: , + }, + { + path: ':roomId/round/result', + element: , + }, + { + path: ':roomId/round/result/status', + element: , + }, + { + path: ':roomId/game/result', + element: , + }, + ], }, ], - errorElement: , }, ]); diff --git a/frontend/src/utils/test-utils.tsx b/frontend/src/utils/test-utils.tsx index 394b660a1..9fda43f21 100644 --- a/frontend/src/utils/test-utils.tsx +++ b/frontend/src/utils/test-utils.tsx @@ -10,7 +10,9 @@ import type { MutableSnapshot } from 'recoil'; import AsyncErrorBoundary from '@/components/common/ErrorBoundary/AsyncErrorBoundary'; import RootErrorBoundary from '@/components/common/ErrorBoundary/RootErrorBoundary'; import Spinner from '@/components/common/Spinner/Spinner'; +import ModalProvider from '@/providers/ModalProvider/ModalProvider'; import ToastProvider from '@/providers/ToastProvider/ToastProvider'; +import { memberInfoState } from '@/recoil/atom'; import GlobalStyle from '@/styles/GlobalStyle'; import { Theme } from '@/styles/Theme'; @@ -34,14 +36,16 @@ const wrapper = ({ - - - - - {children} - - - + + + + + + {children} + + + + @@ -62,4 +66,12 @@ const customRender = (ui: React.ReactNode, options: CustomRenderOptions = {}) => }); }; -export { wrapper, customRender }; +const customRenderWithIsMaster = (Component: React.ReactNode, isMaster: boolean) => { + const initializeState = (snap: MutableSnapshot) => { + snap.set(memberInfoState, { memberId: 1, nickname: 'Test User', isMaster }); + }; + + customRender(Component, { initializeState }); +}; + +export { wrapper, customRender, customRenderWithIsMaster };