-
Notifications
You must be signed in to change notification settings - Fork 18
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat [BD-26]: Timer bar on non-sequence pages (#30)
* feat: Timer bar on non-sequence pages * feat: update fetchExamAttempts URL * test: add tests for stopExam * feat: add separate timer component for non-sequence pages * fix: don't show timer for not authenticated user on non-sequence pages * fix: use required props for exam wrapper Co-authored-by: Viktor Rusakov <[email protected]>
- Loading branch information
1 parent
51b3905
commit 790a476
Showing
14 changed files
with
242 additions
and
78 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,32 @@ | ||
import React 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 }) => ( | ||
<ExamStateContext.Provider value={{ | ||
...state, | ||
showTimer: !!(state.activeAttempt && IS_STARTED_STATUS(state.activeAttempt.attempt_status)), | ||
}} | ||
> | ||
{children} | ||
</ExamStateContext.Provider> | ||
); | ||
|
||
const mapStateToProps = (state) => ({ ...state.examState }); | ||
|
||
const ExamStateProvider = withExamStore( | ||
StateProvider, | ||
mapStateToProps, | ||
dispatchActions, | ||
); | ||
|
||
export default ExamStateProvider; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,64 @@ | ||
import React, { useEffect, useContext } from 'react'; | ||
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'; | ||
|
||
const ExamTimer = ({ courseId }) => { | ||
const state = useContext(ExamStateContext); | ||
const { authenticatedUser } = useContext(AppContext); | ||
const { | ||
activeAttempt, showTimer, stopExam, submitExam, | ||
expireExam, pollAttempt, apiErrorMsg, pingAttempt, | ||
getExamAttemptsData, | ||
} = state; | ||
|
||
// if user is not authenticated they cannot have active exam, so no need for timer | ||
// (also exam API would return 403 error) | ||
if (!authenticatedUser) { | ||
return null; | ||
} | ||
|
||
useEffect(() => { | ||
getExamAttemptsData(courseId); | ||
}, [courseId]); | ||
|
||
return ( | ||
<div className="d-flex flex-column justify-content-center"> | ||
{showTimer && ( | ||
<ExamTimerBlock | ||
attempt={activeAttempt} | ||
stopExamAttempt={stopExam} | ||
submitExam={submitExam} | ||
expireExamAttempt={expireExam} | ||
pollExamAttempt={pollAttempt} | ||
pingAttempt={pingAttempt} | ||
/> | ||
)} | ||
{apiErrorMsg && <ExamAPIError />} | ||
</div> | ||
); | ||
}; | ||
|
||
ExamTimer.propTypes = { | ||
courseId: PropTypes.string.isRequired, | ||
}; | ||
|
||
/** | ||
* OuterExamTimer is the component responsible for showing exam timer on non-sequence pages. | ||
* @param courseId - Id of a course that is checked for active exams, if there is one the timer | ||
* will be shown. | ||
*/ | ||
const OuterExamTimer = ({ courseId }) => ( | ||
<ExamStateProvider> | ||
<ExamTimer courseId={courseId} /> | ||
</ExamStateProvider> | ||
); | ||
|
||
OuterExamTimer.propTypes = { | ||
courseId: PropTypes.string.isRequired, | ||
}; | ||
|
||
export default OuterExamTimer; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,58 @@ | ||
import '@testing-library/jest-dom'; | ||
import { Factory } from 'rosie'; | ||
import React from 'react'; | ||
import OuterExamTimer from './OuterExamTimer'; | ||
import { store, getExamAttemptsData } from '../data'; | ||
import { render } from '../setupTest'; | ||
import { ExamStatus } from '../constants'; | ||
|
||
jest.mock('../data', () => ({ | ||
store: {}, | ||
getExamAttemptsData: jest.fn(), | ||
Emitter: { | ||
on: () => jest.fn(), | ||
once: () => jest.fn(), | ||
off: () => jest.fn(), | ||
emit: () => jest.fn(), | ||
}, | ||
})); | ||
getExamAttemptsData.mockReturnValue(jest.fn()); | ||
store.subscribe = jest.fn(); | ||
store.dispatch = jest.fn(); | ||
|
||
describe('OuterExamTimer', () => { | ||
const courseId = 'course-v1:test+test+test'; | ||
|
||
it('is successfully rendered and shows timer if there is an exam in progress', () => { | ||
const attempt = Factory.build('attempt', { | ||
attempt_status: ExamStatus.STARTED, | ||
}); | ||
store.getState = () => ({ | ||
examState: { | ||
activeAttempt: attempt, | ||
exam: {}, | ||
}, | ||
}); | ||
|
||
const { queryByTestId } = render( | ||
<OuterExamTimer courseId={courseId} />, | ||
{ store }, | ||
); | ||
expect(queryByTestId('exam-timer')).toBeInTheDocument(); | ||
}); | ||
|
||
it('does not render timer if there is no exam in progress', () => { | ||
store.getState = () => ({ | ||
examState: { | ||
activeAttempt: {}, | ||
exam: {}, | ||
}, | ||
}); | ||
|
||
const { queryByTestId } = render( | ||
<OuterExamTimer courseId={courseId} />, | ||
{ store }, | ||
); | ||
expect(queryByTestId('exam-timer')).not.toBeInTheDocument(); | ||
}); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,22 @@ | ||
import React from 'react'; | ||
import ExamWrapper from '../exam/ExamWrapper'; | ||
import ExamStateProvider from './ExamStateProvider'; | ||
|
||
/** | ||
* SequenceExamWrapper is the component responsible for handling special exams. | ||
* It takes control over rendering exam instructions unless exam is started only if | ||
* current sequence item is timed exam. Otherwise, renders any children elements passed. | ||
* @param children - Current course sequence item content (e.g. unit, navigation buttons etc.) | ||
* @returns {JSX.Element} | ||
* @example | ||
* <SequenceExamWrapper sequence={sequence} courseId={courseId}> | ||
* {sequenceContent} | ||
* </SequenceExamWrapper> | ||
*/ | ||
const SequenceExamWrapper = (props) => ( | ||
<ExamStateProvider> | ||
<ExamWrapper {...props} /> | ||
</ExamStateProvider> | ||
); | ||
|
||
export default SequenceExamWrapper; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,50 +1,2 @@ | ||
import React from 'react'; | ||
import ExamWrapper from './exam/ExamWrapper'; | ||
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 }) => ( | ||
<ExamStateContext.Provider value={{ | ||
...state, | ||
showTimer: !!(state.activeAttempt && IS_STARTED_STATUS(state.activeAttempt.attempt_status)), | ||
}} | ||
> | ||
{children} | ||
</ExamStateContext.Provider> | ||
); | ||
|
||
const mapStateToProps = (state) => ({ ...state.examState }); | ||
|
||
export const ExamStateProvider = withExamStore( | ||
StateProvider, | ||
mapStateToProps, | ||
dispatchActions, | ||
); | ||
|
||
/** | ||
* SequenceExamWrapper is the component responsible for handling special exams. | ||
* It takes control over rendering exam instructions unless exam is started only if | ||
* current sequence item is timed exam. Otherwise, renders any children elements passed. | ||
* @param children - Current course sequence item content (e.g. unit, navigation buttons etc.) | ||
* @returns {JSX.Element} | ||
* @example | ||
* <SequenceExamWrapper sequence={sequence} courseId={courseId}> | ||
* {sequenceContent} | ||
* </SequenceExamWrapper> | ||
*/ | ||
const SequenceExamWrapper = (props) => ( | ||
<ExamStateProvider> | ||
<ExamWrapper {...props} /> | ||
</ExamStateProvider> | ||
); | ||
|
||
export default SequenceExamWrapper; | ||
export { default } from './core/SequenceExamWrapper'; | ||
export { default as OuterExamTimer } from './core/OuterExamTimer'; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.