Skip to content

Commit

Permalink
Fixed exam remaining time display offset (#140)
Browse files Browse the repository at this point in the history
* fix: Fixed exam remaining time display offset

* fix: Removed unused time_remaining_seconds reference in TimerProvider

* fix: Renamed a couple of things and removed unneeded check

* fix: Updated tests and useCallback issue

* chore: Added snapshots updates

* fix: Fixed snapshot issue involving dates

* chore: partial progress on TimerProvider coverage

* chore: 100% coverage on TimerProvider.jsx

* chore: Fixed son NIT comments
  • Loading branch information
rijuma authored Mar 4, 2024
1 parent 7cf0be5 commit 0f64420
Show file tree
Hide file tree
Showing 9 changed files with 475 additions and 75 deletions.
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,8 @@
"lint": "fedx-scripts eslint --ext .js --ext .jsx .",
"lint:fix": "fedx-scripts eslint --fix --ext .js --ext .jsx .",
"snapshot": "fedx-scripts jest --updateSnapshot",
"test": "fedx-scripts jest --coverage --passWithNoTests"
"test": "fedx-scripts jest --coverage --passWithNoTests",
"test:watch": "fedx-scripts jest --passWithNoTests --watch"
},
"husky": {
"hooks": {
Expand Down
7 changes: 7 additions & 0 deletions src/data/__snapshots__/redux.test.jsx.snap
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ Object {
"software_download_url": "",
"taking_as_proctored": false,
"time_remaining_seconds": 1799.9,
"timer_ends": Any<String>,
"total_time": "30 minutes",
},
"allowProctoringOptOut": false,
Expand Down Expand Up @@ -108,6 +109,7 @@ Object {
"software_download_url": "",
"taking_as_proctored": false,
"time_remaining_seconds": 1799.9,
"timer_ends": Any<String>,
"total_time": "30 minutes",
},
"allowProctoringOptOut": false,
Expand Down Expand Up @@ -201,6 +203,7 @@ Object {
"software_download_url": "",
"taking_as_proctored": false,
"time_remaining_seconds": 1799.9,
"timer_ends": Any<String>,
"total_time": "30 minutes",
}
`;
Expand Down Expand Up @@ -396,6 +399,7 @@ Object {
"software_download_url": "",
"taking_as_proctored": false,
"time_remaining_seconds": 1799.9,
"timer_ends": Any<String>,
"total_time": "30 minutes",
}
`;
Expand All @@ -415,6 +419,7 @@ Object {
"software_download_url": "",
"taking_as_proctored": false,
"time_remaining_seconds": 1799.9,
"timer_ends": Any<String>,
"total_time": "30 minutes",
}
`;
Expand All @@ -434,6 +439,7 @@ Object {
"software_download_url": "",
"taking_as_proctored": false,
"time_remaining_seconds": 1799.9,
"timer_ends": Any<String>,
"total_time": "30 minutes",
}
`;
Expand All @@ -453,6 +459,7 @@ Object {
"software_download_url": "",
"taking_as_proctored": false,
"time_remaining_seconds": 1799.9,
"timer_ends": Any<String>,
"total_time": "30 minutes",
}
`;
29 changes: 21 additions & 8 deletions src/data/redux.test.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,20 @@ describe('Data layer integration tests', () => {
await executeThunk(thunks.getExamAttemptsData(courseId, contentId), store.dispatch);
};

// This is a shorthand to check snapshots with an asymmetric matcher every time.
const expectSpecialExamAttemptToMatchSnapshot = (data) => expect(data).toMatchSnapshot({
timer_ends: expect.any(String),
});

// This is a shorthand to check snapshots with an asymmetric matcher every time.
const expectStoreToMatchSnapshot = (data) => expect(data).toMatchSnapshot({
specialExams: {
activeAttempt: {
timer_ends: expect.any(String),
},
},
});

beforeEach(() => {
initializeTestConfig();
windowSpy = jest.spyOn(window, 'window', 'get');
Expand Down Expand Up @@ -81,7 +95,7 @@ describe('Data layer integration tests', () => {
await executeThunk(thunks.getExamAttemptsData(courseId, contentId), store.dispatch);

const state = store.getState();
expect(state).toMatchSnapshot();
expectStoreToMatchSnapshot(state);
});

it('Should translate total time correctly', async () => {
Expand Down Expand Up @@ -216,7 +230,7 @@ describe('Data layer integration tests', () => {

await executeThunk(thunks.startTimedExam(), store.dispatch, store.getState);
state = store.getState();
expect(state.specialExams.activeAttempt).toMatchSnapshot();
expectSpecialExamAttemptToMatchSnapshot(state.specialExams.activeAttempt);
expect(axiosMock.history.post[0].data).toEqual(JSON.stringify({
exam_id: exam.id,
start_clock: 'true',
Expand All @@ -233,7 +247,7 @@ describe('Data layer integration tests', () => {

await executeThunk(thunks.startTimedExam(), store.dispatch, store.getState);
const state = store.getState();
expect(state.specialExams.activeAttempt).toMatchSnapshot();
expectSpecialExamAttemptToMatchSnapshot(state.specialExams.activeAttempt);
expect(axiosMock.history.post[0].data).toEqual(JSON.stringify({
exam_id: exam.id,
start_clock: 'true',
Expand Down Expand Up @@ -758,7 +772,7 @@ describe('Data layer integration tests', () => {

await executeThunk(thunks.startProctoredExam(), store.dispatch, store.getState);
state = store.getState();
expect(state.specialExams.activeAttempt).toMatchSnapshot();
expectSpecialExamAttemptToMatchSnapshot(state.specialExams.activeAttempt);
});
});

Expand All @@ -773,7 +787,7 @@ describe('Data layer integration tests', () => {

await executeThunk(thunks.startProctoredExam(), store.dispatch, store.getState);
state = store.getState();
expect(state.specialExams.activeAttempt).toMatchSnapshot();
expectSpecialExamAttemptToMatchSnapshot(state.specialExams.activeAttempt);
});

it('Should fail to fetch if no exam id', async () => {
Expand Down Expand Up @@ -942,7 +956,7 @@ describe('Data layer integration tests', () => {

await executeThunk(thunks.pollAttempt(attempt.exam_started_poll_url), store.dispatch, store.getState);
const state = store.getState();
expect(state.specialExams.activeAttempt).toMatchSnapshot();
expectSpecialExamAttemptToMatchSnapshot(state.specialExams.activeAttempt);
});

describe('pollAttempt api called directly', () => {
Expand Down Expand Up @@ -1015,8 +1029,7 @@ describe('Data layer integration tests', () => {
await executeThunk(thunks.getLatestAttemptData(courseId), store.dispatch);

const state = store.getState();
expect(state)
.toMatchSnapshot();
expectStoreToMatchSnapshot(state);
});
});

Expand Down
5 changes: 3 additions & 2 deletions src/data/slice.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { createSlice } from '@reduxjs/toolkit';
import { appendTimerEnd } from '../helpers';

/* eslint-disable no-param-reassign */
export const examSlice = createSlice({
Expand Down Expand Up @@ -76,10 +77,10 @@ export const examSlice = createSlice({
},
setExamState: (state, { payload }) => {
state.exam = payload.exam;
state.activeAttempt = payload.activeAttempt;
state.activeAttempt = appendTimerEnd(payload.activeAttempt);
},
setActiveAttempt: (state, { payload }) => {
state.activeAttempt = payload.activeAttempt;
state.activeAttempt = appendTimerEnd(payload.activeAttempt);
state.apiErrorMsg = '';
},
setProctoringSettings: (state, { payload }) => {
Expand Down
3 changes: 3 additions & 0 deletions src/data/thunks.js
Original file line number Diff line number Diff line change
Expand Up @@ -272,6 +272,9 @@ export function pollAttempt(url) {

try {
const data = await pollExamAttempt(url);
if (!data) {
throw new Error('Poll Exam failed to fetch.');
}
const updatedAttempt = {
...currentAttempt,
time_remaining_seconds: data.time_remaining_seconds,
Expand Down
17 changes: 17 additions & 0 deletions src/helpers.js
Original file line number Diff line number Diff line change
Expand Up @@ -36,3 +36,20 @@ export const generateHumanizedTime = (timeRemainingSeconds) => {
}
return remainingTime;
};

// The only information we get on the remaining time on the active exam attempt
// from the endpoint is the remaining seconds. We need to have a fixed time reference
// on the time limit to be able to calculate the remaining time accurately.
export const appendTimerEnd = (activeAttempt) => {
if (!activeAttempt?.time_remaining_seconds) {
return activeAttempt;
}

const timerEnds = new Date(Date.now() + activeAttempt.time_remaining_seconds * 1000);
const updatedActiveAttempt = {
...activeAttempt,
timer_ends: timerEnds.toISOString(),
};

return updatedActiveAttempt;
};
Loading

0 comments on commit 0f64420

Please sign in to comment.