From 93a1ea638552c169e5e5bd621d1a238ac67a9961 Mon Sep 17 00:00:00 2001 From: Alie Langston Date: Thu, 1 Feb 2024 12:34:50 -0500 Subject: [PATCH] feat: refactor non instruction components --- src/context.jsx | 4 - src/core/ExamStateProvider.jsx | 35 ---- src/core/OuterExamTimer.jsx | 13 +- src/core/SequenceExamWrapper.jsx | 5 +- src/data/store.js | 16 +- src/exam/Exam.jsx | 19 +- src/exam/ExamAPIError.jsx | 7 +- src/exam/ExamAPIError.test.jsx | 21 +-- src/exam/ExamWrapper.jsx | 20 ++- src/exam/ExamWrapper.test.jsx | 169 +++++++----------- .../ReadyToStartProctoredExamInstructions.jsx | 2 +- src/timer/ExamTimerBlock.jsx | 2 +- 12 files changed, 115 insertions(+), 198 deletions(-) delete mode 100644 src/context.jsx delete mode 100644 src/core/ExamStateProvider.jsx diff --git a/src/context.jsx b/src/context.jsx deleted file mode 100644 index 3005f5a7..00000000 --- a/src/context.jsx +++ /dev/null @@ -1,4 +0,0 @@ -import React from 'react'; - -const ExamStateContext = React.createContext({}); -export default ExamStateContext; diff --git a/src/core/ExamStateProvider.jsx b/src/core/ExamStateProvider.jsx deleted file mode 100644 index 174a2b61..00000000 --- a/src/core/ExamStateProvider.jsx +++ /dev/null @@ -1,35 +0,0 @@ -import React, { useMemo } from 'react'; -import { withExamStore } from '../hocs'; -import * as dispatchActions from '../data/thunks'; -import ExamStateContext from '../context'; -import { IS_STARTED_STATUS } from '../constants'; - -/** - * Make exam state available as a context for all library components. - * @param children - sequence content - * @param state - exam state params and actions - * @returns {JSX.Element} - */ - -// eslint-disable-next-line react/prop-types -const StateProvider = ({ children, ...state }) => { - const contextValue = useMemo(() => ({ - ...state, - showTimer: !!(state.activeAttempt && IS_STARTED_STATUS(state.activeAttempt.attempt_status)), - }), [state]); - return ( - - {children} - - ); -}; - -const mapStateToProps = (state) => ({ ...state.specialExams }); - -const ExamStateProvider = withExamStore( - StateProvider, - mapStateToProps, - dispatchActions, -); - -export default ExamStateProvider; diff --git a/src/core/OuterExamTimer.jsx b/src/core/OuterExamTimer.jsx index fe70e7d6..ce57a9e3 100644 --- a/src/core/OuterExamTimer.jsx +++ b/src/core/OuterExamTimer.jsx @@ -2,16 +2,15 @@ import React, { useEffect, useContext } from 'react'; import { useDispatch, useSelector } from 'react-redux'; import PropTypes from 'prop-types'; import { AppContext } from '@edx/frontend-platform/react'; -import ExamStateContext from '../context'; -import { getLatestAttemptData } from '../data'; import { ExamTimerBlock } from '../timer'; import ExamAPIError from '../exam/ExamAPIError'; -import ExamStateProvider from './ExamStateProvider'; +import { getLatestAttemptData } from '../data/thunks'; +import { IS_STARTED_STATUS } from '../constants'; const ExamTimer = ({ courseId }) => { - const examState = useContext(ExamStateContext); + const { activeAttempt } = useSelector(state => state.specialExams); const { authenticatedUser } = useContext(AppContext); - const { showTimer } = examState; + const showTimer = !!(activeAttempt && IS_STARTED_STATUS(activeAttempt.attempt_status)); const { apiErrorMsg } = useSelector(state => state.specialExams); @@ -48,9 +47,7 @@ ExamTimer.propTypes = { * will be shown. */ const OuterExamTimer = ({ courseId }) => ( - - - + ); OuterExamTimer.propTypes = { diff --git a/src/core/SequenceExamWrapper.jsx b/src/core/SequenceExamWrapper.jsx index dea56e2b..4480ac7f 100644 --- a/src/core/SequenceExamWrapper.jsx +++ b/src/core/SequenceExamWrapper.jsx @@ -1,6 +1,5 @@ import React from 'react'; import ExamWrapper from '../exam/ExamWrapper'; -import ExamStateProvider from './ExamStateProvider'; /** * SequenceExamWrapper is the component responsible for handling special exams. @@ -14,9 +13,7 @@ import ExamStateProvider from './ExamStateProvider'; * */ const SequenceExamWrapper = (props) => ( - - - + ); export default SequenceExamWrapper; diff --git a/src/data/store.js b/src/data/store.js index 9fdef030..3ab0617a 100644 --- a/src/data/store.js +++ b/src/data/store.js @@ -1,8 +1,8 @@ -import { configureStore } from '@reduxjs/toolkit'; -import examReducer from './slice'; - -export default configureStore({ - reducer: { - specialExams: examReducer, - }, -}); +// import { configureStore } from '@reduxjs/toolkit'; +// import examReducer from './slice'; +// +// export default configureStore({ +// reducer: { +// examState: examReducer, +// }, +// }); diff --git a/src/exam/Exam.jsx b/src/exam/Exam.jsx index 7c19b766..9fe11ab2 100644 --- a/src/exam/Exam.jsx +++ b/src/exam/Exam.jsx @@ -1,15 +1,15 @@ -/* eslint-disable react-hooks/exhaustive-deps */ -import React, { useContext, useEffect, useState } from 'react'; +import React, { useEffect, useState } from 'react'; +import { useDispatch, useSelector } from 'react-redux'; import PropTypes from 'prop-types'; import { injectIntl, intlShape, FormattedMessage } from '@edx/frontend-platform/i18n'; import { Alert, Spinner } from '@edx/paragon'; import { Info } from '@edx/paragon/icons'; import { ExamTimerBlock } from '../timer'; import Instructions from '../instructions'; -import ExamStateContext from '../context'; import ExamAPIError from './ExamAPIError'; -import { ExamStatus, ExamType } from '../constants'; +import { ExamStatus, ExamType, IS_STARTED_STATUS } from '../constants'; import messages from './messages'; +import { getProctoringSettings } from '../data/thunks'; /** * Exam component is intended to render exam instructions before and after exam. @@ -23,10 +23,12 @@ import messages from './messages'; const Exam = ({ isGated, isTimeLimited, originalUserIsStaff, canAccessProctoredExams, children, intl, }) => { - const state = useContext(ExamStateContext); const { - isLoading, showTimer, exam, apiErrorMsg, getProctoringSettings, - } = state; + isLoading, activeAttempt, exam, apiErrorMsg, + } = useSelector(state => state.specialExams); + const dispatch = useDispatch(); + + const showTimer = !!(activeAttempt && IS_STARTED_STATUS(activeAttempt.attempt_status)); const { attempt, @@ -59,7 +61,7 @@ const Exam = ({ if (proctoredExamTypes.includes(examType)) { // only fetch proctoring settings for a proctored exam if (examId) { - getProctoringSettings(); + dispatch(getProctoringSettings()); } // Only exclude Timed Exam when restricting access to exams @@ -68,6 +70,7 @@ const Exam = ({ // this makes sure useEffect gets called only one time after the exam has been fetched // we can't leave this empty since initially exam is just an empty object, so // API calls above would not get triggered + // eslint-disable-next-line react-hooks/exhaustive-deps }, [examId]); if (isLoading) { diff --git a/src/exam/ExamAPIError.jsx b/src/exam/ExamAPIError.jsx index a6b0e271..157f7e10 100644 --- a/src/exam/ExamAPIError.jsx +++ b/src/exam/ExamAPIError.jsx @@ -1,15 +1,14 @@ -import React, { useContext } from 'react'; +import React from 'react'; +import { useSelector } from 'react-redux'; import { getConfig } from '@edx/frontend-platform'; import { Alert, Hyperlink, Icon } from '@edx/paragon'; import { Info } from '@edx/paragon/icons'; import { injectIntl, intlShape, FormattedMessage } from '@edx/frontend-platform/i18n'; -import ExamStateContext from '../context'; import messages from './messages'; const ExamAPIError = ({ intl }) => { - const state = useContext(ExamStateContext); const { SITE_NAME, SUPPORT_URL } = getConfig(); - const { apiErrorMsg } = state; + const { apiErrorMsg } = useSelector(state => state.specialExams); const shouldShowApiErrorMsg = !!apiErrorMsg && !apiErrorMsg.includes('<'); return ( diff --git a/src/exam/ExamAPIError.test.jsx b/src/exam/ExamAPIError.test.jsx index edb16d1d..1ae2646b 100644 --- a/src/exam/ExamAPIError.test.jsx +++ b/src/exam/ExamAPIError.test.jsx @@ -3,7 +3,6 @@ import React from 'react'; import { getConfig } from '@edx/frontend-platform'; import { store } from '../data'; import { render } from '../setupTest'; -import ExamStateProvider from '../core/ExamStateProvider'; import ExamAPIError from './ExamAPIError'; const originalConfig = jest.requireActual('@edx/frontend-platform').getConfig(); @@ -26,9 +25,7 @@ describe('ExamAPIError', () => { store.getState = () => ({ specialExams: {} }); const tree = render( - - - , + , { store }, ); @@ -45,9 +42,7 @@ describe('ExamAPIError', () => { store.getState = () => ({ specialExams: {} }); const { getByTestId } = render( - - - , + , { store }, ); @@ -62,9 +57,7 @@ describe('ExamAPIError', () => { }); const { queryByTestId } = render( - - - , + , { store }, ); @@ -77,9 +70,7 @@ describe('ExamAPIError', () => { }); const { queryByTestId } = render( - - - , + , { store }, ); @@ -92,9 +83,7 @@ describe('ExamAPIError', () => { }); const { queryByTestId } = render( - - - , + , { store }, ); diff --git a/src/exam/ExamWrapper.jsx b/src/exam/ExamWrapper.jsx index 68b7eee8..229de2c2 100644 --- a/src/exam/ExamWrapper.jsx +++ b/src/exam/ExamWrapper.jsx @@ -1,14 +1,18 @@ +import { useDispatch, useSelector } from 'react-redux'; import React, { useContext, useEffect } from 'react'; import { AppContext } from '@edx/frontend-platform/react'; import PropTypes from 'prop-types'; import Exam from './Exam'; -import ExamStateContext from '../context'; +import { + getExamAttemptsData, + getAllowProctoringOptOut, + checkExamEntry, +} from '../data/thunks'; /** * Exam wrapper is responsible for triggering initial exam data fetching and rendering Exam. */ const ExamWrapper = ({ children, ...props }) => { - const state = useContext(ExamStateContext); const { authenticatedUser } = useContext(AppContext); const { sequence, @@ -17,9 +21,13 @@ const ExamWrapper = ({ children, ...props }) => { originalUserIsStaff, canAccessProctoredExams, } = props; - const { getExamAttemptsData, getAllowProctoringOptOut, checkExamEntry } = state; + + const { isLoading } = useSelector(state => state.specialExams); + + const dispatch = useDispatch(); + const loadInitialData = async () => { - await getExamAttemptsData(courseId, sequence.id); + await dispatch(getExamAttemptsData(courseId, sequence.id)); await getAllowProctoringOptOut(sequence.allowProctoringOptOut); await checkExamEntry(); }; @@ -28,10 +36,10 @@ const ExamWrapper = ({ children, ...props }) => { useEffect(() => { // fetch exam data on exam sequences or if no exam data has been fetched yet - if (sequence.isTimeLimited || state.isLoading) { + if (sequence.isTimeLimited || isLoading) { loadInitialData(); } - // eslint-disable-next-line react-hooks/exhaustive-deps + // eslint-disable-next-line react-hooks/exhaustive-deps }, []); // if the user is browsing public content (not logged in) they cannot be in an exam diff --git a/src/exam/ExamWrapper.test.jsx b/src/exam/ExamWrapper.test.jsx index 04e43ab1..ee1d78e5 100644 --- a/src/exam/ExamWrapper.test.jsx +++ b/src/exam/ExamWrapper.test.jsx @@ -5,7 +5,6 @@ import SequenceExamWrapper from './ExamWrapper'; import { store, startTimedExam } from '../data'; import { getExamAttemptsData } from '../data/thunks'; import { render, waitFor } from '../setupTest'; -import ExamStateProvider from '../core/ExamStateProvider'; import { ExamStatus, ExamType } from '../constants'; jest.mock('../data', () => ({ @@ -46,11 +45,9 @@ describe('SequenceExamWrapper', () => { it('is successfully rendered and shows instructions if the user is not staff', () => { const { queryByTestId } = render( - - -
children
-
-
, + +
children
+
, { store }, ); expect(queryByTestId('exam-instructions-title')).toHaveTextContent('Subsection is a Timed Exam (30 minutes)'); @@ -66,11 +63,9 @@ describe('SequenceExamWrapper', () => { }), }); const { queryByTestId } = render( - - -
children
-
-
, + +
children
+
, { store }, ); expect(queryByTestId('proctored-exam-instructions-title')).toHaveTextContent('This exam is proctored'); @@ -83,11 +78,9 @@ describe('SequenceExamWrapper', () => { }), }); const { queryByTestId } = render( - - -
children
-
-
, + +
children
+
, { store }, ); expect(queryByTestId('spinner')).toBeInTheDocument(); @@ -101,11 +94,9 @@ describe('SequenceExamWrapper', () => { }); const { queryByTestId } = render( - - -
children
-
-
, + +
children
+
, { store }, ); expect(queryByTestId('exam-instructions-title')).toHaveTextContent('Subsection is a Timed Exam (30 minutes)'); @@ -120,11 +111,9 @@ describe('SequenceExamWrapper', () => { }); const { queryByTestId } = render( - - -
children
-
-
, + +
children
+
, { store }, ); expect(queryByTestId('exam-instructions-title')).not.toBeInTheDocument(); @@ -133,11 +122,9 @@ describe('SequenceExamWrapper', () => { it('does not fetch exam data if already loaded and the sequence is not an exam', async () => { render( - - -
children
-
-
, + +
children
+
, { store }, ); // assert the exam data is not fetched @@ -153,11 +140,9 @@ describe('SequenceExamWrapper', () => { }); render( - - -
children
-
-
, + +
children
+
, { store }, ); await waitFor(() => expect(getExamAttemptsData).toHaveBeenCalled()); @@ -165,11 +150,9 @@ describe('SequenceExamWrapper', () => { it('does not take any actions if sequence item is not exam', () => { const { getByTestId } = render( - - -
children
-
-
, + +
children
+
, { store }, ); expect(getByTestId('sequence-content')).toHaveTextContent('children'); @@ -180,11 +163,9 @@ describe('SequenceExamWrapper', () => { authenticatedUser: null, }; const { getByTestId } = render( - - -
children
-
-
, + +
children
+
, { store, appContext }, ); expect(getByTestId('sequence-content')).toHaveTextContent('children'); @@ -199,11 +180,9 @@ describe('SequenceExamWrapper', () => { }), }); const { queryByTestId } = render( - - -
children
-
-
, + +
children
+
, { store }, ); expect(queryByTestId('sequence-content')).toHaveTextContent('children'); @@ -220,11 +199,9 @@ describe('SequenceExamWrapper', () => { }), }); const { queryByTestId } = render( - - -
children
-
-
, + +
children
+
, { store }, ); expect(queryByTestId('sequence-content')).toHaveTextContent('children'); @@ -243,11 +220,9 @@ describe('SequenceExamWrapper', () => { }), }); const { queryByTestId } = render( - - -
children
-
-
, + +
children
+
, { store }, ); expect(queryByTestId('sequence-content')).toHaveTextContent('children'); @@ -267,11 +242,9 @@ describe('SequenceExamWrapper', () => { }), }); const { queryByTestId } = render( - - -
children
-
-
, + +
children
+
, { store }, ); expect(queryByTestId('sequence-content')).toHaveTextContent('children'); @@ -292,11 +265,9 @@ describe('SequenceExamWrapper', () => { }), }); const { queryByTestId } = render( - - -
children
-
-
, + +
children
+
, { store }, ); expect(queryByTestId('sequence-content')).toHaveTextContent('children'); @@ -305,11 +276,9 @@ describe('SequenceExamWrapper', () => { it('does not display masquerade alert if sequence is not time gated', () => { const { queryByTestId } = render( - - -
children
-
-
, + +
children
+
, { store }, ); expect(queryByTestId('sequence-content')).toHaveTextContent('children'); @@ -328,15 +297,13 @@ describe('SequenceExamWrapper', () => { }), }); const { queryByTestId } = render( - - -
children
-
-
, + +
children
+
, { store }, ); expect(queryByTestId('no-access')).toHaveTextContent('You do not have access to proctored exams with your current enrollment.'); @@ -355,15 +322,13 @@ describe('SequenceExamWrapper', () => { }), }); const { queryByTestId } = render( - - -
children
-
-
, + +
children
+
, { store }, ); expect(queryByTestId('no-access')).toBeNull(); @@ -382,15 +347,13 @@ describe('SequenceExamWrapper', () => { }), }); const { queryByTestId } = render( - - -
children
-
-
, + +
children
+
, { store }, ); expect(queryByTestId('no-access')).toBeNull(); diff --git a/src/instructions/proctored_exam/ReadyToStartProctoredExamInstructions.jsx b/src/instructions/proctored_exam/ReadyToStartProctoredExamInstructions.jsx index 7839d00a..5e3ce50f 100644 --- a/src/instructions/proctored_exam/ReadyToStartProctoredExamInstructions.jsx +++ b/src/instructions/proctored_exam/ReadyToStartProctoredExamInstructions.jsx @@ -20,7 +20,7 @@ const ReadyToStartProctoredExamInstructions = () => { useEffect(() => { dispatch(getExamReviewPolicy()); - // eslint-disable-next-line react-hooks/exhaustive-deps + // eslint-disable-next-line react-hooks/exhaustive-deps }, []); const handleStart = () => { diff --git a/src/timer/ExamTimerBlock.jsx b/src/timer/ExamTimerBlock.jsx index fca4c7d7..0364da1e 100644 --- a/src/timer/ExamTimerBlock.jsx +++ b/src/timer/ExamTimerBlock.jsx @@ -55,7 +55,7 @@ const ExamTimerBlock = injectIntl(({ intl }) => { Emitter.off(TIMER_LIMIT_REACHED, () => dispatch(expireExamAttempt)); Emitter.off(TIMER_REACHED_NULL, onTimeReachedNull); }; - // eslint-disable-next-line react-hooks/exhaustive-deps + // eslint-disable-next-line react-hooks/exhaustive-deps }, []); return (