Merge branch 'master' into KristinAoki/TNL-8511
This commit is contained in:
5
.env
5
.env
@@ -1,3 +1,6 @@
|
||||
# See README.rst for explanations of these.
|
||||
# If you add a new learning MFE-specific variable, please note it there!
|
||||
|
||||
NODE_ENV='production'
|
||||
ACCESS_TOKEN_COOKIE_NAME=''
|
||||
BASE_URL=''
|
||||
@@ -32,4 +35,4 @@ TERMS_OF_SERVICE_URL=''
|
||||
TWITTER_HASHTAG=''
|
||||
TWITTER_URL=''
|
||||
USER_INFO_COOKIE_NAME=''
|
||||
SESSION_COOKIE_DOMAIN=''
|
||||
SESSION_COOKIE_DOMAIN=''
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
# See README.rst for explanations of these.
|
||||
# If you add a new learning MFE-specific variable, please note it there!
|
||||
|
||||
NODE_ENV='development'
|
||||
ACCESS_TOKEN_COOKIE_NAME='edx-jwt-cookie-header-payload'
|
||||
BASE_URL='http://localhost:2000'
|
||||
@@ -32,4 +35,4 @@ TERMS_OF_SERVICE_URL='https://www.edx.org/edx-terms-service'
|
||||
TWITTER_HASHTAG='myedxjourney'
|
||||
TWITTER_URL='https://twitter.com/edXOnline'
|
||||
USER_INFO_COOKIE_NAME='edx-user-info'
|
||||
SESSION_COOKIE_DOMAIN='localhost'
|
||||
SESSION_COOKIE_DOMAIN='localhost'
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
# See README.rst for explanations of these.
|
||||
# If you add a new learning MFE-specific variable, please note it there!
|
||||
|
||||
NODE_ENV='test'
|
||||
ACCESS_TOKEN_COOKIE_NAME='edx-jwt-cookie-header-payload'
|
||||
BASE_URL='http://localhost:2000'
|
||||
|
||||
4
.github/workflows/validate.yml
vendored
4
.github/workflows/validate.yml
vendored
@@ -11,11 +11,11 @@ jobs:
|
||||
- 12
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/setup-node@v1
|
||||
- uses: actions/setup-node@v2
|
||||
with:
|
||||
node-version: ${{ matrix.node-version }}
|
||||
- run: make validate.ci
|
||||
- name: Upload coverage
|
||||
uses: codecov/codecov-action@v1
|
||||
uses: codecov/codecov-action@v2
|
||||
with:
|
||||
fail_ci_if_error: true
|
||||
|
||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -8,6 +8,7 @@ coverage
|
||||
dist/
|
||||
src/i18n/transifex_input.json
|
||||
temp/babel-plugin-react-intl
|
||||
logs
|
||||
|
||||
### pyenv ###
|
||||
.python-version
|
||||
|
||||
4
.husky/pre-commit
Executable file
4
.husky/pre-commit
Executable file
@@ -0,0 +1,4 @@
|
||||
#!/bin/sh
|
||||
. "$(dirname "$0")/_/husky.sh"
|
||||
|
||||
npm run lint
|
||||
96
README.rst
96
README.rst
@@ -1,23 +1,21 @@
|
||||
|Coveralls| |npm_version| |npm_downloads| |license|
|
||||
|codecov| |license|
|
||||
|
||||
frontend-app-learning
|
||||
=========================
|
||||
|
||||
Please tag **@edx/teaching-and-learning** on any PRs or issues. Thanks.
|
||||
|
||||
Introduction
|
||||
------------
|
||||
|
||||
React app for edX learning.
|
||||
This is the Learning MFE (micro-frontend application), which renders all
|
||||
learner-facing course pages (like the course outline, the progress page,
|
||||
actual course content, etc).
|
||||
|
||||
.. |Coveralls| image:: https://img.shields.io/coveralls/edx/frontend-app-learning.svg?branch=master
|
||||
:target: https://coveralls.io/github/edx/frontend-app-learning
|
||||
.. |npm_version| image:: https://img.shields.io/npm/v/@edx/frontend-app-learning.svg
|
||||
:target: @edx/frontend-app-learning
|
||||
.. |npm_downloads| image:: https://img.shields.io/npm/dt/@edx/frontend-app-learning.svg
|
||||
:target: @edx/frontend-app-learning
|
||||
.. |license| image:: https://img.shields.io/npm/l/@edx/frontend-app-learning.svg
|
||||
:target: @edx/frontend-app-learning
|
||||
Please tag **@edx/engage-squad** on any PRs or issues. Thanks.
|
||||
|
||||
.. |codecov| image:: https://codecov.io/gh/edx/frontend-app-learning/branch/master/graph/badge.svg?token=3z7XvuzTq3
|
||||
:target: https://codecov.io/gh/edx/frontend-app-learning
|
||||
.. |license| image:: https://img.shields.io/badge/license-AGPL-informational
|
||||
:target: https://github.com/edx/frontend-app-account/blob/master/LICENSE
|
||||
|
||||
Development
|
||||
-----------
|
||||
@@ -25,22 +23,10 @@ Development
|
||||
Start Devstack
|
||||
^^^^^^^^^^^^^^
|
||||
|
||||
To use this application `devstack <https://github.com/edx/devstack>`__ must be running and you must be logged into it.
|
||||
To use this application, `devstack <https://github.com/edx/devstack>`__ must be running and you must be logged into it.
|
||||
|
||||
- Start devstack
|
||||
- Log in (http://localhost:18000/login)
|
||||
|
||||
Start the development server
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
In this project, install requirements and start the development server by running:
|
||||
|
||||
.. code:: bash
|
||||
|
||||
npm install
|
||||
npm start # The server will run on port 1995
|
||||
|
||||
Once the dev server is up, visit http://localhost:2000/course/course-v1:edX+DemoX+Demo_Course to view the demo course. You can replace ``course-v1:edX+DemoX+Demo_Course`` with a different course key.
|
||||
- Run ``make dev.up.lms``
|
||||
- Visit http://localhost:2000/course/course-v1:edX+DemoX+Demo_Course to view the demo course. You can replace ``course-v1:edX+DemoX+Demo_Course`` with a different course key.
|
||||
|
||||
Local module development
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
@@ -67,3 +53,59 @@ file (which is git-ignored) that defines where to find your local modules, for i
|
||||
};
|
||||
|
||||
See https://github.com/edx/frontend-build#local-module-configuration-for-webpack for more details.
|
||||
|
||||
Deployment
|
||||
----------
|
||||
|
||||
The Learning MFE is similar to all the other Open edX MFEs. Read the Open
|
||||
edX Developer Guide's section on
|
||||
`MFE applications <https://edx.readthedocs.io/projects/edx-developer-docs/en/latest/developers_guide/micro_frontends_in_open_edx.html>`_.
|
||||
|
||||
Environment Variables
|
||||
^^^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
This MFE is configured via environment variables supplied at build time.
|
||||
All micro-frontends have a shared set of required environment variables,
|
||||
as documented in the Open edX Developer Guide under
|
||||
`Required Environment Variables <https://edx.readthedocs.io/projects/edx-developer-docs/en/latest/developers_guide/micro_frontends_in_open_edx.html#required-environment-variables>`_.
|
||||
|
||||
The learning micro-frontend also supports the following additional variables:
|
||||
|
||||
SOCIAL_UTM_MILESTONE_CAMPAIGN
|
||||
This value is passed as the ``utm_campaign`` parameter for social-share
|
||||
links when celebrating learning milestones in the course. Optional.
|
||||
|
||||
Example: ``milestone``
|
||||
|
||||
SUPPORT_URL_CALCULATOR_MATH
|
||||
A link that explains how to use the in-course calculator. You can use the
|
||||
one in the example below, if you don't want to have your own branded version.
|
||||
|
||||
Example: https://support.edx.org/hc/en-us/articles/360000038428-Entering-math-expressions-in-assignments-or-the-calculator
|
||||
|
||||
SUPPORT_URL_ID_VERIFICATION
|
||||
A link that explains how to verify your ID. Shown in contexts where you need
|
||||
to verify yourself to earn a certificate. The example link below is probably too
|
||||
edx.org-specific to use for your own site.
|
||||
|
||||
Example: https://support.edx.org/hc/en-us/articles/206503858-How-do-I-verify-my-identity
|
||||
|
||||
SUPPORT_URL_VERIFIED_CERTIFICATE
|
||||
A link that explains what a verified certificate is. You can use the
|
||||
one in the example below, if you don't want to have your own branded version.
|
||||
Optional.
|
||||
|
||||
Example: https://support.edx.org/hc/en-us/articles/206502008-What-is-a-verified-certificate
|
||||
|
||||
TWITTER_HASHTAG
|
||||
This value is used in the Twitter social-share link when celebrating learning
|
||||
milestones in the course. Will prefill the suggested post with this hashtag.
|
||||
Optional.
|
||||
|
||||
Example: ``brandedhashtag``
|
||||
|
||||
TWITTER_URL
|
||||
A link to your Twitter account. The Twitter social-share link won't appear
|
||||
unless this is set. Optional.
|
||||
|
||||
Example: https://twitter.com/edXOnline
|
||||
|
||||
15713
package-lock.json
generated
15713
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
68
package.json
68
package.json
@@ -16,15 +16,11 @@
|
||||
"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 .",
|
||||
"prepare": "husky install",
|
||||
"snapshot": "fedx-scripts jest --updateSnapshot",
|
||||
"start": "fedx-scripts webpack-dev-server --progress",
|
||||
"test": "fedx-scripts jest --coverage --passWithNoTests"
|
||||
},
|
||||
"husky": {
|
||||
"hooks": {
|
||||
"pre-commit": "npm run lint"
|
||||
}
|
||||
},
|
||||
"author": "edX",
|
||||
"license": "AGPL-3.0",
|
||||
"homepage": "https://github.com/edx/frontend-app-learning#readme",
|
||||
@@ -36,49 +32,51 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@edx/brand": "npm:@edx/brand-openedx@1.1.0",
|
||||
"@edx/frontend-component-footer": "10.1.5",
|
||||
"@edx/frontend-enterprise": "4.2.3",
|
||||
"@edx/frontend-lib-special-exams": "1.11.0",
|
||||
"@edx/frontend-platform": "1.11.0",
|
||||
"@edx/paragon": "16.5.0",
|
||||
"@fortawesome/fontawesome-svg-core": "1.2.34",
|
||||
"@fortawesome/free-brands-svg-icons": "5.13.1",
|
||||
"@fortawesome/free-regular-svg-icons": "5.13.1",
|
||||
"@fortawesome/free-solid-svg-icons": "5.13.1",
|
||||
"@fortawesome/react-fontawesome": "0.1.14",
|
||||
"@reduxjs/toolkit": "1.3.6",
|
||||
"classnames": "2.2.6",
|
||||
"core-js": "3.6.5",
|
||||
"@edx/frontend-component-footer": "10.1.6",
|
||||
"@edx/frontend-enterprise-utils": "0.1.7",
|
||||
"@edx/frontend-lib-special-exams": "1.12.0",
|
||||
"@edx/frontend-platform": "1.12.3",
|
||||
"@edx/paragon": "16.7.0",
|
||||
"@fortawesome/fontawesome-svg-core": "1.2.36",
|
||||
"@fortawesome/free-brands-svg-icons": "5.15.4",
|
||||
"@fortawesome/free-regular-svg-icons": "5.15.4",
|
||||
"@fortawesome/free-solid-svg-icons": "5.15.4",
|
||||
"@fortawesome/react-fontawesome": "0.1.15",
|
||||
"@pact-foundation/pact": "9.16.0",
|
||||
"@reduxjs/toolkit": "1.6.1",
|
||||
"classnames": "2.3.1",
|
||||
"core-js": "3.16.1",
|
||||
"js-cookie": "2.2.1",
|
||||
"lodash.camelcase": "^4.3.0",
|
||||
"lodash.camelcase": "4.3.0",
|
||||
"prop-types": "15.7.2",
|
||||
"react": "16.13.1",
|
||||
"react": "17.0.2",
|
||||
"react-break": "1.3.2",
|
||||
"react-dom": "16.13.1",
|
||||
"react-helmet": "6.0.0",
|
||||
"react-dom": "17.0.2",
|
||||
"react-helmet": "6.1.0",
|
||||
"react-redux": "7.2.4",
|
||||
"react-router": "5.2.0",
|
||||
"react-router-dom": "5.2.0",
|
||||
"react-share": "4.2.1",
|
||||
"redux": "4.0.5",
|
||||
"regenerator-runtime": "0.13.7",
|
||||
"react-share": "4.4.0",
|
||||
"redux": "4.1.1",
|
||||
"regenerator-runtime": "0.13.9",
|
||||
"reselect": "4.0.0",
|
||||
"truncate-html": "1.0.3"
|
||||
"truncate-html": "1.0.4",
|
||||
"util": "0.12.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@edx/frontend-build": "5.5.5",
|
||||
"@edx/frontend-build": "8.0.0",
|
||||
"@testing-library/dom": "7.16.3",
|
||||
"@testing-library/jest-dom": "5.10.1",
|
||||
"@testing-library/jest-dom": "5.14.1",
|
||||
"@testing-library/react": "10.3.0",
|
||||
"@testing-library/user-event": "12.0.17",
|
||||
"axios-mock-adapter": "1.18.2",
|
||||
"codecov": "3.8.2",
|
||||
"es-check": "5.1.4",
|
||||
"@testing-library/user-event": "12.8.3",
|
||||
"axios-mock-adapter": "1.19.0",
|
||||
"codecov": "3.8.3",
|
||||
"es-check": "5.2.4",
|
||||
"glob": "7.1.7",
|
||||
"husky": "3.1.0",
|
||||
"jest": "24.9.0",
|
||||
"husky": "7.0.1",
|
||||
"jest": "27.0.6",
|
||||
"jest-chain": "1.1.5",
|
||||
"reactifex": "1.1.1",
|
||||
"rosie": "2.0.1"
|
||||
"rosie": "2.1.0"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,7 +9,7 @@ import { faSpinner } from '@fortawesome/free-solid-svg-icons';
|
||||
import { useModel } from '../../generic/model-store';
|
||||
|
||||
import messages from './messages';
|
||||
import { useEnrollClickHandler } from './hooks';
|
||||
import useEnrollClickHandler from './clickHook';
|
||||
|
||||
function EnrollmentAlert({ intl, payload }) {
|
||||
const {
|
||||
|
||||
35
src/alerts/enrollment-alert/clickHook.js
Normal file
35
src/alerts/enrollment-alert/clickHook.js
Normal file
@@ -0,0 +1,35 @@
|
||||
import { useContext, useState, useCallback } from 'react';
|
||||
import { sendTrackEvent } from '@edx/frontend-platform/analytics';
|
||||
|
||||
import { UserMessagesContext, ALERT_TYPES } from '../../generic/user-messages';
|
||||
|
||||
import { postCourseEnrollment } from './data/api';
|
||||
|
||||
// Separated into its own file to avoid a circular dependency inside this directory
|
||||
|
||||
function useEnrollClickHandler(courseId, orgId, successText) {
|
||||
const [loading, setLoading] = useState(false);
|
||||
const { addFlash } = useContext(UserMessagesContext);
|
||||
const enrollClickHandler = useCallback(() => {
|
||||
setLoading(true);
|
||||
postCourseEnrollment(courseId).then(() => {
|
||||
addFlash({
|
||||
dismissible: true,
|
||||
flash: true,
|
||||
text: successText,
|
||||
type: ALERT_TYPES.SUCCESS,
|
||||
topic: 'course',
|
||||
});
|
||||
setLoading(false);
|
||||
sendTrackEvent('edx.bi.user.course-home.enrollment', {
|
||||
org_key: orgId,
|
||||
courserun_key: courseId,
|
||||
});
|
||||
global.location.reload();
|
||||
});
|
||||
}, [courseId]);
|
||||
|
||||
return { enrollClickHandler, loading };
|
||||
}
|
||||
|
||||
export default useEnrollClickHandler;
|
||||
@@ -1,15 +1,12 @@
|
||||
/* eslint-disable import/prefer-default-export */
|
||||
import React, {
|
||||
useContext, useState, useCallback, useMemo,
|
||||
useContext, useMemo,
|
||||
} from 'react';
|
||||
import { sendTrackEvent } from '@edx/frontend-platform/analytics';
|
||||
import { AppContext } from '@edx/frontend-platform/react';
|
||||
|
||||
import { UserMessagesContext, ALERT_TYPES, useAlert } from '../../generic/user-messages';
|
||||
import { useAlert } from '../../generic/user-messages';
|
||||
import { useModel } from '../../generic/model-store';
|
||||
|
||||
import { postCourseEnrollment } from './data/api';
|
||||
|
||||
const EnrollmentAlert = React.lazy(() => import('./EnrollmentAlert'));
|
||||
|
||||
export function useEnrollmentAlert(courseId) {
|
||||
@@ -40,28 +37,3 @@ export function useEnrollmentAlert(courseId) {
|
||||
|
||||
return { clientEnrollmentAlert: EnrollmentAlert };
|
||||
}
|
||||
|
||||
export function useEnrollClickHandler(courseId, orgId, successText) {
|
||||
const [loading, setLoading] = useState(false);
|
||||
const { addFlash } = useContext(UserMessagesContext);
|
||||
const enrollClickHandler = useCallback(() => {
|
||||
setLoading(true);
|
||||
postCourseEnrollment(courseId).then(() => {
|
||||
addFlash({
|
||||
dismissible: true,
|
||||
flash: true,
|
||||
text: successText,
|
||||
type: ALERT_TYPES.SUCCESS,
|
||||
topic: 'course',
|
||||
});
|
||||
setLoading(false);
|
||||
sendTrackEvent('edx.bi.user.course-home.enrollment', {
|
||||
org_key: orgId,
|
||||
courserun_key: courseId,
|
||||
});
|
||||
global.location.reload();
|
||||
});
|
||||
}, [courseId]);
|
||||
|
||||
return { enrollClickHandler, loading };
|
||||
}
|
||||
|
||||
@@ -18,9 +18,7 @@ function AuthenticatedUserDropdown({ enterpriseLearnerPortalLink, intl, username
|
||||
);
|
||||
if (enterpriseLearnerPortalLink && Object.keys(enterpriseLearnerPortalLink).length > 0) {
|
||||
dashboardMenuItem = (
|
||||
<Dropdown.Item
|
||||
href={enterpriseLearnerPortalLink.href}
|
||||
>
|
||||
<Dropdown.Item href={enterpriseLearnerPortalLink.href}>
|
||||
{enterpriseLearnerPortalLink.content}
|
||||
</Dropdown.Item>
|
||||
);
|
||||
@@ -62,13 +60,17 @@ function AuthenticatedUserDropdown({ enterpriseLearnerPortalLink, intl, username
|
||||
}
|
||||
|
||||
AuthenticatedUserDropdown.propTypes = {
|
||||
enterpriseLearnerPortalLink: PropTypes.string,
|
||||
intl: intlShape.isRequired,
|
||||
username: PropTypes.string.isRequired,
|
||||
enterpriseLearnerPortalLink: PropTypes.shape({
|
||||
type: PropTypes.string,
|
||||
href: PropTypes.string,
|
||||
content: PropTypes.string,
|
||||
}),
|
||||
};
|
||||
|
||||
AuthenticatedUserDropdown.defaultProps = {
|
||||
enterpriseLearnerPortalLink: '',
|
||||
enterpriseLearnerPortalLink: undefined,
|
||||
};
|
||||
|
||||
export default injectIntl(AuthenticatedUserDropdown);
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import React, { useContext } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { useEnterpriseConfig } from '@edx/frontend-enterprise';
|
||||
import { useEnterpriseConfig } from '@edx/frontend-enterprise-utils';
|
||||
import { getConfig } from '@edx/frontend-platform';
|
||||
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
|
||||
import { AppContext } from '@edx/frontend-platform/react';
|
||||
|
||||
@@ -11,7 +11,7 @@ import enrollmentMessages from '../../../../alerts/enrollment-alert/messages';
|
||||
import genericMessages from '../../../../generic/messages';
|
||||
import messages from './messages';
|
||||
import outlineMessages from '../../messages';
|
||||
import { useEnrollClickHandler } from '../../../../alerts/enrollment-alert/hooks';
|
||||
import useEnrollClickHandler from '../../../../alerts/enrollment-alert/clickHook';
|
||||
import { useModel } from '../../../../generic/model-store';
|
||||
|
||||
function PrivateCourseAlert({ intl, payload }) {
|
||||
|
||||
@@ -83,17 +83,21 @@ function CertificateStatus({ intl }) {
|
||||
const idVerificationSupportLink = <IdVerificationSupportLink />;
|
||||
const profileLink = <ProfileLink />;
|
||||
|
||||
// Some learners have a valid ("downloadable") certificate without being in a passing
|
||||
// state (e.g. learners who have been added to a course's allowlist), so we need to
|
||||
// skip grade validation for these learners
|
||||
const certIsDownloadable = certStatus === 'downloadable';
|
||||
if (mode === COURSE_EXIT_MODES.disabled) {
|
||||
certEventName = 'certificate_status_disabled';
|
||||
} else if (mode === COURSE_EXIT_MODES.nonPassing) {
|
||||
} else if (mode === COURSE_EXIT_MODES.nonPassing && !certIsDownloadable) {
|
||||
certCase = 'notPassing';
|
||||
certEventName = 'not_passing';
|
||||
body = intl.formatMessage(messages[`${certCase}Body`]);
|
||||
} else if (mode === COURSE_EXIT_MODES.inProgress) {
|
||||
} else if (mode === COURSE_EXIT_MODES.inProgress && !certIsDownloadable) {
|
||||
certCase = 'inProgress';
|
||||
certEventName = 'has_scheduled_content';
|
||||
body = intl.formatMessage(messages[`${certCase}Body`]);
|
||||
} else if (mode === COURSE_EXIT_MODES.celebration) {
|
||||
} else if (mode === COURSE_EXIT_MODES.celebration || certIsDownloadable) {
|
||||
switch (certStatus) {
|
||||
case 'requesting':
|
||||
certCase = 'requestable';
|
||||
|
||||
@@ -64,7 +64,7 @@ function SubsectionTitleCell({ intl, subsection }) {
|
||||
{displayName}
|
||||
</a>
|
||||
) : (
|
||||
<span className="small">{displayName}</span>
|
||||
<span className="greyed-out small">{displayName}</span>
|
||||
)}
|
||||
</span>
|
||||
</Row>
|
||||
|
||||
@@ -244,7 +244,7 @@ function Sequence({
|
||||
if (sequenceStatus === 'loaded') {
|
||||
return (
|
||||
<div>
|
||||
<SequenceExamWrapper sequence={sequence} courseId={courseId}>
|
||||
<SequenceExamWrapper sequence={sequence} courseId={courseId} isStaff={course.isStaff}>
|
||||
{defaultContent}
|
||||
</SequenceExamWrapper>
|
||||
<CourseLicense license={course.license || undefined} />
|
||||
|
||||
423
src/courseware/data/pact-tests/frontend-app-learning-lms.json
Normal file
423
src/courseware/data/pact-tests/frontend-app-learning-lms.json
Normal file
@@ -0,0 +1,423 @@
|
||||
{
|
||||
"consumer": {
|
||||
"name": "frontend-app-learning"
|
||||
},
|
||||
"provider": {
|
||||
"name": "lms"
|
||||
},
|
||||
"interactions": [
|
||||
{
|
||||
"description": "a request to get course blocks",
|
||||
"providerState": "Blocks data exists for course_id course-v1:edX+DemoX+Demo_Course",
|
||||
"request": {
|
||||
"method": "GET",
|
||||
"path": "/api/courses/v2/blocks/",
|
||||
"query": "course_id=course-v1%3AedX%2BDemoX%2BDemo_Course&username=Mock+User&depth=3&requested_fields=children%2Ceffort_activities%2Ceffort_time%2Cshow_gated_sections%2Cgraded%2Cspecial_exam_info%2Chas_scheduled_content"
|
||||
},
|
||||
"response": {
|
||||
"status": 200,
|
||||
"headers": {
|
||||
},
|
||||
"body": {
|
||||
"root": "block-v1:edX+DemoX+Demo_Course+type@course+block@course",
|
||||
"blocks": {
|
||||
"block-v1:edX+DemoX+Demo_Course+type@course+block@course": {
|
||||
"id": "block-v1:edX+DemoX+Demo_Course+type@course+block@course",
|
||||
"block_id": "course",
|
||||
"lms_web_url": "/courses/course-v1:edX+DemoX+Demo_Course/jump_to/block-v1:edX+DemoX+Demo_Course+type@course+block@course",
|
||||
"legacy_web_url": "/courses/course-v1:edX+DemoX+Demo_Course/jump_to/block-v1:edX+DemoX+Demo_Course+type@course+block@course?experience=legacy",
|
||||
"student_view_url": "/xblock/block-v1:edX+DemoX+Demo_Course+type@course+block@course",
|
||||
"type": "course",
|
||||
"display_name": "Demonstration Course"
|
||||
}
|
||||
}
|
||||
},
|
||||
"matchingRules": {
|
||||
"$.body.root": {
|
||||
"match": "type"
|
||||
},
|
||||
"$.body.blocks": {
|
||||
"match": "type"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"description": "a request to get course metadata",
|
||||
"providerState": "course metadata exists for course_id course-v1:edX+DemoX+Demo_Course",
|
||||
"request": {
|
||||
"method": "GET",
|
||||
"path": "/api/courseware/course/course-v1:edX+DemoX+Demo_Course",
|
||||
"query": "browser_timezone=Asia%2FKarachi"
|
||||
},
|
||||
"response": {
|
||||
"status": 200,
|
||||
"headers": {
|
||||
},
|
||||
"body": {
|
||||
"access_expiration": {
|
||||
"expiration_date": "2013-02-05T05:00:00Z",
|
||||
"masquerading_expired_course": false,
|
||||
"upgrade_deadline": "2013-02-05T05:00:00Z",
|
||||
"upgrade_url": "link"
|
||||
},
|
||||
"can_show_upgrade_sock": false,
|
||||
"content_type_gating_enabled": false,
|
||||
"end": "2013-02-05T05:00:00Z",
|
||||
"enrollment": {
|
||||
"mode": "audit",
|
||||
"is_active": true
|
||||
},
|
||||
"enrollment_start": "2013-02-05T05:00:00Z",
|
||||
"enrollment_end": "2013-02-05T05:00:00Z",
|
||||
"id": "course-v1:edX+DemoX+Demo_Course",
|
||||
"license": "all-rights-reserved",
|
||||
"name": "Demonstration Course",
|
||||
"number": "DemoX",
|
||||
"offer": {
|
||||
"code": "code",
|
||||
"expiration_date": "2013-02-05T05:00:00Z",
|
||||
"original_price": "$99",
|
||||
"discounted_price": "$99",
|
||||
"percentage": 50,
|
||||
"upgrade_url": "url"
|
||||
},
|
||||
"org": "edX",
|
||||
"related_programs": null,
|
||||
"short_description": "",
|
||||
"start": "2013-02-05T05:00:00Z",
|
||||
"tabs": [
|
||||
{
|
||||
"title": "Course",
|
||||
"slug": "courseware",
|
||||
"priority": 0,
|
||||
"type": "courseware",
|
||||
"url": "http://localhost:2000/course/course-v1:edX+DemoX+Demo_Course/home"
|
||||
}
|
||||
],
|
||||
"user_timezone": null,
|
||||
"verified_mode": {
|
||||
"access_expiration_date": null,
|
||||
"currency": "USD",
|
||||
"currency_symbol": "$",
|
||||
"price": 149,
|
||||
"sku": "8CF08E5",
|
||||
"upgrade_url": "http://localhost:18130/basket/add/?sku=8CF08E5"
|
||||
},
|
||||
"show_calculator": false,
|
||||
"original_user_is_staff": true,
|
||||
"can_view_legacy_courseware": true,
|
||||
"is_staff": true,
|
||||
"course_access": {
|
||||
"has_access": true,
|
||||
"error_code": null,
|
||||
"developer_message": null,
|
||||
"user_message": null,
|
||||
"additional_context_user_message": null,
|
||||
"user_fragment": null
|
||||
},
|
||||
"notes": {
|
||||
"enabled": false,
|
||||
"visible": true
|
||||
},
|
||||
"marketing_url": null,
|
||||
"celebrations": {
|
||||
"irst_section": false,
|
||||
"streak_length_to_celebrate": null,
|
||||
"streak_discount_experiment_enabled": false
|
||||
},
|
||||
"user_has_passing_grade": false,
|
||||
"course_exit_page_is_active": false,
|
||||
"certificate_data": {
|
||||
"cert_status": "audit_passing",
|
||||
"cert_web_view_url": null,
|
||||
"download_url": null,
|
||||
"certificate_available_date": null
|
||||
},
|
||||
"verify_identity_url": null,
|
||||
"verification_status": "none",
|
||||
"linkedin_add_to_profile_url": null,
|
||||
"is_mfe_special_exams_enabled": false,
|
||||
"is_mfe_proctored_exams_enabled": false,
|
||||
"user_needs_integrity_signature": false
|
||||
},
|
||||
"matchingRules": {
|
||||
"$.body.access_expiration.expiration_date": {
|
||||
"match": "regex",
|
||||
"regex": "^(?:[1-9]\\d{3}-(?:(?:0[1-9]|1[0-2])-(?:0[1-9]|1\\d|2[0-8])|(?:0[13-9]|1[0-2])-(?:29|30)|(?:0[13578]|1[02])-31)|(?:[1-9]\\d(?:0[48]|[2468][048]|[13579][26])|(?:[2468][048]|[13579][26])00)-02-29)T(?:[01]\\d|2[0-3]):[0-5]\\d:[0-5]\\d(?:Z|[+-][01]\\d:[0-5]\\d)$"
|
||||
},
|
||||
"$.body.access_expiration.masquerading_expired_course": {
|
||||
"match": "type"
|
||||
},
|
||||
"$.body.access_expiration.upgrade_deadline": {
|
||||
"match": "regex",
|
||||
"regex": "^(?:[1-9]\\d{3}-(?:(?:0[1-9]|1[0-2])-(?:0[1-9]|1\\d|2[0-8])|(?:0[13-9]|1[0-2])-(?:29|30)|(?:0[13578]|1[02])-31)|(?:[1-9]\\d(?:0[48]|[2468][048]|[13579][26])|(?:[2468][048]|[13579][26])00)-02-29)T(?:[01]\\d|2[0-3]):[0-5]\\d:[0-5]\\d(?:Z|[+-][01]\\d:[0-5]\\d)$"
|
||||
},
|
||||
"$.body.access_expiration.upgrade_url": {
|
||||
"match": "type"
|
||||
},
|
||||
"$.body.can_show_upgrade_sock": {
|
||||
"match": "type"
|
||||
},
|
||||
"$.body.content_type_gating_enabled": {
|
||||
"match": "type"
|
||||
},
|
||||
"$.body.end": {
|
||||
"match": "regex",
|
||||
"regex": "^(?:[1-9]\\d{3}-(?:(?:0[1-9]|1[0-2])-(?:0[1-9]|1\\d|2[0-8])|(?:0[13-9]|1[0-2])-(?:29|30)|(?:0[13578]|1[02])-31)|(?:[1-9]\\d(?:0[48]|[2468][048]|[13579][26])|(?:[2468][048]|[13579][26])00)-02-29)T(?:[01]\\d|2[0-3]):[0-5]\\d:[0-5]\\d(?:Z|[+-][01]\\d:[0-5]\\d)$"
|
||||
},
|
||||
"$.body.enrollment.mode": {
|
||||
"match": "regex",
|
||||
"regex": "^(audit|verified)$"
|
||||
},
|
||||
"$.body.enrollment.is_active": {
|
||||
"match": "type"
|
||||
},
|
||||
"$.body.enrollment_start": {
|
||||
"match": "regex",
|
||||
"regex": "^(?:[1-9]\\d{3}-(?:(?:0[1-9]|1[0-2])-(?:0[1-9]|1\\d|2[0-8])|(?:0[13-9]|1[0-2])-(?:29|30)|(?:0[13578]|1[02])-31)|(?:[1-9]\\d(?:0[48]|[2468][048]|[13579][26])|(?:[2468][048]|[13579][26])00)-02-29)T(?:[01]\\d|2[0-3]):[0-5]\\d:[0-5]\\d(?:Z|[+-][01]\\d:[0-5]\\d)$"
|
||||
},
|
||||
"$.body.enrollment_end": {
|
||||
"match": "regex",
|
||||
"regex": "^(?:[1-9]\\d{3}-(?:(?:0[1-9]|1[0-2])-(?:0[1-9]|1\\d|2[0-8])|(?:0[13-9]|1[0-2])-(?:29|30)|(?:0[13578]|1[02])-31)|(?:[1-9]\\d(?:0[48]|[2468][048]|[13579][26])|(?:[2468][048]|[13579][26])00)-02-29)T(?:[01]\\d|2[0-3]):[0-5]\\d:[0-5]\\d(?:Z|[+-][01]\\d:[0-5]\\d)$"
|
||||
},
|
||||
"$.body.id": {
|
||||
"match": "regex",
|
||||
"regex": "[\\w\\-~.:]"
|
||||
},
|
||||
"$.body.license": {
|
||||
"match": "type"
|
||||
},
|
||||
"$.body.name": {
|
||||
"match": "type"
|
||||
},
|
||||
"$.body.number": {
|
||||
"match": "type"
|
||||
},
|
||||
"$.body.offer.code": {
|
||||
"match": "type"
|
||||
},
|
||||
"$.body.offer.expiration_date": {
|
||||
"match": "regex",
|
||||
"regex": "^(?:[1-9]\\d{3}-(?:(?:0[1-9]|1[0-2])-(?:0[1-9]|1\\d|2[0-8])|(?:0[13-9]|1[0-2])-(?:29|30)|(?:0[13578]|1[02])-31)|(?:[1-9]\\d(?:0[48]|[2468][048]|[13579][26])|(?:[2468][048]|[13579][26])00)-02-29)T(?:[01]\\d|2[0-3]):[0-5]\\d:[0-5]\\d(?:Z|[+-][01]\\d:[0-5]\\d)$"
|
||||
},
|
||||
"$.body.offer.original_price": {
|
||||
"match": "type"
|
||||
},
|
||||
"$.body.offer.discounted_price": {
|
||||
"match": "type"
|
||||
},
|
||||
"$.body.offer.percentage": {
|
||||
"match": "type"
|
||||
},
|
||||
"$.body.offer.upgrade_url": {
|
||||
"match": "type"
|
||||
},
|
||||
"$.body.org": {
|
||||
"match": "type"
|
||||
},
|
||||
"$.body.short_description": {
|
||||
"match": "type"
|
||||
},
|
||||
"$.body.start": {
|
||||
"match": "regex",
|
||||
"regex": "^(?:[1-9]\\d{3}-(?:(?:0[1-9]|1[0-2])-(?:0[1-9]|1\\d|2[0-8])|(?:0[13-9]|1[0-2])-(?:29|30)|(?:0[13578]|1[02])-31)|(?:[1-9]\\d(?:0[48]|[2468][048]|[13579][26])|(?:[2468][048]|[13579][26])00)-02-29)T(?:[01]\\d|2[0-3]):[0-5]\\d:[0-5]\\d(?:Z|[+-][01]\\d:[0-5]\\d)$"
|
||||
},
|
||||
"$.body.tabs": {
|
||||
"min": 1
|
||||
},
|
||||
"$.body.tabs[*].*": {
|
||||
"match": "type"
|
||||
},
|
||||
"$.body.verified_mode": {
|
||||
"match": "type"
|
||||
},
|
||||
"$.body.show_calculator": {
|
||||
"match": "type"
|
||||
},
|
||||
"$.body.original_user_is_staff": {
|
||||
"match": "type"
|
||||
},
|
||||
"$.body.can_view_legacy_courseware": {
|
||||
"match": "type"
|
||||
},
|
||||
"$.body.is_staff": {
|
||||
"match": "type"
|
||||
},
|
||||
"$.body.course_access": {
|
||||
"match": "type"
|
||||
},
|
||||
"$.body.course_access.has_access": {
|
||||
"match": "type"
|
||||
},
|
||||
"$.body.notes.enabled": {
|
||||
"match": "type"
|
||||
},
|
||||
"$.body.notes.visible": {
|
||||
"match": "type"
|
||||
},
|
||||
"$.body.celebrations.irst_section": {
|
||||
"match": "type"
|
||||
},
|
||||
"$.body.celebrations.streak_discount_experiment_enabled": {
|
||||
"match": "type"
|
||||
},
|
||||
"$.body.user_has_passing_grade": {
|
||||
"match": "type"
|
||||
},
|
||||
"$.body.course_exit_page_is_active": {
|
||||
"match": "type"
|
||||
},
|
||||
"$.body.certificate_data.cert_status": {
|
||||
"match": "type"
|
||||
},
|
||||
"$.body.verification_status": {
|
||||
"match": "type"
|
||||
},
|
||||
"$.body.is_mfe_special_exams_enabled": {
|
||||
"match": "type"
|
||||
},
|
||||
"$.body.is_mfe_proctored_exams_enabled": {
|
||||
"match": "type"
|
||||
},
|
||||
"$.body.user_needs_integrity_signature": {
|
||||
"match": "type"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"description": "a request to get sequence metadata",
|
||||
"providerState": "sequence metadata data exists for sequence_id block-v1:edX+DemoX+Demo_Course+type@sequential+block@basic_questions",
|
||||
"request": {
|
||||
"method": "GET",
|
||||
"path": "/api/courseware/sequence/block-v1:edX+DemoX+Demo_Course+type@sequential+block@basic_questions"
|
||||
},
|
||||
"response": {
|
||||
"status": 200,
|
||||
"headers": {
|
||||
},
|
||||
"body": {
|
||||
"items": [
|
||||
{
|
||||
"content": "",
|
||||
"page_title": "Pointing on a Picture",
|
||||
"type": "problem",
|
||||
"id": "block-v1:edX+DemoX+Demo_Course+type@vertical+block@2152d4a4aadc4cb0af5256394a3d1fc7",
|
||||
"bookmarked": false,
|
||||
"path": "Example Week 1: Getting Started > Homework - Question Styles > Pointing on a Picture",
|
||||
"graded": true,
|
||||
"contains_content_type_gated_content": false,
|
||||
"href": ""
|
||||
}
|
||||
],
|
||||
"item_id": "block-v1:edX+DemoX+Demo_Course+type@sequential+block@basic_questions",
|
||||
"is_time_limited": false,
|
||||
"is_proctored": false,
|
||||
"position": null,
|
||||
"tag": "sequential",
|
||||
"banner_text": null,
|
||||
"save_position": false,
|
||||
"show_completion": false,
|
||||
"gated_content": {
|
||||
"prereq_id": null,
|
||||
"prereq_url": null,
|
||||
"prereq_section_name": null,
|
||||
"gated": false,
|
||||
"gated_section_name": "Homework - Question Styles"
|
||||
},
|
||||
"display_name": "Homework - Question Styles",
|
||||
"format": "Homework"
|
||||
},
|
||||
"matchingRules": {
|
||||
"$.body.items": {
|
||||
"min": 1
|
||||
},
|
||||
"$.body.items[*].*": {
|
||||
"match": "type"
|
||||
},
|
||||
"$.body.item_id": {
|
||||
"match": "type"
|
||||
},
|
||||
"$.body.is_time_limited": {
|
||||
"match": "type"
|
||||
},
|
||||
"$.body.is_proctored": {
|
||||
"match": "type"
|
||||
},
|
||||
"$.body.tag": {
|
||||
"match": "type"
|
||||
},
|
||||
"$.body.save_position": {
|
||||
"match": "type"
|
||||
},
|
||||
"$.body.show_completion": {
|
||||
"match": "type"
|
||||
},
|
||||
"$.body.gated_content": {
|
||||
"match": "type"
|
||||
},
|
||||
"$.body.display_name": {
|
||||
"match": "type"
|
||||
},
|
||||
"$.body.format": {
|
||||
"match": "type"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"description": "a request to set sequence position against activeUnitIndex",
|
||||
"providerState": "sequence position data exists for course_id course-v1:edX+DemoX+Demo_Course, sequence_id block-v1:edX+DemoX+Demo_Course+type@sequential+block@basic_questions and activeUnitIndex 0",
|
||||
"request": {
|
||||
"method": "POST",
|
||||
"path": "/courses/course-v1:edX+DemoX+Demo_Course/xblock/block-v1:edX+DemoX+Demo_Course+type@sequential+block@basic_questions/handler/goto_position",
|
||||
"body": {
|
||||
"position": 1
|
||||
}
|
||||
},
|
||||
"response": {
|
||||
"status": 200,
|
||||
"headers": {
|
||||
},
|
||||
"body": {
|
||||
"success": true
|
||||
},
|
||||
"matchingRules": {
|
||||
"$.body.success": {
|
||||
"match": "type"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"description": "a request to get completion block",
|
||||
"providerState": "completion block data exists for course_id course-v1:edX+DemoX+Demo_Course, sequence_id block-v1:edX+DemoX+Demo_Course+type@sequential+block@basic_questions and usageId block-v1:edX+DemoX+Demo_Course+type@vertical+block@47dbd5f836544e61877a483c0b75606c",
|
||||
"request": {
|
||||
"method": "POST",
|
||||
"path": "/courses/course-v1:edX+DemoX+Demo_Course/xblock/block-v1:edX+DemoX+Demo_Course+type@sequential+block@basic_questions/handler/get_completion",
|
||||
"body": {
|
||||
"usage_key": "block-v1:edX+DemoX+Demo_Course+type@vertical+block@47dbd5f836544e61877a483c0b75606c"
|
||||
}
|
||||
},
|
||||
"response": {
|
||||
"status": 200,
|
||||
"headers": {
|
||||
},
|
||||
"body": {
|
||||
"complete": true
|
||||
},
|
||||
"matchingRules": {
|
||||
"$.body.complete": {
|
||||
"match": "type"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
"metadata": {
|
||||
"pactSpecification": {
|
||||
"version": "2.0.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
437
src/courseware/data/pact-tests/lmsPact.test.jsx
Normal file
437
src/courseware/data/pact-tests/lmsPact.test.jsx
Normal file
@@ -0,0 +1,437 @@
|
||||
import { Pact, Matchers } from '@pact-foundation/pact';
|
||||
import path from 'path';
|
||||
import { mergeConfig, getConfig } from '@edx/frontend-platform';
|
||||
import { getAuthenticatedUser } from '@edx/frontend-platform/auth';
|
||||
|
||||
import {
|
||||
getCourseBlocks, getCourseMetadata, getSequenceMetadata, postSequencePosition, getBlockCompletion,
|
||||
} from '../api';
|
||||
import { initializeMockApp } from '../../../setupTest';
|
||||
|
||||
const {
|
||||
somethingLike: like, term, boolean, string, eachLike, integer,
|
||||
} = Matchers;
|
||||
const provider = new Pact({
|
||||
consumer: 'frontend-app-learning',
|
||||
provider: 'lms',
|
||||
log: path.resolve(process.cwd(), 'src/courseware/data/pact-tests/logs', 'pact.log'),
|
||||
dir: path.resolve(process.cwd(), 'src/courseware/data/pact-tests'),
|
||||
logLevel: 'DEBUG',
|
||||
cors: true,
|
||||
});
|
||||
|
||||
describe('Courseware Service', () => {
|
||||
const courseId = 'course-v1:edX+DemoX+Demo_Course';
|
||||
const sequenceId = 'block-v1:edX+DemoX+Demo_Course+type@sequential+block@basic_questions';
|
||||
const usageId = 'block-v1:edX+DemoX+Demo_Course+type@vertical+block@47dbd5f836544e61877a483c0b75606c';
|
||||
const dateRegex = '^(?:[1-9]\\d{3}-(?:(?:0[1-9]|1[0-2])-(?:0[1-9]|1\\d|2[0-8])|(?:0[13-9]|1[0-2])-(?:29|30)|(?:0[13578]|1[02])-31)|(?:[1-9]\\d(?:0[48]|[2468][048]|[13579][26])|(?:[2468][048]|[13579][26])00)-02-29)T(?:[01]\\d|2[0-3]):[0-5]\\d:[0-5]\\d(?:Z|[+-][01]\\d:[0-5]\\d)$';
|
||||
const opaqueKeysRegex = '[\\w\\-~.:]';
|
||||
let authenticatedUser;
|
||||
beforeAll(async () => {
|
||||
initializeMockApp();
|
||||
await provider
|
||||
.setup()
|
||||
.then((options) => mergeConfig({
|
||||
LMS_BASE_URL: `http://localhost:${options.port}`,
|
||||
}, 'Custom app config for pact tests'));
|
||||
authenticatedUser = getAuthenticatedUser();
|
||||
});
|
||||
|
||||
afterEach(() => provider.verify());
|
||||
afterAll(() => provider.finalize());
|
||||
|
||||
describe('When a request to get course blocks is made', () => {
|
||||
it('returns normalized course blocks', async () => {
|
||||
await provider.addInteraction({
|
||||
state: `Blocks data exists for course_id ${courseId}`,
|
||||
uponReceiving: 'a request to get course blocks',
|
||||
withRequest: {
|
||||
method: 'GET',
|
||||
path: '/api/courses/v2/blocks/',
|
||||
query: {
|
||||
course_id: courseId,
|
||||
username: authenticatedUser ? authenticatedUser.username : '',
|
||||
depth: '3',
|
||||
requested_fields: 'children,effort_activities,effort_time,show_gated_sections,graded,special_exam_info,has_scheduled_content',
|
||||
},
|
||||
},
|
||||
willRespondWith: {
|
||||
status: 200,
|
||||
body:
|
||||
{
|
||||
root: string('block-v1:edX+DemoX+Demo_Course+type@course+block@course'),
|
||||
blocks: like({
|
||||
'block-v1:edX+DemoX+Demo_Course+type@course+block@course': {
|
||||
id: 'block-v1:edX+DemoX+Demo_Course+type@course+block@course',
|
||||
block_id: 'course',
|
||||
lms_web_url: '/courses/course-v1:edX+DemoX+Demo_Course/jump_to/block-v1:edX+DemoX+Demo_Course+type@course+block@course',
|
||||
legacy_web_url: '/courses/course-v1:edX+DemoX+Demo_Course/jump_to/block-v1:edX+DemoX+Demo_Course+type@course+block@course?experience=legacy',
|
||||
student_view_url: '/xblock/block-v1:edX+DemoX+Demo_Course+type@course+block@course',
|
||||
type: 'course',
|
||||
display_name: 'Demonstration Course',
|
||||
},
|
||||
}),
|
||||
},
|
||||
},
|
||||
});
|
||||
const normalizedCourseBlock = {
|
||||
'block-v1:edX+DemoX+Demo_Course+type@course+block@course': {
|
||||
id: 'course-v1:edX+DemoX+Demo_Course',
|
||||
title: 'Demonstration Course',
|
||||
sectionIds: [],
|
||||
hasScheduledContent: false,
|
||||
},
|
||||
};
|
||||
const response = await getCourseBlocks(courseId);
|
||||
expect(response).toBeTruthy();
|
||||
expect(response.courses).toEqual(normalizedCourseBlock);
|
||||
expect(response.sections).toEqual({});
|
||||
expect(response.sequences).toEqual({});
|
||||
expect(response.units).toEqual({});
|
||||
});
|
||||
});
|
||||
|
||||
describe('When a request to get course metadata is made', () => {
|
||||
it('returns normalized course metadata', async () => {
|
||||
await provider.addInteraction({
|
||||
state: `course metadata exists for course_id ${courseId}`,
|
||||
uponReceiving: 'a request to get course metadata',
|
||||
withRequest: {
|
||||
method: 'GET',
|
||||
path: `/api/courseware/course/${courseId}`,
|
||||
query: {
|
||||
browser_timezone: Intl.DateTimeFormat().resolvedOptions().timeZone,
|
||||
},
|
||||
},
|
||||
willRespondWith: {
|
||||
status: 200,
|
||||
body:
|
||||
{
|
||||
access_expiration: {
|
||||
expiration_date: term({
|
||||
generate: '2013-02-05T05:00:00Z',
|
||||
matcher: dateRegex,
|
||||
}),
|
||||
masquerading_expired_course: boolean(false),
|
||||
upgrade_deadline: term({
|
||||
generate: '2013-02-05T05:00:00Z',
|
||||
matcher: dateRegex,
|
||||
}),
|
||||
upgrade_url: string('link'),
|
||||
},
|
||||
can_show_upgrade_sock: boolean(false),
|
||||
content_type_gating_enabled: boolean(false),
|
||||
end: term({
|
||||
generate: '2013-02-05T05:00:00Z',
|
||||
matcher: dateRegex,
|
||||
}),
|
||||
enrollment: {
|
||||
mode: term({
|
||||
generate: 'audit',
|
||||
matcher: '^(audit|verified)$',
|
||||
}),
|
||||
is_active: boolean(true),
|
||||
},
|
||||
enrollment_start: term({
|
||||
generate: '2013-02-05T05:00:00Z',
|
||||
matcher: dateRegex,
|
||||
}),
|
||||
enrollment_end: term({
|
||||
generate: '2013-02-05T05:00:00Z',
|
||||
matcher: dateRegex,
|
||||
}),
|
||||
id: term({
|
||||
generate: 'course-v1:edX+DemoX+Demo_Course',
|
||||
matcher: opaqueKeysRegex,
|
||||
}),
|
||||
license: string('all-rights-reserved'),
|
||||
name: like('Demonstration Course'),
|
||||
number: like('DemoX'),
|
||||
offer: {
|
||||
code: string('code'),
|
||||
expiration_date: term({
|
||||
generate: '2013-02-05T05:00:00Z',
|
||||
matcher: dateRegex,
|
||||
}),
|
||||
original_price: string('$99'),
|
||||
discounted_price: string('$99'),
|
||||
percentage: integer(50),
|
||||
upgrade_url: string('url'),
|
||||
},
|
||||
org: like('edX'),
|
||||
related_programs: null,
|
||||
short_description: like(''),
|
||||
start: term({
|
||||
generate: '2013-02-05T05:00:00Z',
|
||||
matcher: dateRegex,
|
||||
}),
|
||||
tabs: eachLike({
|
||||
title: 'Course', slug: 'courseware', priority: 0, type: 'courseware', url: `${getConfig().BASE_URL}/course/course-v1:edX+DemoX+Demo_Course/home`,
|
||||
}),
|
||||
user_timezone: null,
|
||||
verified_mode: like({
|
||||
access_expiration_date: null,
|
||||
currency: 'USD',
|
||||
currency_symbol: '$',
|
||||
price: 149,
|
||||
sku: '8CF08E5',
|
||||
upgrade_url: `${getConfig().ECOMMERCE_BASE_URL}/basket/add/?sku=8CF08E5`,
|
||||
}),
|
||||
show_calculator: boolean(false),
|
||||
original_user_is_staff: boolean(true),
|
||||
can_view_legacy_courseware: boolean(true),
|
||||
is_staff: boolean(true),
|
||||
course_access: like({
|
||||
has_access: boolean(true),
|
||||
error_code: null,
|
||||
developer_message: null,
|
||||
user_message: null,
|
||||
additional_context_user_message: null,
|
||||
user_fragment: null,
|
||||
}),
|
||||
notes: { enabled: boolean(false), visible: boolean(true) },
|
||||
marketing_url: null,
|
||||
celebrations: {
|
||||
irst_section: boolean(false),
|
||||
streak_length_to_celebrate: null,
|
||||
streak_discount_experiment_enabled: boolean(false),
|
||||
},
|
||||
user_has_passing_grade: boolean(false),
|
||||
course_exit_page_is_active: boolean(false),
|
||||
certificate_data: {
|
||||
cert_status: string('audit_passing'), cert_web_view_url: null, download_url: null, certificate_available_date: null,
|
||||
},
|
||||
verify_identity_url: null,
|
||||
verification_status: string('none'),
|
||||
linkedin_add_to_profile_url: null,
|
||||
is_mfe_special_exams_enabled: boolean(false),
|
||||
is_mfe_proctored_exams_enabled: boolean(false),
|
||||
user_needs_integrity_signature: boolean(false),
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const normalizedCourseMetadata = {
|
||||
accessExpiration: {
|
||||
expirationDate: '2013-02-05T05:00:00Z',
|
||||
masqueradingExpiredCourse: false,
|
||||
upgradeDeadline: '2013-02-05T05:00:00Z',
|
||||
upgradeUrl: 'link',
|
||||
},
|
||||
canShowUpgradeSock: false,
|
||||
contentTypeGatingEnabled: false,
|
||||
id: 'course-v1:edX+DemoX+Demo_Course',
|
||||
title: 'Demonstration Course',
|
||||
number: 'DemoX',
|
||||
offer: {
|
||||
code: 'code',
|
||||
discountedPrice: '$99',
|
||||
expirationDate: '2013-02-05T05:00:00Z',
|
||||
originalPrice: '$99',
|
||||
percentage: 50,
|
||||
upgradeUrl: 'url',
|
||||
},
|
||||
org: 'edX',
|
||||
enrollmentStart: '2013-02-05T05:00:00Z',
|
||||
enrollmentEnd: '2013-02-05T05:00:00Z',
|
||||
end: '2013-02-05T05:00:00Z',
|
||||
start: '2013-02-05T05:00:00Z',
|
||||
enrollmentMode: 'audit',
|
||||
isEnrolled: true,
|
||||
courseAccess: {
|
||||
hasAccess: true,
|
||||
errorCode: null,
|
||||
developerMessage: null,
|
||||
userMessage: null,
|
||||
additionalContextUserMessage: null,
|
||||
userFragment: null,
|
||||
},
|
||||
canViewLegacyCourseware: true,
|
||||
originalUserIsStaff: true,
|
||||
isStaff: true,
|
||||
license: 'all-rights-reserved',
|
||||
verifiedMode: {
|
||||
accessExpirationDate: null,
|
||||
currency: 'USD',
|
||||
currencySymbol: '$',
|
||||
price: 149,
|
||||
sku: '8CF08E5',
|
||||
upgradeUrl: `${getConfig().ECOMMERCE_BASE_URL}/basket/add/?sku=8CF08E5`,
|
||||
},
|
||||
tabs: [
|
||||
{
|
||||
title: 'Course',
|
||||
slug: 'courseware',
|
||||
priority: 0,
|
||||
type: 'courseware',
|
||||
url: `${getConfig().BASE_URL}/course/course-v1:edX+DemoX+Demo_Course/home`,
|
||||
},
|
||||
],
|
||||
userTimezone: null,
|
||||
showCalculator: false,
|
||||
notes: { enabled: false, visible: true },
|
||||
marketingUrl: null,
|
||||
celebrations: {
|
||||
irstSection: false,
|
||||
streakLengthToCelebrate: null,
|
||||
streakDiscountExperimentEnabled: false,
|
||||
},
|
||||
userHasPassingGrade: false,
|
||||
courseExitPageIsActive: false,
|
||||
certificateData: {
|
||||
certStatus: 'audit_passing',
|
||||
certWebViewUrl: null,
|
||||
downloadUrl: null,
|
||||
certificateAvailableDate: null,
|
||||
},
|
||||
timeOffsetMillis: 0,
|
||||
verifyIdentityUrl: null,
|
||||
verificationStatus: 'none',
|
||||
linkedinAddToProfileUrl: null,
|
||||
relatedPrograms: null,
|
||||
userNeedsIntegritySignature: false,
|
||||
specialExamsEnabledWaffleFlag: false,
|
||||
proctoredExamsEnabledWaffleFlag: false,
|
||||
isMasquerading: false,
|
||||
};
|
||||
const response = await getCourseMetadata(courseId);
|
||||
expect(response).toBeTruthy();
|
||||
expect(response).toEqual(normalizedCourseMetadata);
|
||||
});
|
||||
});
|
||||
|
||||
describe('When a request to get sequence metadata is made', () => {
|
||||
it('returns normalized sequence metadata ', async () => {
|
||||
await provider.addInteraction({
|
||||
state: `sequence metadata data exists for sequence_id ${sequenceId}`,
|
||||
uponReceiving: 'a request to get sequence metadata',
|
||||
withRequest: {
|
||||
method: 'GET',
|
||||
path: `/api/courseware/sequence/${sequenceId}`,
|
||||
},
|
||||
willRespondWith: {
|
||||
status: 200,
|
||||
body:
|
||||
{
|
||||
items: eachLike({
|
||||
content: '',
|
||||
page_title: 'Pointing on a Picture',
|
||||
type: 'problem',
|
||||
id: 'block-v1:edX+DemoX+Demo_Course+type@vertical+block@2152d4a4aadc4cb0af5256394a3d1fc7',
|
||||
bookmarked: false,
|
||||
path: 'Example Week 1: Getting Started > Homework - Question Styles > Pointing on a Picture',
|
||||
graded: true,
|
||||
contains_content_type_gated_content: false,
|
||||
href: '',
|
||||
}),
|
||||
item_id: string('block-v1:edX+DemoX+Demo_Course+type@sequential+block@basic_questions'),
|
||||
is_time_limited: boolean(false),
|
||||
is_proctored: boolean(false),
|
||||
position: null,
|
||||
tag: boolean('sequential'),
|
||||
banner_text: null,
|
||||
save_position: boolean(false),
|
||||
show_completion: boolean(false),
|
||||
gated_content: like({
|
||||
prereq_id: null,
|
||||
prereq_url: null,
|
||||
prereq_section_name: null,
|
||||
gated: false,
|
||||
gated_section_name: 'Homework - Question Styles',
|
||||
}),
|
||||
display_name: boolean('Homework - Question Styles'),
|
||||
format: boolean('Homework'),
|
||||
},
|
||||
},
|
||||
});
|
||||
const normalizedSequenceMetadata = {
|
||||
sequence: {
|
||||
id: 'block-v1:edX+DemoX+Demo_Course+type@sequential+block@basic_questions',
|
||||
blockType: 'sequential',
|
||||
unitIds: [
|
||||
'block-v1:edX+DemoX+Demo_Course+type@vertical+block@2152d4a4aadc4cb0af5256394a3d1fc7',
|
||||
],
|
||||
bannerText: null,
|
||||
format: 'Homework',
|
||||
title: 'Homework - Question Styles',
|
||||
gatedContent: {
|
||||
prereqId: null,
|
||||
prereqUrl: null,
|
||||
prereqSectionName: null,
|
||||
gated: false,
|
||||
gatedSectionName: 'Homework - Question Styles',
|
||||
},
|
||||
isTimeLimited: false,
|
||||
isProctored: false,
|
||||
activeUnitIndex: 0,
|
||||
saveUnitPosition: false,
|
||||
showCompletion: false,
|
||||
allowProctoringOptOut: undefined,
|
||||
},
|
||||
units: [
|
||||
{
|
||||
id: 'block-v1:edX+DemoX+Demo_Course+type@vertical+block@2152d4a4aadc4cb0af5256394a3d1fc7',
|
||||
sequenceId: 'block-v1:edX+DemoX+Demo_Course+type@sequential+block@basic_questions',
|
||||
bookmarked: false,
|
||||
complete: undefined,
|
||||
title: 'Pointing on a Picture',
|
||||
contentType: 'problem',
|
||||
graded: true,
|
||||
containsContentTypeGatedContent: false,
|
||||
},
|
||||
],
|
||||
};
|
||||
const response = await getSequenceMetadata(sequenceId);
|
||||
expect(response).toBeTruthy();
|
||||
expect(response).toEqual(normalizedSequenceMetadata);
|
||||
});
|
||||
});
|
||||
|
||||
describe('When a request to set sequence position against Unit Index is made', () => {
|
||||
it('returns if the request was success or failure', async () => {
|
||||
await provider.addInteraction({
|
||||
state: `sequence position data exists for course_id ${courseId}, sequence_id ${sequenceId} and activeUnitIndex 0`,
|
||||
uponReceiving: 'a request to set sequence position against activeUnitIndex',
|
||||
withRequest: {
|
||||
method: 'POST',
|
||||
path: `/courses/${courseId}/xblock/${sequenceId}/handler/goto_position`,
|
||||
body: { position: 1 }, // Position is 1-indexed on the provider side and 0-indexed in the consumer side.
|
||||
},
|
||||
willRespondWith: {
|
||||
status: 200,
|
||||
body:
|
||||
{
|
||||
success: boolean(true),
|
||||
},
|
||||
},
|
||||
});
|
||||
const response = await postSequencePosition(courseId, sequenceId, 0);
|
||||
expect(response).toBeTruthy();
|
||||
expect(response).toEqual({ success: true });
|
||||
});
|
||||
});
|
||||
|
||||
describe('When a request to get completion block is made', () => {
|
||||
it('returns the completion status', async () => {
|
||||
await provider.addInteraction({
|
||||
state: `completion block data exists for course_id ${courseId}, sequence_id ${sequenceId} and usageId ${usageId}`,
|
||||
uponReceiving: 'a request to get completion block',
|
||||
withRequest: {
|
||||
method: 'POST',
|
||||
path: `/courses/${courseId}/xblock/${sequenceId}/handler/get_completion`,
|
||||
body: { usage_key: usageId },
|
||||
},
|
||||
willRespondWith: {
|
||||
status: 200,
|
||||
body:
|
||||
{
|
||||
complete: boolean(true),
|
||||
},
|
||||
},
|
||||
});
|
||||
const response = await getBlockCompletion(courseId, sequenceId, usageId);
|
||||
expect(response).toBeTruthy();
|
||||
expect(response).toEqual(true);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -32,6 +32,8 @@
|
||||
"learning.outline.alert.cert.when": "This course ends on {courseEndDateFormatted}. Final grades and certificates are\n scheduled to be available after {certificateAvailableDate}.",
|
||||
"cert.alert.earned.unavailable.header": "Your grade and certificate will be ready soon!",
|
||||
"cert.alert.earned.ready.header": "Congratulations! Your certificate is ready.",
|
||||
"cert.alert.notPassing.header": "You are not eligible for a certificate",
|
||||
"cert.alert.notPassing.button": "View grades",
|
||||
"learning.outline.alert.end.short": "ينتهي هذا المساق في غضون {timeRemaining}في {courseEndTime}.",
|
||||
"learning.outline.alert.end.long": "يبدأ المساق في {timeRemaining} بتاريخ {courseStartDate}.",
|
||||
"learning.outline.alert.start.short": "يبدأ المساق في غضون {timeRemaining} في {courseStartDate}.",
|
||||
|
||||
@@ -32,6 +32,8 @@
|
||||
"learning.outline.alert.cert.when": "This course ends on {courseEndDateFormatted}. Final grades and certificates are\n scheduled to be available after {certificateAvailableDate}.",
|
||||
"cert.alert.earned.unavailable.header": "Your grade and certificate will be ready soon!",
|
||||
"cert.alert.earned.ready.header": "Congratulations! Your certificate is ready.",
|
||||
"cert.alert.notPassing.header": "You are not eligible for a certificate",
|
||||
"cert.alert.notPassing.button": "View grades",
|
||||
"learning.outline.alert.end.short": "Este curso acaba en {timeRemaining} a la/s {courseEndTime}.",
|
||||
"learning.outline.alert.end.long": "El curso comienza en {timeRemaining} el {courseStartDate}.",
|
||||
"learning.outline.alert.start.short": "El curso comienza en {timeRemaining} a la/s {courseStartTime}.",
|
||||
|
||||
@@ -32,6 +32,8 @@
|
||||
"learning.outline.alert.cert.when": "This course ends on {courseEndDateFormatted}. Final grades and certificates are\n scheduled to be available after {certificateAvailableDate}.",
|
||||
"cert.alert.earned.unavailable.header": "Your grade and certificate will be ready soon!",
|
||||
"cert.alert.earned.ready.header": "Congratulations! Your certificate is ready.",
|
||||
"cert.alert.notPassing.header": "You are not eligible for a certificate",
|
||||
"cert.alert.notPassing.button": "View grades",
|
||||
"learning.outline.alert.end.short": "This course is ending {timeRemaining} at {courseEndTime}.",
|
||||
"learning.outline.alert.end.long": "Course starts {timeRemaining} on {courseStartDate}.",
|
||||
"learning.outline.alert.start.short": "Course starts {timeRemaining} at {courseStartTime}.",
|
||||
|
||||
@@ -32,6 +32,8 @@
|
||||
"learning.outline.alert.cert.when": "This course ends on {courseEndDateFormatted}. Final grades and certificates are\n scheduled to be available after {certificateAvailableDate}.",
|
||||
"cert.alert.earned.unavailable.header": "Your grade and certificate will be ready soon!",
|
||||
"cert.alert.earned.ready.header": "Congratulations! Your certificate is ready.",
|
||||
"cert.alert.notPassing.header": "You are not eligible for a certificate",
|
||||
"cert.alert.notPassing.button": "View grades",
|
||||
"learning.outline.alert.end.short": "This course is ending {timeRemaining} at {courseEndTime}.",
|
||||
"learning.outline.alert.end.long": "Course starts {timeRemaining} on {courseStartDate}.",
|
||||
"learning.outline.alert.start.short": "Course starts {timeRemaining} at {courseStartTime}.",
|
||||
|
||||
Reference in New Issue
Block a user