Skip to content

Commit

Permalink
Merge branch 'main' into bilalqamar95/jest-v29-upgrade
Browse files Browse the repository at this point in the history
  • Loading branch information
BilalQamar95 committed Apr 4, 2024
2 parents 31812ec + c2f041f commit cfca465
Show file tree
Hide file tree
Showing 71 changed files with 3,638 additions and 2,277 deletions.
8 changes: 8 additions & 0 deletions .env.test
Original file line number Diff line number Diff line change
@@ -1,3 +1,11 @@
# required for tests
BASE_URL='http://localhost:2000'
LOGIN_URL='http://localhost:18000/login'
LOGOUT_URL='http://localhost:18000/logout'
REFRESH_ACCESS_TOKEN_ENDPOINT='http://localhost:18000/login_refresh'
ACCESS_TOKEN_COOKIE_NAME='edx-jwt-cookie-header-payload'
CSRF_TOKEN_API_PATH='/csrf/api/v1/token'
# ------------------------
LANGUAGE_PREFERENCE_COOKIE_NAME='openedx-language-preference'
LMS_BASE_URL='http://localhost:18000'
EXAMS_BASE_URL='http://localhost:18740'
Expand Down
2 changes: 1 addition & 1 deletion .eslintrc.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// eslint-disable-next-line import/no-extraneous-dependencies
const { createConfig } = require('@edx/frontend-build');
const { createConfig } = require('@openedx/frontend-build');

module.exports = createConfig('eslint');
2 changes: 1 addition & 1 deletion .github/workflows/lockfileversion-check.yml
Original file line number Diff line number Diff line change
Expand Up @@ -10,4 +10,4 @@ on:

jobs:
version-check:
uses: openedx/.github/.github/workflows/lockfileversion-check-v3.yml@master
uses: openedx/.github/.github/workflows/lockfile-check.yml@master
2 changes: 1 addition & 1 deletion jest.config.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
const { createConfig } = require('@edx/frontend-build');
const { createConfig } = require('@openedx/frontend-build');

module.exports = createConfig('jest', {
// setupFilesAfterEnv is used after the jest environment has been loaded. In general this is what you want.
Expand Down
3,408 changes: 2,260 additions & 1,148 deletions package-lock.json

Large diffs are not rendered by default.

14 changes: 7 additions & 7 deletions 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 All @@ -53,8 +54,8 @@
"eventemitter3": "^4.0.7"
},
"peerDependencies": {
"@edx/frontend-platform": "^4.2.0 || ^5.0.0 || ^6.0.0",
"@edx/paragon": "^19.4.1 || ^20.22.4",
"@edx/frontend-platform": "^7.0.0",
"@openedx/paragon": "^22.0.0",
"@reduxjs/toolkit": "^1.5.1",
"prop-types": "^15.7.2",
"react": "^16.14.0 || ^17.0.0",
Expand All @@ -65,10 +66,10 @@
"redux": "^4.0.5"
},
"devDependencies": {
"@edx/frontend-build": "https://github.com/openedx/frontend-build/tarball/bilalqamar95/jest-v29-upgrade",
"@edx/frontend-platform": "5.5.4",
"@edx/paragon": "20.44.0",
"@edx/frontend-platform": "^7.1.3",
"@edx/reactifex": "^2.1.1",
"@openedx/frontend-build": "^13.0.29",
"@openedx/paragon": "^22.2.0",
"@reduxjs/toolkit": "^1.5.1",
"@testing-library/dom": "7.16.3",
"@testing-library/jest-dom": "5.10.1",
Expand All @@ -81,7 +82,6 @@
"prop-types": "15.7.2",
"react": "17.0.2",
"react-dom": "17.0.2",
"react-intl": "^5.25.0",
"react-redux": "^7.2.9",
"react-router": "6.15.0",
"react-router-dom": "6.15.0",
Expand Down
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().examState;
return !!exam?.id;
}
};

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

export function getExamAccess() {
const { exam, examAccessToken } = store.getState().examState;
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().examState;
const { dispatch } = store;
if (!exam) {
return Promise.resolve();
}
return dispatch(examRequiresAccessToken());
}
return () => dispatch(examRequiresAccessToken());
};
67 changes: 44 additions & 23 deletions src/api.test.jsx
Original file line number Diff line number Diff line change
@@ -1,60 +1,81 @@
import { Factory } from 'rosie';

import { isExam, getExamAccess, fetchExamAccess } from './api';
import { store } from './data';
import { useExamAccessToken, useFetchExamAccessToken, useIsExam } from './api';
import { initializeMockApp, 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');
initializeMockApp();
const mockExam = Factory.build('exam', { attempt: Factory.build('attempt') });
const mockToken = Factory.build('examAccessToken');
const mockState = { examState: { exam: mockExam, examAccessToken: mockToken } };
store.getState = jest.fn().mockReturnValue(mockState);
const mockState = { specialExams: { exam: mockExam, examAccessToken: mockToken } };
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 = { examState: { exam: null, examAccessToken: null } };
store.getState = jest.fn().mockReturnValue(mockState);
const mockState = { specialExams: { exam: null, examAccessToken: null } };
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);
});
});
});
4 changes: 0 additions & 4 deletions src/context.jsx

This file was deleted.

35 changes: 0 additions & 35 deletions src/core/ExamStateProvider.jsx

This file was deleted.

29 changes: 9 additions & 20 deletions src/core/OuterExamTimer.jsx
Original file line number Diff line number Diff line change
@@ -1,22 +1,20 @@
import React, { useEffect, useContext } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import PropTypes from 'prop-types';
import { AppContext } from '@edx/frontend-platform/react';
import ExamStateContext from '../context';
import { ExamTimerBlock } from '../timer';
import ExamAPIError from '../exam/ExamAPIError';
import ExamStateProvider from './ExamStateProvider';
import { getLatestAttemptData } from '../data';
import { IS_STARTED_STATUS } from '../constants';

const ExamTimer = ({ courseId }) => {
const state = useContext(ExamStateContext);
const { activeAttempt, apiErrorMsg } = useSelector(state => state.specialExams);
const { authenticatedUser } = useContext(AppContext);
const {
activeAttempt, showTimer, stopExam, submitExam,
expireExam, pollAttempt, apiErrorMsg, pingAttempt,
getLatestAttemptData,
} = state;
const showTimer = !!(activeAttempt && IS_STARTED_STATUS(activeAttempt.attempt_status));
const dispatch = useDispatch();

useEffect(() => {
getLatestAttemptData(courseId);
dispatch(getLatestAttemptData(courseId));
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [courseId]);

Expand All @@ -29,14 +27,7 @@ const ExamTimer = ({ courseId }) => {
return (
<div className="d-flex flex-column justify-content-center">
{showTimer && (
<ExamTimerBlock
attempt={activeAttempt}
stopExamAttempt={stopExam}
submitExam={submitExam}
expireExamAttempt={expireExam}
pollExamAttempt={pollAttempt}
pingAttempt={pingAttempt}
/>
<ExamTimerBlock />
)}
{apiErrorMsg && <ExamAPIError />}
</div>
Expand All @@ -53,9 +44,7 @@ ExamTimer.propTypes = {
* will be shown.
*/
const OuterExamTimer = ({ courseId }) => (
<ExamStateProvider>
<ExamTimer courseId={courseId} />
</ExamStateProvider>
<ExamTimer courseId={courseId} />
);

OuterExamTimer.propTypes = {
Expand Down
17 changes: 10 additions & 7 deletions src/core/OuterExamTimer.test.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,11 @@ import '@testing-library/jest-dom';
import { Factory } from 'rosie';
import React from 'react';
import OuterExamTimer from './OuterExamTimer';
import { store, getLatestAttemptData } from '../data';
import { render } from '../setupTest';
import { getLatestAttemptData } from '../data';
import { initializeTestStore, render } from '../setupTest';
import { ExamStatus } from '../constants';

jest.mock('../data', () => ({
store: {},
getLatestAttemptData: jest.fn(),
Emitter: {
on: () => jest.fn(),
Expand All @@ -17,18 +16,22 @@ jest.mock('../data', () => ({
},
}));
getLatestAttemptData.mockReturnValue(jest.fn());
store.subscribe = jest.fn();
store.dispatch = jest.fn();

describe('OuterExamTimer', () => {
const courseId = 'course-v1:test+test+test';

let store;

beforeEach(() => {
store = initializeTestStore();
});

it('is successfully rendered and shows timer if there is an exam in progress', () => {
const attempt = Factory.build('attempt', {
attempt_status: ExamStatus.STARTED,
});
store.getState = () => ({
examState: {
specialExams: {
activeAttempt: attempt,
exam: {
time_limit_mins: 60,
Expand All @@ -45,7 +48,7 @@ describe('OuterExamTimer', () => {

it('does not render timer if there is no exam in progress', () => {
store.getState = () => ({
examState: {
specialExams: {
activeAttempt: {},
exam: {},
},
Expand Down
5 changes: 1 addition & 4 deletions src/core/SequenceExamWrapper.jsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import React from 'react';
import ExamWrapper from '../exam/ExamWrapper';
import ExamStateProvider from './ExamStateProvider';

/**
* SequenceExamWrapper is the component responsible for handling special exams.
Expand All @@ -14,9 +13,7 @@ import ExamStateProvider from './ExamStateProvider';
* </SequenceExamWrapper>
*/
const SequenceExamWrapper = (props) => (
<ExamStateProvider>
<ExamWrapper {...props} />
</ExamStateProvider>
<ExamWrapper {...props} />
);

export default SequenceExamWrapper;
2 changes: 1 addition & 1 deletion src/data/__factories__/examState.factory.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import './exam.factory';
import './proctoringSettings.factory';
import './examAccessToken.factory';

Factory.define('examState')
Factory.define('specialExams')
.attr('proctoringSettings', Factory.build('proctoringSettings'))
.attr('exam', Factory.build('exam'))
.attr('examAccessToken', Factory.build('examAccessToken'))
Expand Down
Loading

0 comments on commit cfca465

Please sign in to comment.