Skip to content

Commit

Permalink
feat: refresh accessibility string in UI (#94)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
ilee2u authored Apr 6, 2023
1 parent a37b9fa commit 661fb18
Show file tree
Hide file tree
Showing 8 changed files with 86 additions and 19 deletions.
1 change: 0 additions & 1 deletion src/data/__factories__/attempt.factory.js
Original file line number Diff line number Diff line change
Expand Up @@ -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: '',
Expand Down
11 changes: 0 additions & 11 deletions src/data/__snapshots__/redux.test.jsx.snap
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand All @@ -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",
Expand Down Expand Up @@ -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",
Expand Down Expand Up @@ -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",
Expand All @@ -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",
Expand Down Expand Up @@ -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",
Expand Down Expand Up @@ -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",
Expand All @@ -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",
Expand All @@ -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",
Expand Down Expand Up @@ -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",
Expand All @@ -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",
Expand Down
1 change: 0 additions & 1 deletion src/data/slice.js
Original file line number Diff line number Diff line change
Expand Up @@ -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: '',
Expand Down
1 change: 0 additions & 1 deletion src/data/thunks.js
Original file line number Diff line number Diff line change
Expand Up @@ -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({
Expand Down
30 changes: 29 additions & 1 deletion src/timer/CountDownTimer.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 (
<div
className="exam-timer-clock d-flex justify-content-between"
style={{ minWidth: isShowTimer ? '110px' : '32px' }}
>
{isShowTimer && timer.getTimeString()}
<span className="sr-only timer-announce" aria-live="assertive">{generateAccessbilityString(timer.timeState)}</span>
{isShowTimer && timeString}
<span
className="pl-2 d-flex flex-column justify-content-center"
id="toggle_timer"
Expand Down
58 changes: 56 additions & 2 deletions src/timer/CountDownTimer.test.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,8 @@ describe('ExamTimerBlock', () => {
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());
Expand Down Expand Up @@ -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(
<ExamTimerBlock
attempt={attempt}
stopExamAttempt={stopExamAttempt}
expireExamAttempt={expireExamAttempt}
pollExamAttempt={pollAttempt}
submitExam={submitAttempt}
/>,
);

// expect the a11y string to be a certain output
await waitFor(() => expect(screen.getByText(`you have ${timeString} remaining`)).toBeInTheDocument());
});
});
});
1 change: 0 additions & 1 deletion src/timer/ExamTimerBlock.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -121,7 +121,6 @@ const ExamTimerBlock = injectIntl(({
/>
</Button>
)}
<span className="sr-only timer-announce" aria-live="assertive">{attempt.accessibility_time_string}</span>

<CountDownTimer />

Expand Down
2 changes: 1 addition & 1 deletion src/timer/TimerProvider.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
Expand Down

0 comments on commit 661fb18

Please sign in to comment.