diff --git a/src/context.jsx b/src/context.jsx
index 3005f5a7..89e3698a 100644
--- a/src/context.jsx
+++ b/src/context.jsx
@@ -1,4 +1,4 @@
-import React from 'react';
-
-const ExamStateContext = React.createContext({});
-export default ExamStateContext;
+// import React from 'react';
+//
+// const ExamStateContext = React.createContext({});
+// export default ExamStateContext;
diff --git a/src/core/ExamStateProvider.jsx b/src/core/ExamStateProvider.jsx
index 174a2b61..17a2e864 100644
--- a/src/core/ExamStateProvider.jsx
+++ b/src/core/ExamStateProvider.jsx
@@ -1,35 +1,35 @@
-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;
+// 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 f5ef7fbf..d7fab5d1 100644
--- a/src/core/OuterExamTimer.jsx
+++ b/src/core/OuterExamTimer.jsx
@@ -1,23 +1,32 @@
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 { ExamTimerBlock } from '../timer';
import ExamAPIError from '../exam/ExamAPIError';
-import ExamStateProvider from './ExamStateProvider';
+import {
+ getLatestAttemptData,
+ stopExam,
+ submitExam,
+ expireExam,
+ pollAttempt,
+ pingAttempt,
+} from '../data/thunks';
+import { IS_STARTED_STATUS } from '../constants';
const ExamTimer = ({ courseId }) => {
- const state = useContext(ExamStateContext);
const { authenticatedUser } = useContext(AppContext);
+
const {
- activeAttempt, showTimer, stopExam, submitExam,
- expireExam, pollAttempt, apiErrorMsg, pingAttempt,
- getLatestAttemptData,
- } = state;
+ activeAttempt, apiErrorMsg,
+ } = useSelector(state => state.specialExams);
+ const dispatch = useDispatch();
+
+ const showTimer = !!(activeAttempt && IS_STARTED_STATUS(activeAttempt.attempt_status));
useEffect(() => {
- getLatestAttemptData(courseId);
- // eslint-disable-next-line react-hooks/exhaustive-deps
+ dispatch(getLatestAttemptData(courseId));
+ // eslint-disable-next-line react-hooks/exhaustive-deps
}, [courseId]);
// if user is not authenticated they cannot have active exam, so no need for timer
@@ -31,11 +40,11 @@ const ExamTimer = ({ courseId }) => {
{showTimer && (
dispatch(stopExam())}
+ submitExam={() => dispatch(submitExam())}
+ expireExamAttempt={() => dispatch(expireExam())}
+ pollExamAttempt={(url) => dispatch(pollAttempt(url))}
+ pingAttempt={(timeout, url) => dispatch(pingAttempt(timeout, url))}
/>
)}
{apiErrorMsg && }
@@ -53,9 +62,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 4a2049ad..56c074c0 100644
--- a/src/exam/Exam.jsx
+++ b/src/exam/Exam.jsx
@@ -1,15 +1,22 @@
-/* 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,
+ stopExam,
+ submitExam,
+ expireExam,
+ pollAttempt,
+ pingAttempt,
+} from '../data/thunks';
/**
* Exam component is intended to render exam instructions before and after exam.
@@ -23,12 +30,12 @@ import messages from './messages';
const Exam = ({
isGated, isTimeLimited, originalUserIsStaff, canAccessProctoredExams, children, intl,
}) => {
- const state = useContext(ExamStateContext);
const {
- isLoading, activeAttempt, showTimer, stopExam, exam,
- expireExam, pollAttempt, apiErrorMsg, pingAttempt,
- getProctoringSettings, submitExam,
- } = state;
+ isLoading, activeAttempt, exam, apiErrorMsg,
+ } = useSelector(state => state.specialExams);
+ const dispatch = useDispatch();
+
+ const showTimer = !!(activeAttempt && IS_STARTED_STATUS(activeAttempt.attempt_status));
const {
attempt,
@@ -61,7 +68,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
@@ -70,6 +77,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) {
@@ -106,11 +114,11 @@ const Exam = ({
{showTimer && (
dispatch(stopExam())}
+ submitExam={() => dispatch(submitExam())}
+ expireExamAttempt={() => dispatch(expireExam())}
+ pollExamAttempt={(url) => dispatch(pollAttempt(url))}
+ pingAttempt={(timeout, url) => dispatch(pingAttempt(timeout, url))}
/>
)}
{ // show the error message only if you are in the exam sequence
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/CountDownTimer.test.jsx b/src/timer/CountDownTimer.test.jsx
index 0aa7b909..35fd2818 100644
--- a/src/timer/CountDownTimer.test.jsx
+++ b/src/timer/CountDownTimer.test.jsx
@@ -7,7 +7,7 @@ import {
import examStore from '../data/store';
jest.mock('../data/store', () => ({
- examStore: {},
+ specialExams: {},
}));
describe('ExamTimerBlock', () => {
diff --git a/src/timer/ExamTimerBlock.jsx b/src/timer/ExamTimerBlock.jsx
index 3744e11a..a268ba39 100644
--- a/src/timer/ExamTimerBlock.jsx
+++ b/src/timer/ExamTimerBlock.jsx
@@ -54,7 +54,7 @@ const ExamTimerBlock = injectIntl(({
Emitter.off(TIMER_LIMIT_REACHED, expireExamAttempt);
Emitter.off(TIMER_REACHED_NULL, onTimeReachedNull);
};
- // eslint-disable-next-line react-hooks/exhaustive-deps
+ // eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
return (