Skip to content

Commit e5f04d9

Browse files
fix: useExamAccess hook does not make call to fetchExamAccessToken after isExam changes (#1644)
This commit fixes a bug that prevents the fetchExamAccessToken hook in the useExamAccess hook from running. This occurs when the value of isExam returned by the useIsExam hook changes from false to true. Because the dependency array of the useEffect hook within the useExamAccess hook was ['id'], the useEffect hook would not rerun on changes to isExam. The fix is to add isExam to the dependency array.
1 parent f39a50e commit e5f04d9

File tree

2 files changed

+65
-17
lines changed

2 files changed

+65
-17
lines changed

src/courseware/course/sequence/Unit/hooks/useExamAccess.js

+4-3
Original file line numberDiff line numberDiff line change
@@ -18,9 +18,10 @@ const useExamAccess = ({
1818
const fetchExamAccessToken = useFetchExamAccessToken();
1919

2020
// NOTE: We cannot use this hook in the useEffect hook below to grab the updated exam access token in the finally
21-
// block, due to the rules of hooks. Instead, we get the value of the exam access token from a call to the hook.
21+
// block, due to the rules of hooks. Instead, we get the value of the exam access token from a call to
22+
// the hook below.
2223
// When the fetchExamAccessToken call completes, the useExamAccess hook will re-run
23-
// (due to a change to the Redux store, and, thus, a change to the the context), at which point the updated
24+
// (due to a change to the Redux store, and, thus, a change to the context), at which point the updated
2425
// exam access token will be fetched via the useExamAccessToken hook call below.
2526
// The important detail is that there should never be a return value (false, '').
2627
const examAccessToken = useExamAccessToken();
@@ -35,7 +36,7 @@ const useExamAccess = ({
3536
logError(error);
3637
});
3738
}
38-
}, [id]);
39+
}, [id, isExam]);
3940

4041
return {
4142
blockAccess,

src/courseware/course/sequence/Unit/hooks/useExamAccess.test.jsx

+61-14
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
1+
import { useState } from 'react';
12
import { logError } from '@edx/frontend-platform/logging';
2-
import { renderHook } from '@testing-library/react-hooks';
3+
import { act, renderHook } from '@testing-library/react-hooks';
34
import { useExamAccessToken, useFetchExamAccessToken, useIsExam } from '@edx/frontend-lib-special-exams';
45

56
import { initializeMockApp } from '../../../../../setupTest';
@@ -16,10 +17,27 @@ jest.mock('@edx/frontend-lib-special-exams', () => ({
1617

1718
const id = 'test-id';
1819

20+
// This object allows us to manipulate the value of the accessToken.
21+
const testAccessToken = { curr: '' };
22+
1923
const mockFetchExamAccessToken = jest.fn().mockImplementation(() => Promise.resolve());
2024
useFetchExamAccessToken.mockReturnValue(mockFetchExamAccessToken);
2125

22-
const testAccessToken = 'test-access-token';
26+
const mockUseIsExam = (initialState = false) => {
27+
const [isExam, setIsExam] = useState(initialState);
28+
29+
// This setTimeout block is intended to replicate the case where a unit is an exam, but
30+
// the call to fetch exam metadata has not yet completed. That is the value of isExam starts
31+
// as false and transitions to true once the call resolves.
32+
if (!initialState) {
33+
setTimeout(
34+
() => setIsExam(true),
35+
500,
36+
);
37+
}
38+
39+
return isExam;
40+
};
2341

2442
describe('useExamAccess hook', () => {
2543
beforeAll(async () => {
@@ -28,13 +46,13 @@ describe('useExamAccess hook', () => {
2846
});
2947
beforeEach(() => {
3048
jest.clearAllMocks();
31-
32-
// Mock implementations from previous test runs may not have been "consumed", so reset mock implementations.
49+
jest.useFakeTimers();
3350
useExamAccessToken.mockReset();
34-
useExamAccessToken.mockReturnValueOnce('');
35-
useExamAccessToken.mockReturnValueOnce(testAccessToken);
51+
52+
useIsExam.mockImplementation(() => mockUseIsExam());
53+
useExamAccessToken.mockImplementation(() => testAccessToken.curr);
3654
});
37-
describe.only('behavior', () => {
55+
describe('behavior', () => {
3856
it('returns accessToken and blockAccess and doesn\'t call token API if not an exam', () => {
3957
const { result } = renderHook(() => useExamAccess({ id }));
4058
const { accessToken, blockAccess } = result.current;
@@ -43,31 +61,60 @@ describe('useExamAccess hook', () => {
4361
expect(blockAccess).toBe(false);
4462
expect(mockFetchExamAccessToken).not.toHaveBeenCalled();
4563
});
46-
it('returns true for blockAccess if an exam but accessToken not yet fetched', () => {
47-
useIsExam.mockImplementation(() => (true));
64+
it('returns true for blockAccess if an exam but accessToken not yet fetched', async () => {
65+
useIsExam.mockImplementation(() => mockUseIsExam(true));
4866

49-
const { result } = renderHook(() => useExamAccess({ id }));
67+
const { result, waitForNextUpdate } = renderHook(() => useExamAccess({ id }));
5068
const { accessToken, blockAccess } = result.current;
5169

5270
expect(accessToken).toEqual('');
5371
expect(blockAccess).toBe(true);
5472
expect(mockFetchExamAccessToken).toHaveBeenCalled();
73+
74+
// This is to get rid of the act(...) warning.
75+
await act(async () => {
76+
await waitForNextUpdate();
77+
});
5578
});
5679
it('returns false for blockAccess if an exam and accessToken fetch succeeds', async () => {
57-
useIsExam.mockImplementation(() => (true));
80+
useIsExam.mockImplementation(() => mockUseIsExam(true));
5881
const { result, waitForNextUpdate } = renderHook(() => useExamAccess({ id }));
5982

6083
// We wait for the promise to resolve and for updates to state to complete so that blockAccess is updated.
6184
await waitForNextUpdate();
6285

6386
const { accessToken, blockAccess } = result.current;
6487

65-
expect(accessToken).toEqual(testAccessToken);
88+
expect(accessToken).toEqual(testAccessToken.curr);
89+
expect(blockAccess).toBe(false);
90+
expect(mockFetchExamAccessToken).toHaveBeenCalled();
91+
});
92+
it('in progress', async () => {
93+
const { result, waitForNextUpdate } = renderHook(() => useExamAccess({ id }));
94+
95+
let { accessToken, blockAccess } = result.current;
96+
97+
expect(accessToken).toEqual('');
98+
expect(blockAccess).toBe(false);
99+
expect(mockFetchExamAccessToken).not.toHaveBeenCalled();
100+
101+
testAccessToken.curr = 'test-access-token';
102+
103+
// The runAllTimers will update the value of isExam, and the waitForNextUpdate will
104+
// wait for call to setBlockAccess in the finally clause of useEffect hook.
105+
await act(async () => {
106+
jest.runAllTimers();
107+
await waitForNextUpdate();
108+
});
109+
110+
({ accessToken, blockAccess } = result.current);
111+
112+
expect(accessToken).toEqual('test-access-token');
66113
expect(blockAccess).toBe(false);
67114
expect(mockFetchExamAccessToken).toHaveBeenCalled();
68115
});
69116
it('returns false for blockAccess if an exam and accessToken fetch fails', async () => {
70-
useIsExam.mockImplementation(() => (true));
117+
useIsExam.mockImplementation(() => mockUseIsExam(true));
71118

72119
const testError = 'test-error';
73120
mockFetchExamAccessToken.mockImplementationOnce(() => Promise.reject(testError));
@@ -79,7 +126,7 @@ describe('useExamAccess hook', () => {
79126

80127
const { accessToken, blockAccess } = result.current;
81128

82-
expect(accessToken).toEqual(testAccessToken);
129+
expect(accessToken).toEqual(testAccessToken.curr);
83130
expect(blockAccess).toBe(false);
84131
expect(mockFetchExamAccessToken).toHaveBeenCalled();
85132
expect(logError).toHaveBeenCalledWith(testError);

0 commit comments

Comments
 (0)