Skip to content

Commit

Permalink
feat: refactor public API to use hooks instead of references to expor…
Browse files Browse the repository at this point in the history
…ted store

This commit refactors the public API, used by the frontend-app-learning application to interact with the frontend-lib-special-exams state, to export a series of hooks. Originally, the public API imported the frontend-lib-special-exams store directly and operated on it in a series of exported functions. This posed a problem for our need to merge the frontend-app-learning and frontend-lib-special-exams stores, because the special exams store is initialized in this repository and used by public API. In order to eventually be able to remove the creation of the store in this repository, we have to remove references to the store by interfacing with the Redux more directly by using the useDispatch and useSelector hooks.

This commit also exports the root reducer from this library. This root reducer will be imported by the frontend-app-learning application and used to configure its store.
  • Loading branch information
MichaelRoytman committed Feb 5, 2024
1 parent 6ec658d commit 5fc54c0
Show file tree
Hide file tree
Showing 4 changed files with 64 additions and 36 deletions.
29 changes: 17 additions & 12 deletions src/api.js
Original file line number Diff line number Diff line change
@@ -1,23 +1,28 @@
import { examRequiresAccessToken, store } from './data';
import { useDispatch, useSelector } from 'react-redux';
import { examRequiresAccessToken } from './data';

export const useIsExam = () => {
const { exam } = useSelector(state => state.specialExams);

export function isExam() {
const { exam } = store.getState().specialExams;
return !!exam?.id;
}
};

export const useExamAccessToken = () => {
const { exam, examAccessToken } = useSelector(state => state.specialExams);

export function getExamAccess() {
const { exam, examAccessToken } = store.getState().specialExams;
if (!exam) {
return '';
}

return examAccessToken.exam_access_token;
}
};

export const useFetchExamAccessToken = () => {
const { exam } = useSelector(state => state.specialExams);
const dispatch = useDispatch();

export async function fetchExamAccess() {
const { exam } = store.getState().specialExams;
const { dispatch } = store;
if (!exam) {
return Promise.resolve();
}
return dispatch(examRequiresAccessToken());
}
return () => dispatch(examRequiresAccessToken());
};
62 changes: 41 additions & 21 deletions src/api.test.jsx
Original file line number Diff line number Diff line change
@@ -1,60 +1,80 @@
import { Factory } from 'rosie';

import { isExam, getExamAccess, fetchExamAccess } from './api';
import { store } from './data';
import { useExamAccessToken, useFetchExamAccessToken, useIsExam } from './api';
import { initializeTestStore, render } from './setupTest';

/**
* Hooks must be run in the scope of a component. To run the hook, wrap it in a test component whose sole
* responsibility it is to run the hook and assign it to a return value that is returned by the function.
* @param {*} hook: the hook function to run
* @param {*} store: an initial store, passed to the call to render
* @returns: the return value of the hook
*/
const getHookReturnValue = (hook, store) => {
let returnVal;
const TestComponent = () => {
returnVal = hook();
return null;
};
render(<TestComponent />, { store });
return returnVal;
};

describe('External API integration tests', () => {
describe('Test isExam with exam', () => {
describe('Test useIsExam with exam', () => {
let store;

beforeAll(() => {
jest.mock('./data');
const mockExam = Factory.build('exam', { attempt: Factory.build('attempt') });
const mockToken = Factory.build('examAccessToken');
const mockState = { specialExams: { exam: mockExam, examAccessToken: mockToken } };
store.getState = jest.fn().mockReturnValue(mockState);
store = initializeTestStore(mockState);
});

afterAll(() => {
jest.clearAllMocks();
jest.resetAllMocks();
});

it('isExam should return true if exam is set', () => {
expect(isExam()).toBe(true);
it('useIsExam should return true if exam is set', () => {
expect(getHookReturnValue(useIsExam, store)).toBe(true);
});

it('getExamAccess should return exam access token if access token', () => {
expect(getExamAccess()).toBeTruthy();
it('useExamAccessToken should return exam access token if access token', () => {
expect(getHookReturnValue(useExamAccessToken, store)).toBeTruthy();
});

it('fetchExamAccess should dispatch get exam access token', () => {
const dispatchReturn = fetchExamAccess();
expect(dispatchReturn).toBeInstanceOf(Promise);
it('useFetchExamAccessToken should dispatch get exam access token', () => {
// The useFetchExamAccessToken hook returns a function that calls dispatch, so we must call the returned
// value to invoke dispatch.
expect(getHookReturnValue(useFetchExamAccessToken, store)()).toBeInstanceOf(Promise);
});
});

describe('Test isExam without exam', () => {
describe('Test useIsExam without exam', () => {
let store;

beforeAll(() => {
jest.mock('./data');
const mockState = { specialExams: { exam: null, examAccessToken: null } };
store.getState = jest.fn().mockReturnValue(mockState);
store = initializeTestStore(mockState);
});

afterAll(() => {
jest.clearAllMocks();
jest.resetAllMocks();
});

it('isExam should return false if exam is not set', () => {
expect(isExam()).toBe(false);
it('useIsExam should return false if exam is not set', () => {
expect(getHookReturnValue(useIsExam, store)).toBe(false);
});

it('getExamAccess should return empty string if exam access token not set', () => {
expect(getExamAccess()).toBeFalsy();
it('useExamAccessToken should return empty string if exam access token not set', () => {
expect(getHookReturnValue(useExamAccessToken, store)).toBeFalsy();
});

it('fetchExamAccess should not dispatch get exam access token', () => {
const dispatchReturn = fetchExamAccess();
expect(dispatchReturn).toBeInstanceOf(Promise);
it('useFetchExamAccessToken should not dispatch get exam access token', () => {
expect(getHookReturnValue(useFetchExamAccessToken, store)).toBeInstanceOf(Promise);
});
});
});
2 changes: 2 additions & 0 deletions src/data/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ export {
checkExamEntry,
} from './thunks';

export { default as reducer } from './slice';

export {
expireExamAttempt,
} from './slice';
Expand Down
7 changes: 4 additions & 3 deletions src/index.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,8 @@
export { default } from './core/SequenceExamWrapper';
export { default as OuterExamTimer } from './core/OuterExamTimer';
export {
getExamAccess,
isExam,
fetchExamAccess,
useExamAccessToken,
useFetchExamAccessToken,
useIsExam,
} from './api';
export { reducer } from './data';

0 comments on commit 5fc54c0

Please sign in to comment.