diff --git a/client/package.json b/client/package.json index e4df199c..aab5201d 100644 --- a/client/package.json +++ b/client/package.json @@ -60,6 +60,7 @@ }, "devDependencies": { "@babel/plugin-transform-private-property-in-object": "^7.22.11", + "@testing-library/react-hooks": "^8.0.1", "eslint": "^8.47.0", "eslint-config-prettier": "^9.0.0", "eslint-plugin-prettier": "^5.0.0", diff --git a/client/src/hooks/useCountdownTimer.js b/client/src/hooks/useCountdownTimer.js new file mode 100644 index 00000000..eabc91b4 --- /dev/null +++ b/client/src/hooks/useCountdownTimer.js @@ -0,0 +1,30 @@ +import {useEffect} from 'react'; + +const useCountdownTimer = ( + isSuccess, + navigate, + countdown, + setCountdown, + route = '/welcome', +) => { + useEffect(() => { + let timer = null; + if (isSuccess) { + timer = setInterval(() => { + if (countdown > 1) { + setCountdown((prevCount) => prevCount - 1); + } else { + clearInterval(timer); + navigate(route); + } + }, 1000); + } + return () => { + clearInterval(timer); + }; + }, [isSuccess, countdown, navigate]); + + return countdown; +}; + +export default useCountdownTimer; diff --git a/client/src/hooks/useCountdownTimer.test.js b/client/src/hooks/useCountdownTimer.test.js new file mode 100644 index 00000000..bde901f1 --- /dev/null +++ b/client/src/hooks/useCountdownTimer.test.js @@ -0,0 +1,66 @@ +import {renderHook, act} from '@testing-library/react-hooks'; +import useCountdownTimer from './useCountdownTimer'; + +jest.useFakeTimers(); + +describe('useCountdownTimer', () => { + afterEach(() => { + jest.clearAllTimers(); + jest.useRealTimers(); + }); + beforeEach(() => { + jest.useFakeTimers(); + }); + it('should navigate to "/welcome" when countdown reaches 0', () => { + const navigateMock = jest.fn(); + let countdown = 3; + const setCountdownMock = jest.fn().mockImplementation((value) => { + countdown = value; + }); + const {result} = renderHook(() => + useCountdownTimer(true, navigateMock, countdown, setCountdownMock), + ); + act(() => { + jest.advanceTimersByTime(3000); + }); + setTimeout(() => { + expect(result.current).toBe(0); + expect(navigateMock).toHaveBeenCalledWith('/welcome'); + }, 3000); + }); + + it('should not navigate when isSuccess is false', () => { + const navigateMock = jest.fn(); + let countdown = 3; + const setCountdownMock = jest.fn().mockImplementation((value) => { + countdown = value; + }); + const {result} = renderHook(() => + useCountdownTimer(false, navigateMock, countdown, setCountdownMock), + ); + act(() => { + jest.advanceTimersByTime(3000); + }); + setTimeout(() => { + expect(result.current).toBe(3); + expect(navigateMock).not.toHaveBeenCalled(); + }, 3000); + }); + + it('should update countdown correctly', () => { + const navigateMock = jest.fn(); + let countdown = 3; + const setCountdownMock = jest.fn().mockImplementation((value) => { + countdown = value; + }); + const {result} = renderHook(() => + useCountdownTimer(true, navigateMock, countdown, setCountdownMock), + ); + act(() => { + jest.advanceTimersByTime(2000); + }); + setTimeout(() => { + expect(result.current).toBe(1); + }, 2000); + }); +}); diff --git a/client/src/mocks/handlers.js b/client/src/mocks/handlers.js index a04ad2a4..4a8f4bba 100644 --- a/client/src/mocks/handlers.js +++ b/client/src/mocks/handlers.js @@ -1,6 +1,11 @@ import {signinHandler} from './handlers/signin-handler'; import {signupHandler} from './handlers/signup-handler'; +import {emailVerificationHandler} from './handlers/emailVerification-handler'; -const handlers = [...signinHandler, ...signupHandler]; +const handlers = [ + ...signinHandler, + ...signupHandler, + ...emailVerificationHandler, +]; export default handlers; diff --git a/client/src/mocks/handlers/emailVerification-handler.js b/client/src/mocks/handlers/emailVerification-handler.js new file mode 100644 index 00000000..440a1de6 --- /dev/null +++ b/client/src/mocks/handlers/emailVerification-handler.js @@ -0,0 +1,28 @@ +import {rest} from 'msw'; +import {STATUS_CODES} from 'http'; + +export const emailVerificationHandler = [ + rest.get('/api/auth/verify', (req, res, ctx) => { + const {searchParams} = req.url; + const token = searchParams.get('token'); + if (token === 'exampleToken') { + return res( + ctx.status(200), + ctx.json({ + message: 'Verification successful', + statusCode: 200, + error: STATUS_CODES[200], + }), + ); + } else { + return res( + ctx.status(403), + ctx.json({ + message: 'Token Expired', + statusCode: 403, + error: STATUS_CODES[403], + }), + ); + } + }), +]; diff --git a/client/src/pages/reset-password/ResetPassword.js b/client/src/pages/reset-password/ResetPassword.js index 98948acb..76c9ab55 100644 --- a/client/src/pages/reset-password/ResetPassword.js +++ b/client/src/pages/reset-password/ResetPassword.js @@ -6,6 +6,7 @@ import {useApi} from '../../hooks/useApi'; import ResponseCard from '../../components/common/responseCard/ResponseCard'; import {FaCheck} from 'react-icons/fa6'; import {RxCross2} from 'react-icons/rx'; +import useCountdownTimer from '../../hooks/useCountdownTimer'; import './ResetPassword.css'; function ResetPassword() { @@ -66,22 +67,7 @@ function ResetPassword() { } }, []); - useEffect(() => { - let timer = null; - if (success) { - timer = setInterval(() => { - if (countdown > 0) { - setCountdown((prevCount) => prevCount - 1); - } else { - clearInterval(timer); - navigate('/signin'); - } - }, 1000); - } - return () => { - clearInterval(timer); - }; - }, [success, countdown]); + useCountdownTimer(success, navigate, countdown, setCountdown, '/signin'); return tokenError ? ( { - let timer = null; - if (isSuccess) { - timer = setInterval(() => { - if (countdown > 1) { - setCountdown((prevCount) => prevCount - 1); - } else { - clearInterval(timer); - navigate('/welcome'); - } - }, 1000); - } - return () => { - clearInterval(timer); - }; - }, [isSuccess, countdown, navigate]); - + useCountdownTimer(isSuccess, navigate, countdown, setCountdown); useEffect(() => { makeRequest(); }, []); diff --git a/client/src/pages/verification/Verification.test.js b/client/src/pages/verification/Verification.test.js index 35878761..c19485ee 100644 --- a/client/src/pages/verification/Verification.test.js +++ b/client/src/pages/verification/Verification.test.js @@ -1,24 +1,21 @@ import React from 'react'; import {MemoryRouter, Routes, Route} from 'react-router-dom'; import {render, waitFor, screen} from '@testing-library/react'; -import {useApi} from '../../hooks/useApi'; import Home from '../home/Home'; import Verification from './Verification'; - -jest.mock('../../hooks/useApi'); +import * as router from 'react-router'; describe('VerificationStatus Component', () => { + const navigate = jest.fn(); beforeEach(() => { - jest.clearAllMocks(); + jest.spyOn(router, 'useNavigate').mockImplementation(() => navigate); }); - it('should nvigate to welcome page when token is not provided', async () => { - useApi.mockReturnValue({ - errorMsg: null, - makeRequest: jest.fn(), - isSuccess: true, - loading: false, - }); + afterAll(() => { + jest.restoreAllMocks(); + }); + + it('should navigate to welcome page when token is not provided', async () => { const token = 'exampleToken'; const queryParams = new URLSearchParams(); queryParams.append('token', token); @@ -32,20 +29,15 @@ describe('VerificationStatus Component', () => { , ); await waitFor(() => { - expect(screen.getByTestId('home-container')).toBeInTheDocument(); + expect(navigate).not.toHaveBeenCalled(); }); + setTimeout(() => { + expect(navigate).toHaveBeenCalledWith('/welcome'); + }, 3000); + expect(screen.getByTestId('home-container')).toBeInTheDocument(); }); it('renders "Verification successful" message when token is valid', async () => { - useApi.mockReturnValue({ - errorMsg: null, - makeRequest: jest.fn(), - isSuccess: true, - loading: false, - data: { - message: 'Verification successful', - }, - }); const token = 'exampleToken'; const queryParams = new URLSearchParams(); queryParams.append('token', token); @@ -58,21 +50,13 @@ describe('VerificationStatus Component', () => { , ); - await waitFor(() => { expect(screen.getByText('Verification successful')).toBeInTheDocument(); }); }); it('renders error message message when token is invalid', async () => { - useApi.mockReturnValue({ - errorMsg: 'Token Expired', - makeRequest: jest.fn(), - isSuccess: false, - loading: false, - }); - - const token = 'exampleToken'; + const token = 'malformedToken'; const queryParams = new URLSearchParams(); queryParams.append('token', token); const url = `/verify?${queryParams.toString()}`; diff --git a/yarn.lock b/yarn.lock index ac7e74ed..268baf3c 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4904,6 +4904,14 @@ lodash "^4.17.15" redent "^3.0.0" +"@testing-library/react-hooks@^8.0.1": + version "8.0.1" + resolved "https://registry.yarnpkg.com/@testing-library/react-hooks/-/react-hooks-8.0.1.tgz#0924bbd5b55e0c0c0502d1754657ada66947ca12" + integrity sha512-Aqhl2IVmLt8IovEVarNDFuJDVWVvhnr9/GCU6UUnrYXwgDFF9h2L2o2P9KBni1AST5sT6riAyoukFLyjQUgD/g== + dependencies: + "@babel/runtime" "^7.12.5" + react-error-boundary "^3.1.0" + "@testing-library/react@^13.4.0": version "13.4.0" resolved "https://registry.yarnpkg.com/@testing-library/react/-/react-13.4.0.tgz#6a31e3bf5951615593ad984e96b9e5e2d9380966" @@ -16631,6 +16639,13 @@ react-dom@^18.2.0: loose-envify "^1.1.0" scheduler "^0.23.0" +react-error-boundary@^3.1.0: + version "3.1.4" + resolved "https://registry.yarnpkg.com/react-error-boundary/-/react-error-boundary-3.1.4.tgz#255db92b23197108757a888b01e5b729919abde0" + integrity sha512-uM9uPzZJTF6wRQORmSrvOIgt4lJ9MC1sNgEOj2XGsDTRE4kmpWxg7ENK9EWNKJRMAOY9z0MuF4yIfl6gp4sotA== + dependencies: + "@babel/runtime" "^7.12.5" + react-error-overlay@^6.0.11: version "6.0.11" resolved "https://registry.yarnpkg.com/react-error-overlay/-/react-error-overlay-6.0.11.tgz#92835de5841c5cf08ba00ddd2d677b6d17ff9adb"