Skip to content

Commit

Permalink
Merge branch 'main' into mashal-m/update-README
Browse files Browse the repository at this point in the history
  • Loading branch information
Mashal-m authored Oct 30, 2023
2 parents 978eb3f + a56bca6 commit 9e332aa
Show file tree
Hide file tree
Showing 14 changed files with 2,493 additions and 2,184 deletions.
4 changes: 2 additions & 2 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ tx_url1 = https://www.transifex.com/api/2/project/edx-platform/resource/$(transi
tx_url2 = https://www.transifex.com/api/2/project/edx-platform/resource/$(transifex_resource)/source/

# This directory must match .babelrc .
transifex_temp = ./temp/babel-plugin-react-intl
transifex_temp = ./temp/babel-plugin-formatjs

build:
rm -rf ./dist
Expand All @@ -23,7 +23,7 @@ precommit:
npm audit

requirements:
npm install
npm ci

i18n.extract:
# Pulling display strings from .jsx files into .json files...
Expand Down
4,388 changes: 2,279 additions & 2,109 deletions package-lock.json

Large diffs are not rendered by default.

17 changes: 9 additions & 8 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -26,9 +26,10 @@
"build": "make build",
"prepublishOnly": "npm run build",
"semantic-release": "semantic-release",
"i18n_extract": "BABEL_ENV=i18n fedx-scripts babel src --quiet > /dev/null",
"i18n_extract": "fedx-scripts formatjs extract",
"is-es5": "es-check es5 ./dist/*.js",
"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"
},
Expand All @@ -52,20 +53,20 @@
"eventemitter3": "^4.0.7"
},
"peerDependencies": {
"@edx/frontend-platform": "^4.2.0",
"@edx/frontend-platform": "^4.2.0 || ^5.0.0",
"@edx/paragon": "^19.4.1 || ^20.22.4",
"@reduxjs/toolkit": "^1.5.1",
"prop-types": "^15.7.2",
"react": "^16.14.0 || ^17.0.0",
"react-dom": "^16.14.0 || ^17.0.0",
"react-redux": "^7.1.3",
"react-router": "^5.1.2",
"react-router-dom": "^5.1.2",
"react-router": "^5.1.2 || ^6.0.0",
"react-router-dom": "^5.1.2 || ^6.0.0",
"redux": "^4.0.5"
},
"devDependencies": {
"@edx/frontend-build": "12.8.16",
"@edx/frontend-platform": "4.6.0",
"@edx/frontend-build": "13.0.1",
"@edx/frontend-platform": "5.5.4",
"@edx/paragon": "20.44.0",
"@edx/reactifex": "^2.1.1",
"@reduxjs/toolkit": "^1.5.1",
Expand All @@ -82,8 +83,8 @@
"react-dom": "17.0.2",
"react-intl": "^5.25.0",
"react-redux": "^7.2.9",
"react-router": "5.1.2",
"react-router-dom": "5.1.2",
"react-router": "6.15.0",
"react-router-dom": "6.15.0",
"redux": "4.0.5",
"rosie": "2.0.1",
"semantic-release": "^20.1.3"
Expand Down
2 changes: 2 additions & 0 deletions src/constants.js
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,8 @@ export const ONBOARDING_ERRORS = [
export const IS_STARTED_STATUS = (status) => [ExamStatus.STARTED, ExamStatus.READY_TO_SUBMIT].includes(status);
export const IS_INCOMPLETE_STATUS = (status) => INCOMPLETE_STATUSES.includes(status);
export const IS_ONBOARDING_ERROR = (status) => ONBOARDING_ERRORS.includes(status);
// if the exam is proctored we expect the software to be monitoring these states
export const IS_PROCTORED_STATUS = (status) => IS_STARTED_STATUS(status) || status === ExamStatus.READY_TO_START;

// Available actions are taken from
// https://github.com/edx/edx-proctoring/blob/1444ca40a43869fb4e2731cea4862888c5b5f286/edx_proctoring/views.py#L765
Expand Down
4 changes: 2 additions & 2 deletions src/data/__factories__/attempt.factory.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,8 @@ Factory.define('attempt')
attempt_status: 'started',
in_timed_exam: true,
taking_as_proctored: false,
exam_type: 'a timed exam',
exam_display_name: 'timed',
exam_display_name: 'a timed exam',
exam_type: 'timed',
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',
Expand Down
48 changes: 24 additions & 24 deletions src/data/__snapshots__/redux.test.jsx.snap
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,9 @@ Object {
"attempt_status": "started",
"course_id": "course-v1:test+special+exam",
"desktop_application_js_url": "",
"exam_display_name": "timed",
"exam_display_name": "a timed exam",
"exam_started_poll_url": "/api/edx_proctoring/v1/proctored_exam/attempt/1",
"exam_type": "a timed exam",
"exam_type": "timed",
"exam_url_path": "http://localhost:2000/course/course-v1:test+special+exam/block-v1:test+special+exam+type@sequential+block@abc123",
"in_timed_exam": true,
"software_download_url": "",
Expand All @@ -35,9 +35,9 @@ Object {
"attempt_status": "started",
"course_id": "course-v1:test+special+exam",
"desktop_application_js_url": "",
"exam_display_name": "timed",
"exam_display_name": "a timed exam",
"exam_started_poll_url": "/api/edx_proctoring/v1/proctored_exam/attempt/1",
"exam_type": "a timed exam",
"exam_type": "timed",
"exam_url_path": "http://localhost:2000/course/course-v1:test+special+exam/block-v1:test+special+exam+type@sequential+block@abc123",
"in_timed_exam": true,
"software_download_url": "",
Expand Down Expand Up @@ -99,9 +99,9 @@ Object {
"attempt_status": "started",
"course_id": "course-v1:test+special+exam",
"desktop_application_js_url": "",
"exam_display_name": "timed",
"exam_display_name": "a timed exam",
"exam_started_poll_url": "/api/edx_proctoring/v1/proctored_exam/attempt/1",
"exam_type": "a timed exam",
"exam_type": "timed",
"exam_url_path": "http://localhost:2000/course/course-v1:test+special+exam/block-v1:test+special+exam+type@sequential+block@abc123",
"in_timed_exam": true,
"software_download_url": "",
Expand Down Expand Up @@ -190,9 +190,9 @@ Object {
"attempt_status": "started",
"course_id": "course-v1:test+special+exam",
"desktop_application_js_url": "",
"exam_display_name": "timed",
"exam_display_name": "a timed exam",
"exam_started_poll_url": "/api/edx_proctoring/v1/proctored_exam/attempt/1",
"exam_type": "a timed exam",
"exam_type": "timed",
"exam_url_path": "http://localhost:2000/course/course-v1:test+special+exam/block-v1:test+special+exam+type@sequential+block@abc123",
"in_timed_exam": true,
"software_download_url": "",
Expand All @@ -209,9 +209,9 @@ Object {
"attempt_status": "started",
"course_id": "course-v1:test+special+exam",
"desktop_application_js_url": "",
"exam_display_name": "timed",
"exam_display_name": "a timed exam",
"exam_started_poll_url": "/api/edx_proctoring/v1/proctored_exam/attempt/1",
"exam_type": "a timed exam",
"exam_type": "timed",
"exam_url_path": "http://localhost:2000/course/course-v1:test+special+exam/block-v1:test+special+exam+type@sequential+block@abc123",
"in_timed_exam": true,
"software_download_url": "",
Expand All @@ -228,9 +228,9 @@ Object {
"attempt_status": "started",
"course_id": "course-v1:test+special+exam",
"desktop_application_js_url": "",
"exam_display_name": "timed",
"exam_display_name": "a timed exam",
"exam_started_poll_url": "/api/edx_proctoring/v1/proctored_exam/attempt/1",
"exam_type": "a timed exam",
"exam_type": "timed",
"exam_url_path": "http://localhost:2000/course/course-v1:test+special+exam/block-v1:test+special+exam+type@sequential+block@abc123",
"in_timed_exam": true,
"software_download_url": "",
Expand All @@ -253,9 +253,9 @@ Object {
"attempt_status": "created",
"course_id": "course-v1:test+special+exam",
"desktop_application_js_url": "",
"exam_display_name": "timed",
"exam_display_name": "a timed exam",
"exam_started_poll_url": "/api/edx_proctoring/v1/proctored_exam/attempt/1",
"exam_type": "a timed exam",
"exam_type": "timed",
"exam_url_path": "http://localhost:2000/course/course-v1:test+special+exam/block-v1:test+special+exam+type@sequential+block@abc123",
"in_timed_exam": true,
"software_download_url": "",
Expand Down Expand Up @@ -321,9 +321,9 @@ Object {
"attempt_status": "created",
"course_id": "course-v1:test+special+exam",
"desktop_application_js_url": "",
"exam_display_name": "timed",
"exam_display_name": "a timed exam",
"exam_started_poll_url": "/api/edx_proctoring/v1/proctored_exam/attempt/1",
"exam_type": "a timed exam",
"exam_type": "timed",
"exam_url_path": "http://localhost:2000/course/course-v1:test+special+exam/block-v1:test+special+exam+type@sequential+block@abc123",
"in_timed_exam": true,
"software_download_url": "",
Expand Down Expand Up @@ -383,9 +383,9 @@ Object {
"attempt_status": "started",
"course_id": "course-v1:test+special+exam",
"desktop_application_js_url": "",
"exam_display_name": "timed",
"exam_display_name": "a timed exam",
"exam_started_poll_url": "/api/edx_proctoring/v1/proctored_exam/attempt/1",
"exam_type": "a timed exam",
"exam_type": "timed",
"exam_url_path": "http://localhost:2000/course/course-v1:test+special+exam/block-v1:test+special+exam+type@sequential+block@abc123",
"in_timed_exam": true,
"software_download_url": "",
Expand All @@ -402,9 +402,9 @@ Object {
"attempt_status": "started",
"course_id": "course-v1:test+special+exam",
"desktop_application_js_url": "",
"exam_display_name": "timed",
"exam_display_name": "a timed exam",
"exam_started_poll_url": "/api/edx_proctoring/v1/proctored_exam/attempt/1",
"exam_type": "a timed exam",
"exam_type": "timed",
"exam_url_path": "http://localhost:2000/course/course-v1:test+special+exam/block-v1:test+special+exam+type@sequential+block@abc123",
"in_timed_exam": true,
"software_download_url": "",
Expand All @@ -421,9 +421,9 @@ Object {
"attempt_status": "started",
"course_id": "course-v1:test+special+exam",
"desktop_application_js_url": "",
"exam_display_name": "timed",
"exam_display_name": "a timed exam",
"exam_started_poll_url": "/api/edx_proctoring/v1/proctored_exam/attempt/1",
"exam_type": "a timed exam",
"exam_type": "timed",
"exam_url_path": "http://localhost:2000/course/course-v1:test+special+exam/block-v1:test+special+exam+type@sequential+block@abc123",
"in_timed_exam": true,
"software_download_url": "",
Expand All @@ -440,9 +440,9 @@ Object {
"attempt_status": "started",
"course_id": "course-v1:test+special+exam",
"desktop_application_js_url": "",
"exam_display_name": "timed",
"exam_display_name": "a timed exam",
"exam_started_poll_url": "/api/edx_proctoring/v1/proctored_exam/attempt/1",
"exam_type": "a timed exam",
"exam_type": "timed",
"exam_url_path": "http://localhost:2000/course/course-v1:test+special+exam/block-v1:test+special+exam+type@sequential+block@abc123",
"in_timed_exam": true,
"software_download_url": "",
Expand Down
23 changes: 16 additions & 7 deletions src/data/messages/proctorio.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,16 +5,25 @@
* vendor-specific integrations long term. As of now these events
* will trigger on ANY lti integration, not just Proctorio.
*/
export function notifyStartExam() {
window.top.postMessage(
['exam_state_change', 'exam_take'],
'*', // this isn't emitting secure data so any origin is fine
);
export async function checkAppStatus() {
return new Promise((resolve, reject) => {
const handleResponse = event => {
if (event.origin === window.location.origin) {
window.removeEventListener('message', handleResponse);
if (event?.data?.active) {
resolve();
}
reject();
}
};
window.addEventListener('message', handleResponse);
window.top.postMessage(['proctorio_status'], '*');
});
}

export function notifyEndExam() {
export function notifyStartExam() {
window.top.postMessage(
['exam_state_change', 'exam_end'],
['exam_state_change', 'exam_take'],
'*', // this isn't emitting secure data so any origin is fine
);
}
114 changes: 98 additions & 16 deletions src/data/redux.test.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import * as thunks from './thunks';
import executeThunk from '../utils';

import { initializeTestStore, initializeMockApp, initializeTestConfig } from '../setupTest';
import { ExamStatus } from '../constants';
import { ExamStatus, ExamType } from '../constants';

const BASE_API_URL = '/api/edx_proctoring/v1/proctored_exam/attempt';

Expand Down Expand Up @@ -582,21 +582,6 @@ describe('Data layer integration tests', () => {
await executeThunk(thunks.submitExam(), store.dispatch, store.getState);
expect(axiosMock.history.put[0].url).toEqual(updateAttemptStatusLegacyUrl);
});

it('Should notify top window on LTI exam end', async () => {
const mockPostMessage = jest.fn();
windowSpy.mockImplementation(() => ({
top: {
postMessage: mockPostMessage,
},
}));

await initWithExamAttempt();
axiosMock.onGet(fetchExamAttemptsDataUrl).reply(200, { exam: submittedExam });
axiosMock.onPut(`${createUpdateAttemptURL}/${attempt.attempt_id}`).reply(200, { exam_attempt_id: submittedAttempt.attempt_id });
await executeThunk(thunks.submitExam(), store.dispatch, store.getState);
expect(mockPostMessage).toHaveBeenCalledWith(['exam_state_change', 'exam_end'], '*');
});
});

describe('Test expireExam', () => {
Expand Down Expand Up @@ -1095,4 +1080,101 @@ describe('Data layer integration tests', () => {
expect(state.examState.examAccessToken.exam_access_token).toBe('');
});
});

describe('Test checkExamEntry', () => {
const mockPostMessage = jest.fn();
const mockAddEventListener = jest.fn();

beforeEach(() => {
windowSpy.mockImplementation(() => ({
top: {
postMessage: mockPostMessage,
},
location: {
origin: 'https://edx.example.com',
},
addEventListener: mockAddEventListener,
removeEventListener: jest.fn(),
}));
});

afterEach(() => {
jest.clearAllMocks();
axiosMock.reset();
});

it('should check application status for LTI proctored exams in a proctored state', async () => {
const proctoredAttempt = Factory.build('attempt', { attempt_status: ExamStatus.READY_TO_START, exam_type: ExamType.PROCTORED });
const proctoredExam = Factory.build('exam', { type: ExamType.PROCTORED, attempt: proctoredAttempt });
await initWithExamAttempt(proctoredExam, proctoredAttempt);

await executeThunk(thunks.checkExamEntry(), store.dispatch, store.getState);
await new Promise(process.nextTick);
expect(mockPostMessage).toHaveBeenCalled();
});

it('should not check application status for non-LTI proctored exams', async () => {
const proctoredAttempt = Factory.build('attempt', { attempt_status: ExamStatus.READY_TO_START, exam_type: ExamType.PROCTORED, use_legacy_attempt_api: true });
const proctoredExam = Factory.build('exam', { type: ExamType.PROCTORED, attempt: proctoredAttempt });
await initWithExamAttempt(proctoredExam, proctoredAttempt);

await executeThunk(thunks.checkExamEntry(), store.dispatch, store.getState);
await new Promise(process.nextTick);
expect(mockPostMessage).not.toHaveBeenCalled();
});

it('should not check application status for exams in a non-proctored state', async () => {
const nonProctoredAttempt = Factory.build('attempt', { attempt_status: ExamStatus.CREATED });
const proctoredExam = Factory.build('exam', { attempt: nonProctoredAttempt });
await initWithExamAttempt(proctoredExam, nonProctoredAttempt);

await executeThunk(thunks.checkExamEntry(), store.dispatch, store.getState);
await new Promise(process.nextTick);
expect(mockPostMessage).not.toHaveBeenCalled();
});

it('should not check timed exams', async () => {
// default exam is timed
await initWithExamAttempt();

await executeThunk(thunks.checkExamEntry(), store.dispatch, store.getState);
await new Promise(process.nextTick);
expect(mockPostMessage).not.toHaveBeenCalled();
});

it('should transition to error state if check fails', async () => {
const proctoredAttempt = Factory.build('attempt', { attempt_status: ExamStatus.READY_TO_START, exam_type: ExamType.PROCTORED });
const proctoredExam = Factory.build('exam', { type: ExamType.PROCTORED, attempt: proctoredAttempt });
await initWithExamAttempt(proctoredExam, proctoredAttempt);

await executeThunk(thunks.checkExamEntry(), store.dispatch, store.getState);

await new Promise(process.nextTick);
const handleResponseCb = mockAddEventListener.mock.calls[0][1];
axiosMock.onPut(`${createUpdateAttemptURL}/${proctoredAttempt.attempt_id}`).reply(200, { exam_attempt_id: proctoredAttempt.attempt_id });
handleResponseCb({ origin: 'https://edx.example.com', data: { active: false } });

await new Promise(process.nextTick);
const request = axiosMock.history.put[0];
expect(request.data).toEqual(JSON.stringify({
action: 'error',
detail: 'exam reentry disallowed',
}));
});

it('should not transition to error state if check succeeds', async () => {
const proctoredAttempt = Factory.build('attempt', { attempt_status: ExamStatus.READY_TO_START, exam_type: ExamType.PROCTORED });
const proctoredExam = Factory.build('exam', { type: ExamType.PROCTORED, attempt: proctoredAttempt });
await initWithExamAttempt(proctoredExam, proctoredAttempt);

await executeThunk(thunks.checkExamEntry(), store.dispatch, store.getState);

await new Promise(process.nextTick);
const handleResponseCb = mockAddEventListener.mock.calls[0][1];
handleResponseCb({ origin: 'https://edx.example.com', data: { active: true } });

await new Promise(process.nextTick);
expect(axiosMock.history.put.length).toBe(0);
});
});
});
Loading

0 comments on commit 9e332aa

Please sign in to comment.