From 661fb189d925739a7369f37e137bccb4fd70b0e2 Mon Sep 17 00:00:00 2001 From: Isaac Lee <124631592+ilee2u@users.noreply.github.com> Date: Thu, 6 Apr 2023 10:55:09 -0400 Subject: [PATCH] feat: refresh accessibility string in UI (#94) * feat: refresh accessibility string in UI * fix: refactored humanizedTime * feat: moved a11y string logic to api.js * temp: a11y string test debug * test: test generate a11y string func * fix: renamed pollExamAttempt return var * docs: better intl note * fix: round down exam time + 30 sec refresh * test: added round down test * temp: iterative dev on UI move * feat: moved a11y string gen to UI, removed extra polls * fix: replaced poll func & generating string from ints * test: added tests --- src/data/__factories__/attempt.factory.js | 1 - src/data/__snapshots__/redux.test.jsx.snap | 11 ---- src/data/slice.js | 1 - src/data/thunks.js | 1 - src/timer/CountDownTimer.jsx | 30 ++++++++++- src/timer/CountDownTimer.test.jsx | 58 +++++++++++++++++++++- src/timer/ExamTimerBlock.jsx | 1 - src/timer/TimerProvider.jsx | 2 +- 8 files changed, 86 insertions(+), 19 deletions(-) diff --git a/src/data/__factories__/attempt.factory.js b/src/data/__factories__/attempt.factory.js index 885a9b49..cf1ffe85 100644 --- a/src/data/__factories__/attempt.factory.js +++ b/src/data/__factories__/attempt.factory.js @@ -11,7 +11,6 @@ Factory.define('attempt') exam_url_path: 'http://localhost:2000/course/course-v1:test+special+exam/block-v1:test+special+exam+type@sequential+block@abc123', time_remaining_seconds: 1799.9, course_id: 'course-v1:test+special+exam', - accessibility_time_string: 'you have 30 minutes remaining', exam_started_poll_url: '/api/edx_proctoring/v1/proctored_exam/attempt/1', desktop_application_js_url: '', attempt_code: '', diff --git a/src/data/__snapshots__/redux.test.jsx.snap b/src/data/__snapshots__/redux.test.jsx.snap index 8906e7f6..fc593046 100644 --- a/src/data/__snapshots__/redux.test.jsx.snap +++ b/src/data/__snapshots__/redux.test.jsx.snap @@ -11,7 +11,6 @@ exports[`Data layer integration tests Test exams IDA url Should call the exams s Object { "examState": Object { "activeAttempt": Object { - "accessibility_time_string": "you have 30 minutes remaining", "attempt_code": "", "attempt_id": 1, "attempt_status": "started", @@ -31,7 +30,6 @@ Object { "apiErrorMsg": "", "exam": Object { "attempt": Object { - "accessibility_time_string": "you have 30 minutes remaining", "attempt_code": "", "attempt_id": 1, "attempt_status": "started", @@ -96,7 +94,6 @@ exports[`Data layer integration tests Test exams IDA url Should call the exams s Object { "examState": Object { "activeAttempt": Object { - "accessibility_time_string": "you have 30 minutes remaining", "attempt_code": "", "attempt_id": 1, "attempt_status": "ready_to_submit", @@ -142,7 +139,6 @@ exports[`Data layer integration tests Test getExamAttemptsData Should get, and s Object { "examState": Object { "activeAttempt": Object { - "accessibility_time_string": "you have 30 minutes remaining", "attempt_code": "", "attempt_id": 1, "attempt_status": "started", @@ -162,7 +158,6 @@ Object { "apiErrorMsg": "", "exam": Object { "attempt": Object { - "accessibility_time_string": "you have 30 minutes remaining", "attempt_code": "", "attempt_id": 1, "attempt_status": "started", @@ -227,7 +222,6 @@ exports[`Data layer integration tests Test getLatestAttemptData Should get, and Object { "examState": Object { "activeAttempt": Object { - "accessibility_time_string": "you have 30 minutes remaining", "attempt_code": "", "attempt_id": 1, "attempt_status": "started", @@ -303,7 +297,6 @@ Object { exports[`Data layer integration tests Test pollAttempt Should poll exam attempt, and update attempt and exam 1`] = ` Object { - "accessibility_time_string": "you have 30 minutes remaining", "attempt_code": "", "attempt_id": 1, "attempt_status": "started", @@ -323,7 +316,6 @@ Object { exports[`Data layer integration tests Test pollAttempt Should poll exam attempt, and update attempt and exam 2`] = ` Object { - "accessibility_time_string": "you have 30 minutes remaining", "attempt_code": "", "attempt_id": 1, "attempt_status": "started", @@ -349,7 +341,6 @@ Object { "apiErrorMsg": "Request failed with status code 404", "exam": Object { "attempt": Object { - "accessibility_time_string": "you have 30 minutes remaining", "attempt_code": "", "attempt_id": 2, "attempt_status": "created", @@ -412,7 +403,6 @@ Object { exports[`Data layer integration tests Test startProctoredExam Should start exam, and update attempt and exam 1`] = ` Object { - "accessibility_time_string": "you have 30 minutes remaining", "attempt_code": "", "attempt_id": 1, "attempt_status": "started", @@ -432,7 +422,6 @@ Object { exports[`Data layer integration tests Test startTimedExam Should start exam, and update attempt and exam 1`] = ` Object { - "accessibility_time_string": "you have 30 minutes remaining", "attempt_code": "", "attempt_id": 1, "attempt_status": "started", diff --git a/src/data/slice.js b/src/data/slice.js index 6af6c667..d6348da6 100644 --- a/src/data/slice.js +++ b/src/data/slice.js @@ -50,7 +50,6 @@ export const examSlice = createSlice({ time_remaining_seconds: null, course_id: '', attempt_id: null, - accessibility_time_string: '', attempt_status: '', exam_started_poll_url: '', desktop_application_js_url: '', diff --git a/src/data/thunks.js b/src/data/thunks.js index 0bc34e5b..36a51c99 100644 --- a/src/data/thunks.js +++ b/src/data/thunks.js @@ -257,7 +257,6 @@ export function pollAttempt(url) { const updatedAttempt = { ...currentAttempt, time_remaining_seconds: data.time_remaining_seconds, - accessibility_time_string: data.accessibility_time_string, attempt_status: data.status, }; dispatch(setActiveAttempt({ diff --git a/src/timer/CountDownTimer.jsx b/src/timer/CountDownTimer.jsx index 1eb10d90..81c06952 100644 --- a/src/timer/CountDownTimer.jsx +++ b/src/timer/CountDownTimer.jsx @@ -9,15 +9,43 @@ import { TimerContext } from './TimerProvider'; */ const CountDownTimer = injectIntl((props) => { const timer = useContext(TimerContext); + const timeString = timer.getTimeString(); const [isShowTimer, showTimer, hideTimer] = useToggle(true); const { intl } = props; + const generateAccessbilityString = (timeState) => { + const { hours, minutes } = timeState; + + let remainingTime = ''; + + if (hours !== 0) { + remainingTime += `${hours} hour`; + if (hours >= 2) { + remainingTime += 's'; + } + remainingTime += ' and '; + } + remainingTime += `${minutes} minute`; + if (minutes !== 1) { + remainingTime += 's'; + } + + /** + * INTL NOTE: At the moment, these strings are NOT internationalized/translated. + * The back-end also does not support this either. + * + * It is TBD if this needs to be implemented + */ + return `you have ${remainingTime} remaining`; + }; + return (
- {isShowTimer && timer.getTimeString()} + {generateAccessbilityString(timer.timeState)} + {isShowTimer && timeString} { let attempt; let store; const stopExamAttempt = jest.fn(); - const expireExamAttempt = () => {}; - const pollAttempt = () => {}; + const expireExamAttempt = () => { }; + const pollAttempt = () => { }; const submitAttempt = jest.fn(); submitAttempt.mockReturnValue(jest.fn()); stopExamAttempt.mockReturnValue(jest.fn()); @@ -293,4 +293,58 @@ describe('ExamTimerBlock', () => { await waitFor(() => expect(screen.getByText('00:00:19')).toBeInTheDocument()); }); + + const timesToTest = { + // Because times are rounded down, these values are 60 seconds off + '2 hours and 29 minutes': 9000, + '1 hour and 29 minutes': 5400, + '2 hours and 1 minute': 7320, + '1 hour and 1 minute': 3720, + '2 hours and 0 minutes': 7260, + '1 hour and 0 minutes': 3660, + '29 minutes': 1800, + }; + Object.keys(timesToTest).forEach((timeString) => { + it(`Accessibility time string ${timeString} appears as expected based seconds remaining: ${timesToTest[timeString]}`, async () => { + // create a state with the respective number of seconds + const preloadedState = { + examState: { + isLoading: true, + timeIsOver: false, + activeAttempt: { + attempt_status: 'started', + exam_url_path: 'exam_url_path', + exam_display_name: 'exam name', + time_remaining_seconds: timesToTest[timeString], + exam_started_poll_url: '', + taking_as_proctored: false, + exam_type: 'a timed exam', + }, + proctoringSettings: {}, + exam: { + time_limit_mins: 30, + }, + }, + }; + + // Store it in the state + const testStore = await initializeTestStore(preloadedState); + examStore.getState = store.testStore; + attempt = testStore.getState().examState.activeAttempt; + + // render an exam timer block with that data + render( + , + ); + + // expect the a11y string to be a certain output + await waitFor(() => expect(screen.getByText(`you have ${timeString} remaining`)).toBeInTheDocument()); + }); + }); }); diff --git a/src/timer/ExamTimerBlock.jsx b/src/timer/ExamTimerBlock.jsx index 843c0e4b..e7a00377 100644 --- a/src/timer/ExamTimerBlock.jsx +++ b/src/timer/ExamTimerBlock.jsx @@ -121,7 +121,6 @@ const ExamTimerBlock = injectIntl(({ /> )} - {attempt.accessibility_time_string} diff --git a/src/timer/TimerProvider.jsx b/src/timer/TimerProvider.jsx index 6149a321..3e4140f7 100644 --- a/src/timer/TimerProvider.jsx +++ b/src/timer/TimerProvider.jsx @@ -90,7 +90,7 @@ const TimerServiceProvider = ({ if (timerTick % POLL_INTERVAL === 0 && secondsLeft >= 0) { pollExam(); } - // if exam is proctored ping provider app also + // if exam is proctored ping provider app if (workerUrl && timerTick % pingInterval === pingInterval / 2) { pingHandler(pingInterval, workerUrl); }