Skip to content

Commit

Permalink
feat: refactor non instruction components
Browse files Browse the repository at this point in the history
  • Loading branch information
alangsto committed Feb 1, 2024
1 parent 7f6f442 commit fe12cb1
Show file tree
Hide file tree
Showing 15 changed files with 220 additions and 252 deletions.
8 changes: 4 additions & 4 deletions src/context.jsx
Original file line number Diff line number Diff line change
@@ -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;
70 changes: 35 additions & 35 deletions src/core/ExamStateProvider.jsx
Original file line number Diff line number Diff line change
@@ -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 (
<ExamStateContext.Provider value={contextValue}>
{children}
</ExamStateContext.Provider>
);
};

const mapStateToProps = (state) => ({ ...state.examState });

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 (
// <ExamStateContext.Provider value={contextValue}>
// {children}
// </ExamStateContext.Provider>
// );
// };
//
// const mapStateToProps = (state) => ({ ...state.specialExams });
//
// const ExamStateProvider = withExamStore(
// StateProvider,
// mapStateToProps,
// dispatchActions,
// );
//
// export default ExamStateProvider;
40 changes: 23 additions & 17 deletions src/core/OuterExamTimer.jsx
Original file line number Diff line number Diff line change
@@ -1,23 +1,31 @@
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));
}, [courseId]);

Check failure on line 29 in src/core/OuterExamTimer.jsx

View workflow job for this annotation

GitHub Actions / tests

React Hook useEffect has a missing dependency: 'dispatch'. Either include it or remove the dependency array

Check failure on line 29 in src/core/OuterExamTimer.jsx

View workflow job for this annotation

GitHub Actions / tests

React Hook useEffect has a missing dependency: 'dispatch'. Either include it or remove the dependency array

// if user is not authenticated they cannot have active exam, so no need for timer
Expand All @@ -31,11 +39,11 @@ const ExamTimer = ({ courseId }) => {
{showTimer && (
<ExamTimerBlock
attempt={activeAttempt}
stopExamAttempt={stopExam}
submitExam={submitExam}
expireExamAttempt={expireExam}
pollExamAttempt={pollAttempt}
pingAttempt={pingAttempt}
stopExamAttempt={() => dispatch(stopExam())}
submitExam={() => dispatch(submitExam())}
expireExamAttempt={() => dispatch(expireExam())}
pollExamAttempt={(url) => dispatch(pollAttempt(url))}
pingAttempt={(timeout, url) => dispatch(pingAttempt(timeout, url))}
/>
)}
{apiErrorMsg && <ExamAPIError />}
Expand All @@ -53,9 +61,7 @@ ExamTimer.propTypes = {
* will be shown.
*/
const OuterExamTimer = ({ courseId }) => (
<ExamStateProvider>
<ExamTimer courseId={courseId} />
</ExamStateProvider>
<ExamTimer courseId={courseId} />
);

OuterExamTimer.propTypes = {
Expand Down
5 changes: 1 addition & 4 deletions src/core/SequenceExamWrapper.jsx
Original file line number Diff line number Diff line change
@@ -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.
Expand All @@ -14,9 +13,7 @@ import ExamStateProvider from './ExamStateProvider';
* </SequenceExamWrapper>
*/
const SequenceExamWrapper = (props) => (
<ExamStateProvider>
<ExamWrapper {...props} />
</ExamStateProvider>
<ExamWrapper {...props} />
);

export default SequenceExamWrapper;
2 changes: 1 addition & 1 deletion src/data/__factories__/examState.factory.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import './exam.factory';
import './proctoringSettings.factory';
import './examAccessToken.factory';

Factory.define('examState')
Factory.define('specialExams')
.attr('proctoringSettings', Factory.build('proctoringSettings'))
.attr('exam', Factory.build('exam'))
.attr('examAccessToken', Factory.build('examAccessToken'))
Expand Down
16 changes: 8 additions & 8 deletions src/data/store.js
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import { configureStore } from '@reduxjs/toolkit';
import examReducer from './slice';

export default configureStore({
reducer: {
examState: examReducer,
},
});
// import { configureStore } from '@reduxjs/toolkit';
// import examReducer from './slice';
//
// export default configureStore({
// reducer: {
// examState: examReducer,
// },
// });
3 changes: 2 additions & 1 deletion src/data/thunks.js
Original file line number Diff line number Diff line change
Expand Up @@ -473,7 +473,8 @@ export function pingAttempt(timeoutInSeconds, workerUrl) {

// eslint-disable-next-line function-paren-newline
await updateAttemptAfter(
exam.course_id, exam.content_id, endExamWithFailure(activeAttempt.attempt_id, message))(dispatch);
exam.course_id, exam.content_id, endExamWithFailure(activeAttempt.attempt_id, message)

Check failure on line 476 in src/data/thunks.js

View workflow job for this annotation

GitHub Actions / tests

Missing trailing comma

Check failure on line 476 in src/data/thunks.js

View workflow job for this annotation

GitHub Actions / tests

Missing trailing comma
)(dispatch);

Check failure on line 477 in src/data/thunks.js

View workflow job for this annotation

GitHub Actions / tests

Unexpected newline before ')'

Check failure on line 477 in src/data/thunks.js

View workflow job for this annotation

GitHub Actions / tests

Unexpected newline before ')'
});
};
}
Expand Down
37 changes: 22 additions & 15 deletions src/exam/Exam.jsx
Original file line number Diff line number Diff line change
@@ -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.
Expand All @@ -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,
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -106,11 +113,11 @@ const Exam = ({
{showTimer && (
<ExamTimerBlock
attempt={activeAttempt}
stopExamAttempt={stopExam}
submitExam={submitExam}
expireExamAttempt={expireExam}
pollExamAttempt={pollAttempt}
pingAttempt={pingAttempt}
stopExamAttempt={() => 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
Expand Down
7 changes: 3 additions & 4 deletions src/exam/ExamAPIError.jsx
Original file line number Diff line number Diff line change
@@ -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 (
Expand Down
33 changes: 11 additions & 22 deletions src/exam/ExamAPIError.test.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand All @@ -23,12 +22,10 @@ describe('ExamAPIError', () => {
const defaultMessage = 'A system error has occurred with your exam.';

it('renders with the default information', () => {
store.getState = () => ({ examState: {} });
store.getState = () => ({ specialExams: {} });

const tree = render(
<ExamStateProvider>
<ExamAPIError />
</ExamStateProvider>,
<ExamAPIError />,
{ store },
);

Expand All @@ -42,12 +39,10 @@ describe('ExamAPIError', () => {
};
getConfig.mockImplementation(() => config);

store.getState = () => ({ examState: {} });
store.getState = () => ({ specialExams: {} });

const { getByTestId } = render(
<ExamStateProvider>
<ExamAPIError />
</ExamStateProvider>,
<ExamAPIError />,
{ store },
);

Expand All @@ -58,28 +53,24 @@ describe('ExamAPIError', () => {

it('renders error details when provided', () => {
store.getState = () => ({
examState: { apiErrorMsg: 'Something bad has happened' },
specialExams: { apiErrorMsg: 'Something bad has happened' },
});

const { queryByTestId } = render(
<ExamStateProvider>
<ExamAPIError />
</ExamStateProvider>,
<ExamAPIError />,
{ store },
);

expect(queryByTestId('error-details')).toHaveTextContent(store.getState().examState.apiErrorMsg);
expect(queryByTestId('error-details')).toHaveTextContent(store.getState().specialExams.apiErrorMsg);
});

it('renders default message when error is HTML', () => {
store.getState = () => ({
examState: { apiErrorMsg: '<Response is HTML>' },
specialExams: { apiErrorMsg: '<Response is HTML>' },
});

const { queryByTestId } = render(
<ExamStateProvider>
<ExamAPIError />
</ExamStateProvider>,
<ExamAPIError />,
{ store },
);

Expand All @@ -88,13 +79,11 @@ describe('ExamAPIError', () => {

it('renders default message when there is no error message', () => {
store.getState = () => ({
examState: { apiErrorMsg: '' },
specialExams: { apiErrorMsg: '' },
});

const { queryByTestId } = render(
<ExamStateProvider>
<ExamAPIError />
</ExamStateProvider>,
<ExamAPIError />,
{ store },
);

Expand Down
Loading

0 comments on commit fe12cb1

Please sign in to comment.