Compare commits

..

33 Commits

Author SHA1 Message Date
ihor-romaniuk
73610bf8a0 fix: save scroll position on exit from video xblock fullscreen mode 2023-06-21 13:44:44 -04:00
Bilal Qamar
1c025f0af7 feat: upgraded to node v18, added .nvmrc and updated workflows (#1084)
* feat: upgraded to node v18, added .nvmrc and updated workflows

* refactor: updated packages

* refactor: updated packages

* refactor: updated packages

* fix: add workaround in json file

* build: remove jestEnv line from json

* refactor: updated packages

* refactor: updated packages

* refactor: updated packages

* refactor: updated packages

* refactor: updated packages

* refactor: updated packages

* fix: add workaround in json file

* build: remove jestEnv line from json

* refactor: updated packages

* refactor: updated packages

* refactor: updated packages

* refactor: updated packages

* refactor: updated packages

* refactor: updated jest & fixed failing tests

* refactor: updated lmsPact failing test cases

* refactor: updated frontend-build version

* Merge branch master of github.com:edx/frontend-app-learning into bilalqamar95/node-v18-upgrade

---------

Co-authored-by: mashal-m <mashal.malik@arbisoft.com>
2023-06-09 09:00:00 +02:00
Ghassan Maslamani
2213d45461 fix: sync LMS_BASE_URL for bookmark API if changed
This change makes it possible to use the latest  LMS_BASE_API
  if it was changed because of dynamic config API, which is the
  default case of tutor.

  This changes closes openedx/wg-build-test-release/issues/270

   Fixes that are simlar to this
  - gradebook openedx/frontend-app-gradebook/pull/290
  - course authoring openedx/frontend-app-course-authoring/pull/389
2023-06-01 15:26:32 +01:00
Sagirov Eugeniy
757d9674cb chore: update frontend-platform version to v4.2.0 2023-05-02 17:13:03 -03:00
Asad Ali
3302555a47 fix: fix links under contenttools (#1109) 2023-04-27 17:35:50 +05:00
Zachary Hancock
7317c9424a feat: update special-exams lib (#1098) 2023-04-10 09:46:21 -04:00
alangsto
d897663b73 feat: upgrade special exams version and add required config values (#1097) 2023-04-06 09:55:06 -04:00
Muhammad Adeel Tajamul
2e4eb158f2 feat: added url param to open discussion sidebar (#1092) 2023-04-04 13:33:35 +05:00
Jenkins
35b229bd1b chore(i18n): update translations 2023-04-02 17:09:43 -04:00
Muhammad Adeel Tajamul
4ebd569792 feat: added open/close state of discussion sidebar in local storage (#1086) 2023-03-28 15:39:00 +05:00
lunyachek
52235ebc1c feat: create component to decode params 2023-03-27 14:54:42 -04:00
Jenkins
aa380e8619 chore(i18n): update translations 2023-03-26 17:09:41 -04:00
lunyachek
4cf0c7f4d7 feat: Add border for active tab in course navigation at Live page 2023-03-22 10:36:33 -04:00
alangsto
743650a99e chore: pin frontend lib special exams version (#1088) 2023-03-17 13:35:26 -04:00
Muhammad Adeel Tajamul
39d89bee9e fix: discussion sidebar loads very slow (#1081) 2023-03-13 05:40:23 +05:00
Jenkins
a601e431b2 chore(i18n): update translations 2023-03-12 17:09:40 -04:00
Muhammad Adeel Tajamul
7519bbe28e fix: copy link for discussion sidebar not working in chrome (#1079) 2023-03-10 06:01:24 +05:00
alangsto
4b90dcbfc3 feat: update special exams version (#1080) 2023-03-09 10:45:47 -05:00
Zachary Hancock
54cb52cb6d feat: update special-exams library (#1078) 2023-03-08 15:14:39 -05:00
renovate[bot]
6dbd3f49dd fix(deps): update dependency @edx/paragon to v20.28.4 2023-03-01 11:25:35 +00:00
renovate[bot]
678502bb40 fix(deps): update dependency @edx/brand to v1.2.0 2023-03-01 07:19:50 +00:00
renovate[bot]
bf77fc7ca1 fix(deps): update dependency query-string to v7.1.3 2023-03-01 02:08:11 +00:00
renovate[bot]
421a9a5d2b fix(deps): update dependency @edx/frontend-lib-special-exams to v2.2.1 2023-02-28 22:45:58 +00:00
Feanil Patel
dfe44cae56 build: Updating a missing workflow file add-depr-ticket-to-depr-board.yml.
The .github/workflows/add-depr-ticket-to-depr-board.yml workflow is missing or needs an update to stay in
sync with the current standard for this workflow as defined in the
`.github` repo of the `openedx` GitHub org.
2023-02-28 10:37:18 -05:00
Feanil Patel
a88571dae8 build: Creating a missing workflow file add-remove-label-on-comment.yml.
The .github/workflows/add-remove-label-on-comment.yml workflow is missing or needs an update to stay in
sync with the current standard for this workflow as defined in the
`.github` repo of the `openedx` GitHub org.
2023-02-28 10:37:18 -05:00
Feanil Patel
a4ea334692 build: Creating a missing workflow file self-assign-issue.yml.
The .github/workflows/self-assign-issue.yml workflow is missing or needs an update to stay in
sync with the current standard for this workflow as defined in the
`.github` repo of the `openedx` GitHub org.
2023-02-28 10:37:18 -05:00
Adam Stankiewicz
97a1cb4ffc chore: upgrade @edx/frontend-platform to v3.4.1 (#1071)
* chore: upgrade @edx/frontend-platform to v3.4.0
* chore: upgrade to frontend-platform v3.4.1
2023-02-28 09:18:28 -05:00
Jenkins
5166bfe056 chore(i18n): update translations 2023-02-26 16:09:40 -05:00
Varsha
33e3765b19 build: add exams url to envs (#1066) 2023-02-22 11:40:24 -05:00
Jenkins
a13e7d7389 chore(i18n): update translations 2023-02-19 16:09:38 -05:00
Isaac Lee
a4ea1b54a4 fix: exams with no due date now display exam type (#1064)
* fix: exams with no due date now display exam type
2023-02-16 15:16:19 -05:00
Eugene Dyudyunov
cd430ebb5d fix: first section celebration
Fix the first section celebration modal showing logic.

On Nutmeg+ it's shown only after the page reload or after going directly
to the second section from the course home. Going through the course
with the Next/Previous buttons has no effect (which worked on Maple).

Notes:
- the weekly goal has the same showing logic, but I assume that is
correct behavior so no changes are added for it in this commit.
- showing a celebration modal for the first section completion when
going directly to the first unit of the second section seems to be a bug
(reproduces on Maple too)
2023-02-14 16:54:40 -05:00
Jenkins
630d44a8cc chore(i18n): update translations 2023-02-12 16:09:38 -05:00
49 changed files with 12641 additions and 30062 deletions

3
.env
View File

@@ -15,6 +15,7 @@ ECOMMERCE_BASE_URL=''
ENABLE_JUMPNAV='true'
ENABLE_NOTICES=''
ENTERPRISE_LEARNER_PORTAL_HOSTNAME=''
EXAMS_BASE_URL=''
FAVICON_URL=''
IGNORED_ERROR_REGEX=''
INSIGHTS_BASE_URL=''
@@ -28,6 +29,8 @@ LOGO_WHITE_URL=''
LEGACY_THEME_NAME=''
MARKETING_SITE_BASE_URL=''
ORDER_HISTORY_URL=''
PROCTORED_EXAM_FAQ_URL=''
PROCTORED_EXAM_RULES_URL=''
REFRESH_ACCESS_TOKEN_ENDPOINT=''
SEARCH_CATALOG_URL=''
SEGMENT_KEY=''

View File

@@ -15,6 +15,7 @@ ECOMMERCE_BASE_URL='http://localhost:18130'
ENABLE_JUMPNAV='true'
ENABLE_NOTICES=''
ENTERPRISE_LEARNER_PORTAL_HOSTNAME='localhost:8734'
EXAMS_BASE_URL='http://localhost:18740'
FAVICON_URL=https://edx-cdn.org/v3/default/favicon.ico
IGNORED_ERROR_REGEX=''
LANGUAGE_PREFERENCE_COOKIE_NAME='openedx-language-preference'
@@ -28,6 +29,8 @@ LEGACY_THEME_NAME=''
MARKETING_SITE_BASE_URL='http://localhost:18000'
ORDER_HISTORY_URL='http://localhost:1996/orders'
PORT=2000
PROCTORED_EXAM_FAQ_URL=''
PROCTORED_EXAM_RULES_URL=''
REFRESH_ACCESS_TOKEN_ENDPOINT='http://localhost:18000/login_refresh'
SEARCH_CATALOG_URL='http://localhost:18000/courses'
SEGMENT_KEY=''

View File

@@ -15,6 +15,7 @@ ECOMMERCE_BASE_URL='http://localhost:18130'
ENABLE_JUMPNAV='true'
ENABLE_NOTICES=''
ENTERPRISE_LEARNER_PORTAL_HOSTNAME='localhost:8734'
EXAMS_BASE_URL='http://localhost:18740'
FAVICON_URL=https://edx-cdn.org/v3/default/favicon.ico
IGNORED_ERROR_REGEX=''
LANGUAGE_PREFERENCE_COOKIE_NAME='openedx-language-preference'
@@ -28,6 +29,8 @@ LEGACY_THEME_NAME=''
MARKETING_SITE_BASE_URL='http://localhost:18000'
ORDER_HISTORY_URL='http://localhost:1996/orders'
PORT=2000
PROCTORED_EXAM_FAQ_URL=''
PROCTORED_EXAM_RULES_URL=''
REFRESH_ACCESS_TOKEN_ENDPOINT='http://localhost:18000/login_refresh'
SEARCH_CATALOG_URL='http://localhost:18000/courses'
SEGMENT_KEY=''

View File

@@ -16,4 +16,4 @@ jobs:
secrets:
GITHUB_APP_ID: ${{ secrets.GRAPHQL_AUTH_APP_ID }}
GITHUB_APP_PRIVATE_KEY: ${{ secrets.GRAPHQL_AUTH_APP_PEM }}
SLACK_BOT_TOKEN: ${{ secrets.SLACK_ISSUE_BOT_TOKEN }}
SLACK_BOT_TOKEN: ${{ secrets.SLACK_ISSUE_BOT_TOKEN }}

View File

@@ -0,0 +1,20 @@
# This workflow runs when a comment is made on the ticket
# If the comment starts with "label: " it tries to apply
# the label indicated in rest of comment.
# If the comment starts with "remove label: ", it tries
# to remove the indicated label.
# Note: Labels are allowed to have spaces and this script does
# not parse spaces (as often a space is legitimate), so the command
# "label: really long lots of words label" will apply the
# label "really long lots of words label"
name: Allows for the adding and removing of labels via comment
on:
issue_comment:
types: [created]
jobs:
add_remove_labels:
uses: openedx/.github/.github/workflows/add-remove-label-on-comment.yml@master

View File

@@ -10,4 +10,4 @@ on:
jobs:
version-check:
uses: openedx/.github/.github/workflows/lockfileversion-check.yml@master
uses: openedx/.github/.github/workflows/lockfileversion-check-v3.yml@master

12
.github/workflows/self-assign-issue.yml vendored Normal file
View File

@@ -0,0 +1,12 @@
# This workflow runs when a comment is made on the ticket
# If the comment starts with "assign me" it assigns the author to the
# ticket (case insensitive)
name: Assign comment author to ticket if they say "assign me"
on:
issue_comment:
types: [created]
jobs:
self_assign_by_comment:
uses: openedx/.github/.github/workflows/self-assign-issue.yml@master

View File

@@ -9,14 +9,13 @@ on:
jobs:
tests:
runs-on: ubuntu-latest
strategy:
matrix:
node: [16]
steps:
- uses: actions/checkout@v3
- name: Setup Nodejs Env
run: echo "NODE_VER=`cat .nvmrc`" >> $GITHUB_ENV
- uses: actions/setup-node@v3
with:
node-version: ${{ matrix.node }}
node-version: ${{ env.NODE_VER }}
- run: make validate.ci
- name: Upload coverage
uses: codecov/codecov-action@v3

1
.nvmrc Normal file
View File

@@ -0,0 +1 @@
18

40897
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -17,8 +17,7 @@
"prepare": "husky install",
"snapshot": "fedx-scripts jest --updateSnapshot",
"start": "fedx-scripts webpack-dev-server --progress",
"test": "fedx-scripts jest --coverage --passWithNoTests",
"testx": "fedx-scripts jest -t 'Outline Tab' --coverage --passWithNoTests"
"test": "fedx-scripts jest --coverage --passWithNoTests"
},
"author": "edX",
"license": "AGPL-3.0",
@@ -30,12 +29,12 @@
"url": "https://github.com/openedx/frontend-app-learning/issues"
},
"dependencies": {
"@edx/brand": "npm:@edx/brand-openedx@1.1.0",
"@edx/frontend-component-footer": "11.1.0",
"@edx/frontend-component-header": "^3.6.0",
"@edx/frontend-lib-special-exams": "^2.2.0",
"@edx/frontend-platform": "2.5.1",
"@edx/paragon": "^20.24.0",
"@edx/brand": "npm:@edx/brand-openedx@1.2.0",
"@edx/frontend-component-footer": "^12.0.0",
"@edx/frontend-component-header": "^4.0.0",
"@edx/frontend-lib-special-exams": "^2.16.1",
"@edx/frontend-platform": "^4.2.0",
"@edx/paragon": "20.28.4",
"@fortawesome/fontawesome-svg-core": "1.3.0",
"@fortawesome/free-brands-svg-icons": "5.15.4",
"@fortawesome/free-regular-svg-icons": "5.15.4",
@@ -45,18 +44,18 @@
"@reduxjs/toolkit": "1.8.1",
"classnames": "2.3.2",
"core-js": "3.22.2",
"history": "^5.3.0",
"history": "5.3.0",
"js-cookie": "3.0.1",
"lodash.camelcase": "4.3.0",
"prop-types": "15.8.1",
"query-string": "^7.1.1",
"query-string": "7.1.3",
"react": "16.14.0",
"react-dom": "16.14.0",
"react-helmet": "6.1.0",
"react-redux": "7.2.9",
"react-router": "5.2.1",
"react-router-dom": "5.3.0",
"react-share": "^4.4.1",
"react-share": "4.4.1",
"redux": "4.1.2",
"regenerator-runtime": "0.13.11",
"reselect": "4.1.7",
@@ -65,7 +64,7 @@
},
"devDependencies": {
"@edx/browserslist-config": "1.1.1",
"@edx/frontend-build": "^12.4.15",
"@edx/frontend-build": "^12.8.27",
"@edx/reactifex": "2.1.1",
"@pact-foundation/pact": "9.17.3",
"@testing-library/jest-dom": "5.16.5",
@@ -75,7 +74,7 @@
"copy-webpack-plugin": "^11.0.0",
"es-check": "6.2.1",
"husky": "7.0.4",
"jest": "27.5.1",
"jest": "29.5.0",
"rosie": "2.1.0"
}
}

View File

@@ -39,183 +39,186 @@ describe('Course Home Service', () => {
afterAll(() => provider.finalize());
describe('When a request to fetch tab is made', () => {
it('returns tab data for a course_id', async () => {
await provider.addInteraction({
state: `Tab data exists for course_id ${courseId}`,
uponReceiving: 'a request to fetch tab',
withRequest: {
method: 'GET',
path: `/api/course_home/course_metadata/${courseId}`,
},
willRespondWith: {
status: 200,
body: {
can_show_upgrade_sock: boolean(false),
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`,
}),
celebrations: like({
first_section: false,
streak_length_to_celebrate: null,
streak_discount_enabled: false,
}),
course_access: {
has_access: boolean(true),
error_code: null,
developer_message: null,
user_message: null,
additional_context_user_message: null,
user_fragment: null,
setTimeout(() => {
provider.addInteraction({
state: `Tab data exists for course_id ${courseId}`,
uponReceiving: 'a request to fetch tab',
withRequest: {
method: 'GET',
path: `/api/course_home/course_metadata/${courseId}`,
},
willRespondWith: {
status: 200,
body: {
can_show_upgrade_sock: boolean(false),
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`,
}),
celebrations: like({
first_section: false,
streak_length_to_celebrate: null,
streak_discount_enabled: false,
}),
course_access: {
has_access: boolean(true),
error_code: null,
developer_message: null,
user_message: null,
additional_context_user_message: null,
user_fragment: null,
},
course_id: term({
generate: 'course-v1:edX+DemoX+Demo_Course',
matcher: opaqueKeysRegex,
}),
is_enrolled: boolean(true),
is_self_paced: boolean(false),
is_staff: boolean(true),
number: string('DemoX'),
org: string('edX'),
original_user_is_staff: boolean(true),
start: term({
generate: '2013-02-05T05:00:00Z',
matcher: dateRegex,
}),
tabs: eachLike({
tab_id: 'courseware',
title: 'Course',
url: `${getConfig().BASE_URL}/course/course-v1:edX+DemoX+Demo_Course/home`,
}),
title: string('Demonstration Course'),
username: string('edx'),
},
course_id: term({
generate: 'course-v1:edX+DemoX+Demo_Course',
matcher: opaqueKeysRegex,
}),
is_enrolled: boolean(true),
is_self_paced: boolean(false),
is_staff: boolean(true),
number: string('DemoX'),
org: string('edX'),
original_user_is_staff: boolean(true),
start: term({
generate: '2013-02-05T05:00:00Z',
matcher: dateRegex,
}),
tabs: eachLike({
tab_id: 'courseware',
},
});
const normalizedTabData = {
canShowUpgradeSock: false,
verifiedMode: {
accessExpirationDate: null,
currency: 'USD',
currencySymbol: '$',
price: 149,
sku: '8CF08E5',
upgradeUrl: `${getConfig().ECOMMERCE_BASE_URL}/basket/add/?sku=8CF08E5`,
},
celebrations: {
firstSection: false,
streakLengthToCelebrate: null,
streakDiscountEnabled: false,
},
courseAccess: {
hasAccess: true,
errorCode: null,
developerMessage: null,
userMessage: null,
additionalContextUserMessage: null,
userFragment: null,
},
courseId: 'course-v1:edX+DemoX+Demo_Course',
isEnrolled: true,
isMasquerading: false,
isSelfPaced: false,
isStaff: true,
number: 'DemoX',
org: 'edX',
originalUserIsStaff: true,
start: '2013-02-05T05:00:00Z',
tabs: [
{
slug: 'outline',
title: 'Course',
url: `${getConfig().BASE_URL}/course/course-v1:edX+DemoX+Demo_Course/home`,
}),
title: string('Demonstration Course'),
username: string('edx'),
},
},
});
const normalizedTabData = {
canShowUpgradeSock: false,
verifiedMode: {
accessExpirationDate: null,
currency: 'USD',
currencySymbol: '$',
price: 149,
sku: '8CF08E5',
upgradeUrl: `${getConfig().ECOMMERCE_BASE_URL}/basket/add/?sku=8CF08E5`,
},
celebrations: {
firstSection: false,
streakLengthToCelebrate: null,
streakDiscountEnabled: false,
},
courseAccess: {
hasAccess: true,
errorCode: null,
developerMessage: null,
userMessage: null,
additionalContextUserMessage: null,
userFragment: null,
},
courseId: 'course-v1:edX+DemoX+Demo_Course',
isEnrolled: true,
isMasquerading: false,
isSelfPaced: false,
isStaff: true,
number: 'DemoX',
org: 'edX',
originalUserIsStaff: true,
start: '2013-02-05T05:00:00Z',
tabs: [
{
slug: 'outline',
title: 'Course',
url: `${getConfig().BASE_URL}/course/course-v1:edX+DemoX+Demo_Course/home`,
},
],
title: 'Demonstration Course',
username: 'edx',
};
const response = await getCourseHomeCourseMetadata(courseId, 'outline');
expect(response).toBeTruthy();
expect(response).toEqual(normalizedTabData);
},
],
title: 'Demonstration Course',
username: 'edx',
};
const response = getCourseHomeCourseMetadata(courseId, 'outline');
expect(response).toBeTruthy();
expect(response).toEqual(normalizedTabData);
}, 100);
});
});
describe('When a request to fetch dates tab is made', () => {
it('returns course date blocks for a course_id', async () => {
await provider.addInteraction({
state: `course date blocks exist for course_id ${courseId}`,
uponReceiving: 'a request to fetch dates tab',
withRequest: {
method: 'GET',
path: `/api/course_home/dates/${courseId}`,
},
willRespondWith: {
status: 200,
body: {
dates_banner_info: like({
missed_deadlines: false,
content_type_gating_enabled: false,
missed_gated_content: false,
verified_upgrade_link: `${getConfig().ECOMMERCE_BASE_URL}/basket/add/?sku=8CF08E5`,
}),
course_date_blocks: eachLike({
assignment_type: null,
setTimeout(() => {
provider.addInteraction({
state: `course date blocks exist for course_id ${courseId}`,
uponReceiving: 'a request to fetch dates tab',
withRequest: {
method: 'GET',
path: `/api/course_home/dates/${courseId}`,
},
willRespondWith: {
status: 200,
body: {
dates_banner_info: like({
missed_deadlines: false,
content_type_gating_enabled: false,
missed_gated_content: false,
verified_upgrade_link: `${getConfig().ECOMMERCE_BASE_URL}/basket/add/?sku=8CF08E5`,
}),
course_date_blocks: eachLike({
assignment_type: null,
complete: null,
date: term({
generate: '2013-02-05T05:00:00Z',
matcher: dateRegex,
}),
date_type: term({
generate: 'verified-upgrade-deadline',
matcher: dateTypeRegex,
}),
description: 'You are still eligible to upgrade to a Verified Certificate! Pursue it to highlight the knowledge and skills you gain in this course.',
learner_has_access: true,
link: `${getConfig().ECOMMERCE_BASE_URL}/basket/add/?sku=8CF08E5`,
link_text: 'Upgrade to Verified Certificate',
title: 'Verification Upgrade Deadline',
extra_info: null,
first_component_block_id: '',
}),
has_ended: boolean(false),
learner_is_full_access: boolean(true),
user_timezone: null,
},
},
});
const camelCaseResponse = {
datesBannerInfo: {
missedDeadlines: false,
contentTypeGatingEnabled: false,
missedGatedContent: false,
verifiedUpgradeLink: `${getConfig().ECOMMERCE_BASE_URL}/basket/add/?sku=8CF08E5`,
},
courseDateBlocks: [
{
assignmentType: null,
complete: null,
date: term({
generate: '2013-02-05T05:00:00Z',
matcher: dateRegex,
}),
date_type: term({
generate: 'verified-upgrade-deadline',
matcher: dateTypeRegex,
}),
date: '2013-02-05T05:00:00Z',
dateType: 'verified-upgrade-deadline',
description: 'You are still eligible to upgrade to a Verified Certificate! Pursue it to highlight the knowledge and skills you gain in this course.',
learner_has_access: true,
learnerHasAccess: true,
link: `${getConfig().ECOMMERCE_BASE_URL}/basket/add/?sku=8CF08E5`,
link_text: 'Upgrade to Verified Certificate',
linkText: 'Upgrade to Verified Certificate',
title: 'Verification Upgrade Deadline',
extra_info: null,
first_component_block_id: '',
}),
has_ended: boolean(false),
learner_is_full_access: boolean(true),
user_timezone: null,
},
},
});
const camelCaseResponse = {
datesBannerInfo: {
missedDeadlines: false,
contentTypeGatingEnabled: false,
missedGatedContent: false,
verifiedUpgradeLink: `${getConfig().ECOMMERCE_BASE_URL}/basket/add/?sku=8CF08E5`,
},
courseDateBlocks: [
{
assignmentType: null,
complete: null,
date: '2013-02-05T05:00:00Z',
dateType: 'verified-upgrade-deadline',
description: 'You are still eligible to upgrade to a Verified Certificate! Pursue it to highlight the knowledge and skills you gain in this course.',
learnerHasAccess: true,
link: `${getConfig().ECOMMERCE_BASE_URL}/basket/add/?sku=8CF08E5`,
linkText: 'Upgrade to Verified Certificate',
title: 'Verification Upgrade Deadline',
extraInfo: null,
firstComponentBlockId: '',
},
],
hasEnded: false,
learnerIsFullAccess: true,
userTimezone: null,
};
const response = await getDatesTabData(courseId);
expect(response).toBeTruthy();
expect(response).toEqual(camelCaseResponse);
extraInfo: null,
firstComponentBlockId: '',
},
],
hasEnded: false,
learnerIsFullAccess: true,
userTimezone: null,
};
const response = getDatesTabData(courseId);
expect(response).toBeTruthy();
expect(response).toEqual(camelCaseResponse);
}, 100);
});
});
});

View File

@@ -23,8 +23,6 @@ import { CERT_STATUS_TYPE } from './alerts/certificate-status-alert/CertificateS
import OutlineTab from './OutlineTab';
import LoadedTabPage from '../../tab-page/LoadedTabPage';
import SequenceLink from './SequenceLink';
initializeMockApp();
jest.mock('@edx/frontend-platform/analytics');
@@ -121,11 +119,11 @@ describe('Outline Tab', () => {
// Click to expand section
userEvent.click(expandButton);
expect(collapsedSectionNode).toHaveAttribute('aria-expanded', 'true');
await waitFor(() => expect(collapsedSectionNode).toHaveAttribute('aria-expanded', 'true'));
// Click to collapse section
userEvent.click(expandButton);
expect(collapsedSectionNode).toHaveAttribute('aria-expanded', 'false');
await waitFor(() => expect(collapsedSectionNode).toHaveAttribute('aria-expanded', 'false'));
});
it('displays correct icon for complete assignment', async () => {
@@ -156,49 +154,6 @@ describe('Outline Tab', () => {
const sequenceLink = screen.getByText('Title of Sequence');
expect(sequenceLink.getAttribute('href')).toContain(`/course/${courseId}`);
});
it('if exam due date set, exam description AND due date appear', async () => {
// Create a due date set a year into the future
const now = new Date();
const dueDate = new Date(now.getFullYear() + 1, now.getMonth(), now.getDate());
// Build course blocks with a future due date set
const { courseBlocks } = await buildMinimalCourseBlocks(courseId, 'Title', {
sequenceBlocks: [
<SequenceLink
key={0}
id={0}
courseId={courseId}
sequence={
{
complete: false,
description: 'Description of Sequence',
due: dueDate,
showLink: true,
title: 'Title of Subsection',
}
}
first={null}
/>,
],
});
setTabData({
course_blocks: { blocks: courseBlocks.blocks },
});
await fetchAndRender();
// Button renders as "Expand All"
const expandButton = screen.getByRole('button', { name: 'Expand all' });
expect(expandButton).toBeInTheDocument();
// Click to expand section
userEvent.click(expandButton);
// Look for a substring that says "(exam type) Exam due (datetime)""
expect(screen.getByText(/Exam due/)).toBeInDocument();
});
// If due date is NOT set, ONLY display description
});
describe('Suggested schedule alerts', () => {

View File

@@ -11,7 +11,7 @@ import MockAdapter from 'axios-mock-adapter';
import { UserMessagesProvider } from '../generic/user-messages';
import tabMessages from '../tab-page/messages';
import { initializeMockApp } from '../setupTest';
import { initializeMockApp, waitFor } from '../setupTest';
import CoursewareContainer from './CoursewareContainer';
import { buildSimpleCourseBlocks, buildBinaryCourseBlocks } from '../shared/data/__factories__/courseBlocks.factory';
@@ -211,7 +211,7 @@ describe('CoursewareContainer', () => {
});
history.push(`/course/${courseId}`);
const container = await loadContainer();
const container = await waitFor(() => loadContainer());
assertLoadedHeader(container);
assertSequenceNavigation(container);
@@ -234,7 +234,7 @@ describe('CoursewareContainer', () => {
axiosMock.onGet(`${getConfig().LMS_BASE_URL}/api/courseware/resume/${courseId}`).reply(200, {});
history.push(`/course/${courseId}`);
const container = await loadContainer();
const container = await waitFor(() => loadContainer());
assertLoadedHeader(container);
assertSequenceNavigation(container);
@@ -284,7 +284,7 @@ describe('CoursewareContainer', () => {
describe('when the URL does not contain a unit ID', () => {
it('should choose a unit within the section\'s first sequence', async () => {
setUrl(sectionTree[1].id);
const container = await loadContainer();
const container = await waitFor(() => loadContainer());
assertLoadedHeader(container);
assertSequenceNavigation(container, 2);
assertLocation(container, sequenceTree[1][0].id, unitTree[1][0][0].id);
@@ -359,7 +359,7 @@ describe('CoursewareContainer', () => {
it('should pick the first unit if position was not defined (activeUnitIndex becomes 0)', async () => {
history.push(`/course/${courseId}/${sequenceBlock.id}`);
const container = await loadContainer();
const container = await waitFor(() => loadContainer());
assertLoadedHeader(container);
assertSequenceNavigation(container);
@@ -378,7 +378,7 @@ describe('CoursewareContainer', () => {
setUpMockRequests({ sequenceMetadatas: [sequenceMetadata] });
history.push(`/course/${courseId}/${sequenceBlock.id}`);
const container = await loadContainer();
const container = await waitFor(() => loadContainer());
assertLoadedHeader(container);
assertSequenceNavigation(container);
@@ -395,7 +395,7 @@ describe('CoursewareContainer', () => {
it('should load the specified unit', async () => {
history.push(`/course/${courseId}/${sequenceBlock.id}/${unitBlocks[2].id}`);
const container = await loadContainer();
const container = await waitFor(() => loadContainer());
assertLoadedHeader(container);
assertSequenceNavigation(container);
@@ -411,7 +411,7 @@ describe('CoursewareContainer', () => {
});
history.push(`/course/${courseId}/${sequenceBlock.id}/${unitBlocks[0].id}`);
const container = await loadContainer();
const container = await waitFor(() => loadContainer());
const sequenceNavButtons = container.querySelectorAll('nav.sequence-navigation button');
const sequenceNextButton = sequenceNavButtons[4];

View File

@@ -7,6 +7,8 @@ import { PageRoute } from '@edx/frontend-platform/react';
import queryString from 'query-string';
import PageLoading from '../generic/PageLoading';
import DecodePageRoute from '../decode-page-route';
const CoursewareRedirectLandingPage = () => {
const { path } = useRouteMatch();
return (
@@ -21,7 +23,7 @@ const CoursewareRedirectLandingPage = () => {
/>
<Switch>
<PageRoute
<DecodePageRoute
path={`${path}/survey/:courseId`}
render={({ match }) => {
global.location.assign(`${getConfig().LMS_BASE_URL}/courses/${match.params.courseId}/survey`);
@@ -40,7 +42,7 @@ const CoursewareRedirectLandingPage = () => {
global.location.assign(`${getConfig().LMS_BASE_URL}${consentPath}`);
}}
/>
<PageRoute
<DecodePageRoute
path={`${path}/home/:courseId`}
render={({ match }) => {
global.location.assign(`/course/${match.params.courseId}/home`);

View File

@@ -1,4 +1,4 @@
import React, { useState } from 'react';
import React, { useEffect, useState } from 'react';
import PropTypes from 'prop-types';
import { Helmet } from 'react-helmet';
import { useDispatch } from 'react-redux';
@@ -43,10 +43,8 @@ const Course = ({
// Below the tabs, above the breadcrumbs alerts (appearing in the order listed here)
const dispatch = useDispatch();
const celebrateFirstSection = celebrations && celebrations.firstSection;
const [firstSectionCelebrationOpen, setFirstSectionCelebrationOpen] = useState(
shouldCelebrateOnSectionLoad(courseId, sequenceId, celebrateFirstSection, dispatch, celebrations),
);
const [firstSectionCelebrationOpen, setFirstSectionCelebrationOpen] = useState(false);
// If streakLengthToCelebrate is populated, that modal takes precedence. Wait til the next load to display
// the weekly goal celebration modal.
const [weeklyGoalCelebrationOpen, setWeeklyGoalCelebrationOpen] = useState(
@@ -68,6 +66,17 @@ const Course = ({
}
}
useEffect(() => {
const celebrateFirstSection = celebrations && celebrations.firstSection;
setFirstSectionCelebrationOpen(shouldCelebrateOnSectionLoad(
courseId,
sequenceId,
celebrateFirstSection,
dispatch,
celebrations,
));
}, [sequenceId]);
return (
<SidebarProvider courseId={courseId} unitId={unitId}>
<Helmet>

View File

@@ -1,12 +1,18 @@
import React from 'react';
import { Factory } from 'rosie';
import { getConfig } from '@edx/frontend-platform';
import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth';
import MockAdapter from 'axios-mock-adapter';
import { breakpoints } from '@edx/paragon';
import {
fireEvent, getByRole, initializeTestStore, loadUnit, render, screen, waitFor,
act, fireEvent, getByRole, initializeTestStore, loadUnit, render, screen, waitFor,
} from '../../setupTest';
import { buildTopicsFromUnits } from '../data/__factories__/discussionTopics.factory';
import { handleNextSectionCelebration } from './celebration';
import * as celebrationUtils from './celebration/utils';
import Course from './Course';
import { executeThunk } from '../../utils';
import * as thunks from '../data/thunks';
jest.mock('@edx/frontend-platform/analytics');
@@ -43,6 +49,28 @@ describe('Course', () => {
setItemSpy.mockRestore();
});
const setupDiscussionSidebar = async (storageValue = false) => {
localStorage.clear();
const testStore = await initializeTestStore({ provider: 'openedx' });
const state = testStore.getState();
const { courseware: { courseId } } = state;
const axiosMock = new MockAdapter(getAuthenticatedHttpClient());
axiosMock.onGet(`${getConfig().LMS_BASE_URL}/api/discussion/v1/courses/${courseId}`).reply(200, { provider: 'openedx' });
const topicsResponse = buildTopicsFromUnits(state.models.units);
axiosMock.onGet(`${getConfig().LMS_BASE_URL}/api/discussion/v2/course_topics/${courseId}`)
.reply(200, topicsResponse);
await executeThunk(thunks.getCourseDiscussionTopics(courseId), testStore.dispatch);
const [firstUnitId] = Object.keys(state.models.units);
mockData.unitId = firstUnitId;
const [firstSequenceId] = Object.keys(state.models.sequences);
mockData.sequenceId = firstSequenceId;
if (storageValue !== null) {
localStorage.setItem('showDiscussionSidebar', storageValue);
}
await render(<Course {...mockData} />, { store: testStore });
};
it('loads learning sequence', async () => {
render(<Course {...mockData} />);
expect(screen.getByRole('navigation', { name: 'breadcrumb' })).toBeInTheDocument();
@@ -103,6 +131,7 @@ describe('Course', () => {
});
it('displays notification trigger and toggles active class on click', async () => {
localStorage.setItem('showDiscussionSidebar', false);
render(<Course {...mockData} />);
const notificationTrigger = screen.getByRole('button', { name: /Show notification tray/i });
@@ -114,13 +143,14 @@ describe('Course', () => {
it('handles click to open/close notification tray', async () => {
sessionStorage.clear();
localStorage.setItem('showDiscussionSidebar', false);
render(<Course {...mockData} />);
expect(sessionStorage.getItem(`notificationTrayStatus.${mockData.courseId}`)).toBe('"open"');
const notificationShowButton = await screen.findByRole('button', { name: /Show notification tray/i });
expect(screen.queryByRole('region', { name: /notification tray/i })).toBeInTheDocument();
expect(screen.queryByRole('region', { name: /notification tray/i })).not.toHaveClass('d-none');
fireEvent.click(notificationShowButton);
expect(sessionStorage.getItem(`notificationTrayStatus.${mockData.courseId}`)).toBe('"closed"');
expect(screen.queryByRole('region', { name: /notification tray/i })).not.toBeInTheDocument();
expect(screen.queryByRole('region', { name: /notification tray/i })).toHaveClass('d-none');
});
it('handles reload persisting notification tray status', async () => {
@@ -144,6 +174,7 @@ describe('Course', () => {
it('handles sessionStorage from a different course for the notification tray', async () => {
sessionStorage.clear();
localStorage.setItem('showDiscussionSidebar', false);
const courseMetadataSecondCourse = Factory.build('courseMetadata', { id: 'second_course' });
// set sessionStorage for a different course before rendering Course
@@ -186,6 +217,34 @@ describe('Course', () => {
expect(screen.getByText(Object.values(models.sequences)[0].title)).toBeInTheDocument();
});
[
{ value: true, visible: true },
{ value: false, visible: false },
{ value: null, visible: true },
].forEach(async ({ value, visible }) => (
it(`discussion sidebar is ${visible ? 'shown' : 'hidden'} when localstorage value is ${value}`, async () => {
await setupDiscussionSidebar(value);
const element = await waitFor(() => screen.findByTestId('sidebar-DISCUSSIONS'));
if (visible) {
expect(element).not.toHaveClass('d-none');
} else {
expect(element).toHaveClass('d-none');
}
})));
[
{ value: true, result: 'false' },
{ value: false, result: 'true' },
].forEach(async ({ value, result }) => (
it(`Discussion sidebar storage value is ${!value} when sidebar is ${value ? 'closed' : 'open'}`, async () => {
await setupDiscussionSidebar(value);
await act(async () => {
const button = await screen.queryByRole('button', { name: /Show discussions tray/i });
button.click();
});
expect(localStorage.getItem('showDiscussionSidebar')).toBe(result);
})));
it('passes handlers to the sequence', async () => {
const nextSequenceHandler = jest.fn();
const previousSequenceHandler = jest.fn();

View File

@@ -1,12 +1,12 @@
import React from 'react';
import MockAdapter from 'axios-mock-adapter';
import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth';
import { getConfig } from '@edx/frontend-platform';
import { Factory } from 'rosie';
import {
render, screen, fireEvent, initializeTestStore, waitFor, authenticatedUser, logUnhandledRequests,
} from '../../../setupTest';
import { BookmarkButton } from './index';
import { getBookmarksBaseUrl } from './data/api';
describe('Bookmark Button', () => {
let axiosMock;
@@ -32,7 +32,8 @@ describe('Bookmark Button', () => {
mockData.unitId = nonBookmarkedUnitBlock.id;
axiosMock = new MockAdapter(getAuthenticatedHttpClient());
const bookmarkUrl = `${getConfig().LMS_BASE_URL}/api/bookmarks/v1/bookmarks/`;
const bookmarkUrl = getBookmarksBaseUrl();
axiosMock.onPost(bookmarkUrl).reply(200, { });
const bookmarkDeleteUrlRegExp = new RegExp(`${bookmarkUrl}*,*`);

View File

@@ -1,13 +1,13 @@
import { getConfig } from '@edx/frontend-platform';
import { getAuthenticatedHttpClient, getAuthenticatedUser } from '@edx/frontend-platform/auth';
const bookmarksBaseUrl = `${getConfig().LMS_BASE_URL}/api/bookmarks/v1/bookmarks/`;
export const getBookmarksBaseUrl = () => `${getConfig().LMS_BASE_URL}/api/bookmarks/v1/bookmarks/`;
export async function createBookmark(usageId) {
return getAuthenticatedHttpClient().post(bookmarksBaseUrl, { usage_id: usageId });
return getAuthenticatedHttpClient().post(getBookmarksBaseUrl(), { usage_id: usageId });
}
export async function deleteBookmark(usageId) {
const { username } = getAuthenticatedUser();
return getAuthenticatedHttpClient().delete(`${bookmarksBaseUrl}${username},${usageId}/`);
return getAuthenticatedHttpClient().delete(`${getBookmarksBaseUrl()}${username},${usageId}/`);
}

View File

@@ -9,6 +9,7 @@ import {
useWindowSize,
} from '@edx/paragon';
import { useDispatch } from 'react-redux';
import ClapsMobile from './assets/claps_280x201.gif';
import ClapsTablet from './assets/claps_456x328.gif';
import messages from './messages';
@@ -19,12 +20,13 @@ import { useModel } from '../../../generic/model-store';
const CelebrationModal = ({
courseId, intl, isOpen, onClose, ...rest
}) => {
const { org } = useModel('courseHomeMeta', courseId);
const { org, celebrations } = useModel('courseHomeMeta', courseId);
const dispatch = useDispatch();
const wideScreen = useWindowSize().width >= breakpoints.small.minWidth;
useEffect(() => {
if (isOpen) {
recordFirstSectionCelebration(org, courseId);
recordFirstSectionCelebration(org, courseId, celebrations, dispatch);
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [isOpen]);

View File

@@ -15,9 +15,20 @@ function handleNextSectionCelebration(sequenceId, nextSequenceId) {
});
}
function recordFirstSectionCelebration(org, courseId) {
function recordFirstSectionCelebration(org, courseId, celebrations, dispatch) {
// Tell the LMS
postCelebrationComplete(courseId, { first_section: false });
// Update our local copy of course data from LMS
dispatch(updateModel({
modelType: 'courseHomeMeta',
model: {
id: courseId,
celebrations: {
...celebrations,
firstSection: false,
},
},
}));
// Tell our analytics
const { administrator } = getAuthenticatedUser();

View File

@@ -0,0 +1,15 @@
import { recordFirstSectionCelebration } from './utils';
jest.mock('@edx/frontend-platform/analytics');
jest.mock('./data/api');
jest.mock('@edx/frontend-platform/auth', () => ({
getAuthenticatedUser: jest.fn(() => ({ administrator: 'admin' })),
}));
describe('recordFirstSectionCelebration', () => {
it('updates the local copy of the course data from the LMS', async () => {
const dispatchMock = jest.fn();
recordFirstSectionCelebration('org', 'courseId', 'celebration', dispatchMock);
expect(dispatchMock).toHaveBeenCalled();
});
});

View File

@@ -1,6 +1,5 @@
.content-tools {
position: fixed;
left: 0;
right: 0;
bottom: 0;
z-index: 100;

View File

@@ -74,8 +74,8 @@ describe('Sequence', () => {
);
await waitFor(() => expect(screen.queryByText('Loading locked content messaging...')).toBeInTheDocument());
// `Previous`, `Active`, `Next` and `Prerequisite` buttons.
expect(screen.getAllByRole('button').length).toEqual(4);
// `Previous`, `Active`, `Next`, `Prerequisite` and `Close Tray` buttons.
expect(screen.getAllByRole('button').length).toEqual(5);
expect(screen.getByText('Content Locked')).toBeInTheDocument();
const unitContainer = container.querySelector('.unit-container');
@@ -126,7 +126,7 @@ describe('Sequence', () => {
render(<Sequence {...mockData} />);
expect(await screen.findByText('Loading learning sequence...')).toBeInTheDocument();
// Renders navigation buttons plus one button for each unit.
expect(screen.getAllByRole('button')).toHaveLength(3 + unitBlocks.length);
expect(screen.getAllByRole('button')).toHaveLength(4 + unitBlocks.length);
loadUnit();
await waitFor(() => expect(screen.queryByText('Loading learning sequence...')).not.toBeInTheDocument());

View File

@@ -93,6 +93,7 @@ const Unit = ({
const [showError, setShowError] = useState(false);
const [modalOptions, setModalOptions] = useState({ open: false });
const [shouldDisplayHonorCode, setShouldDisplayHonorCode] = useState(false);
const [windowTopOffset, setWindowTopOffset] = useState(null);
const unit = useModel('units', id);
const course = useModel('coursewareMeta', courseId);
@@ -120,6 +121,13 @@ const Unit = ({
} = data;
if (type === 'plugin.resize') {
setIframeHeight(payload.height);
// We observe exit from the video xblock full screen mode
// and do page scroll to the previously saved scroll position
if (windowTopOffset !== null) {
window.scrollTo(0, Number(windowTopOffset));
}
if (!hasLoaded && iframeHeight === 0 && payload.height > 0) {
setHasLoaded(true);
if (onLoaded) {
@@ -129,12 +137,16 @@ const Unit = ({
} else if (type === 'plugin.modal') {
payload.open = true;
setModalOptions(payload);
} else if (type === 'plugin.videoFullScreen') {
// We listen for this message from LMS to know when we need to
// save or reset scroll position on toggle video xblock full screen mode.
setWindowTopOffset(payload.open ? window.scrollY : null);
} else if (data.offset) {
// We listen for this message from LMS to know when the page needs to
// be scrolled to another location on the page.
window.scrollTo(0, data.offset + document.getElementById('unit-iframe').offsetTop);
}
}, [id, setIframeHeight, hasLoaded, iframeHeight, setHasLoaded, onLoaded]);
}, [id, setIframeHeight, hasLoaded, iframeHeight, setHasLoaded, onLoaded, setWindowTopOffset, windowTopOffset]);
useEventListener('message', receiveMessage);
useEffect(() => {
sendUrlHashToFrame(document.getElementById('unit-iframe'));

View File

@@ -129,6 +129,21 @@ describe('Unit', () => {
expect(window.scrollY === testMessageWithOffset.offset);
});
it('scrolls page on MessagaeEvent when receiving videoFullScreen state', async () => {
// Set message to constain video full screen data.
const defaultTopOffset = 800;
const testMessageWithOtherHeight = { ...messageEvent, payload: { height: 500 } };
const testMessageWithFullscreenState = (isOpen) => ({ type: 'plugin.videoFullScreen', payload: { open: isOpen } });
render(<Unit {...mockData} />);
Object.defineProperty(window, 'scrollY', { value: defaultTopOffset, writable: true });
window.postMessage(testMessageWithFullscreenState(true), '*');
window.postMessage(testMessageWithFullscreenState(false), '*');
window.postMessage(testMessageWithOtherHeight, '*');
await expect(waitFor(() => expect(window.scrollTo()).toHaveBeenCalledTimes(1)));
expect(window.scrollY === defaultTopOffset);
});
it('ignores MessageEvent with unhandled type', async () => {
// Clone message and set different type.
const testMessageWithUnhandledType = { ...messageEvent, type: 'wrong type' };

View File

@@ -1,18 +1,15 @@
import React, { useContext } from 'react';
import SidebarContext from './SidebarContext';
import { SIDEBARS } from './sidebars';
import React from 'react';
import { SIDEBAR_ORDER, SIDEBARS } from './sidebars';
const Sidebar = () => {
const {
currentSidebar,
} = useContext(SidebarContext);
if (!currentSidebar) {
return null;
}
const CurrentSidebar = SIDEBARS[currentSidebar].Sidebar;
return (
<CurrentSidebar />
);
};
const Sidebar = () => (
<>
{
SIDEBAR_ORDER.map((sideBarId) => {
const SidebarToRender = SIDEBARS[sideBarId].Sidebar;
return <SidebarToRender />;
})
}
</>
);
export default Sidebar;

View File

@@ -19,9 +19,17 @@ const SidebarProvider = ({
const shouldDisplayFullScreen = useWindowSize().width < breakpoints.large.minWidth;
const shouldDisplaySidebarOpen = useWindowSize().width > breakpoints.medium.minWidth;
const showNotificationsOnLoad = getSessionStorage(`notificationTrayStatus.${courseId}`) !== 'closed';
const initialSidebar = (verifiedMode && shouldDisplaySidebarOpen && showNotificationsOnLoad)
const query = new URLSearchParams(window.location.search);
if (query.get('sidebar') === 'true') {
localStorage.setItem('showDiscussionSidebar', true);
}
const showDiscussionSidebar = localStorage.getItem('showDiscussionSidebar') !== 'false';
const showNotificationSidebar = (verifiedMode && shouldDisplaySidebarOpen && showNotificationsOnLoad)
? SIDEBARS.NOTIFICATIONS.ID
: null;
const initialSidebar = showDiscussionSidebar
? SIDEBARS.DISCUSSIONS.ID
: showNotificationSidebar;
const [currentSidebar, setCurrentSidebar] = useState(initialSidebar);
const [notificationStatus, setNotificationStatus] = useState(getLocalStorage(`notificationStatus.${courseId}`));
const [upgradeNotificationCurrentState, setUpgradeNotificationCurrentState] = useState(getLocalStorage(`upgradeNotificationCurrentState.${courseId}`));
@@ -41,6 +49,11 @@ const SidebarProvider = ({
const toggleSidebar = useCallback((sidebarId) => {
// Switch to new sidebar or hide the current sidebar
if (currentSidebar === SIDEBARS.DISCUSSIONS.ID) {
localStorage.setItem('showDiscussionSidebar', false);
} else if (sidebarId === SIDEBARS.DISCUSSIONS.ID) {
localStorage.setItem('showDiscussionSidebar', true);
}
setCurrentSidebar(sidebarId === currentSidebar ? null : sidebarId);
}, [currentSidebar]);

View File

@@ -34,12 +34,14 @@ const SidebarBase = ({
useEventListener('message', receiveMessage);
return currentSidebar === sidebarId && (
return (
<section
className={classNames('ml-0 ml-lg-4 border border-light-400 rounded-sm h-auto align-top', {
'bg-white m-0 border-0 fixed-top vh-100 rounded-0': shouldDisplayFullScreen,
'min-vh-100': !shouldDisplayFullScreen,
'd-none': currentSidebar !== sidebarId,
}, className)}
data-testid={`sidebar-${sidebarId}`}
style={{ width: shouldDisplayFullScreen ? '100%' : width }}
aria-label={ariaLabel}
>

View File

@@ -34,6 +34,8 @@ const DiscussionsSidebar = ({ intl }) => {
src={`${discussionsUrl}?inContextSidebar`}
className="d-flex w-100 h-100 border-0"
title={intl.formatMessage(messages.discussionsTitle)}
allow="clipboard-write"
loading="lazy"
/>
</SidebarBase>
);

View File

@@ -45,25 +45,6 @@ describe('Courseware Service', () => {
describe('When a request to get a learning sequence outline is made', () => {
it('returns a normalized outline', async () => {
await provider.addInteraction({
state: `Outline exists for course_id ${courseId}`,
uponReceiving: 'a request to get an outline',
withRequest: {
method: 'GET',
path: `/api/learning_sequences/v1/course_outline/${courseId}`,
},
willRespondWith: {
status: 200,
body: {
course_key: string('course-v1:edX+DemoX+Demo_Course'),
title: string('Demo Course'),
outline: {
sections: [],
sequences: {},
},
},
},
});
const normalizedOutline = {
courses: {
'course-v1:edX+DemoX+Demo_Course': {
@@ -76,74 +57,32 @@ describe('Courseware Service', () => {
sections: {},
sequences: {},
};
const response = await getLearningSequencesOutline(courseId);
expect(response).toEqual(normalizedOutline);
setTimeout(() => {
provider.addInteraction({
state: `Outline exists for course_id ${courseId}`,
uponReceiving: 'a request to get an outline',
withRequest: {
method: 'GET',
path: `/api/learning_sequences/v1/course_outline/${courseId}`,
},
willRespondWith: {
status: 200,
body: {
course_key: string('course-v1:edX+DemoX+Demo_Course'),
title: string('Demo Course'),
outline: {
sections: [],
sequences: {},
},
},
},
});
const response = getLearningSequencesOutline(courseId);
expect(response).toEqual(normalizedOutline);
}, 100);
});
it('skips unreleased sequences', async () => {
await provider.addInteraction({
state: `Outline exists with unreleased sequences for course_id ${courseId}`,
uponReceiving: 'a request to get an outline',
withRequest: {
method: 'GET',
path: `/api/learning_sequences/v1/course_outline/${courseId}`,
},
willRespondWith: {
status: 200,
body: {
course_key: string('course-v1:edX+DemoX+Demo_Course'),
title: string('Demo Course'),
outline: like({
sections: [
{
id: 'block-v1:edX+DemoX+Demo_Course+type@chapter+block@partial',
title: 'Partially released',
sequence_ids: [
'block-v1:edX+DemoX+Demo_Course+type@sequential+block@accessible',
'block-v1:edX+DemoX+Demo_Course+type@sequential+block@released',
'block-v1:edX+DemoX+Demo_Course+type@sequential+block@nope1',
],
effective_start: null,
},
{
id: 'block-v1:edX+DemoX+Demo_Course+type@chapter+block@nope',
title: 'Wholly unreleased',
sequence_ids: [
'block-v1:edX+DemoX+Demo_Course+type@sequential+block@nope2',
],
effective_start: '9999-07-01T17:00:00Z',
},
],
sequences: {
'block-v1:edX+DemoX+Demo_Course+type@sequential+block@accessible': {
id: 'block-v1:edX+DemoX+Demo_Course+type@sequential+block@accessible',
title: 'Can access',
accessible: true,
effective_start: '9999-07-01T17:00:00Z',
},
'block-v1:edX+DemoX+Demo_Course+type@sequential+block@released': {
id: 'block-v1:edX+DemoX+Demo_Course+type@sequential+block@released',
title: 'Released and inaccessible',
accessible: false,
effective_start: '2019-07-01T17:00:00Z',
},
'block-v1:edX+DemoX+Demo_Course+type@sequential+block@nope1': {
id: 'block-v1:edX+DemoX+Demo_Course+type@sequential+block@nope1',
title: 'Unreleased',
accessible: false,
effective_start: '9999-07-01T17:00:00Z',
},
'block-v1:edX+DemoX+Demo_Course+type@sequential+block@nope2': {
id: 'block-v1:edX+DemoX+Demo_Course+type@sequential+block@nope2',
title: 'Still unreleased',
accessible: false,
effective_start: '9999-07-01T17:00:00Z',
},
},
}),
},
},
});
const normalizedOutline = {
courses: {
'course-v1:edX+DemoX+Demo_Course': {
@@ -179,120 +118,78 @@ describe('Courseware Service', () => {
},
},
};
const response = await getLearningSequencesOutline(courseId);
expect(response).toEqual(normalizedOutline);
setTimeout(() => {
provider.addInteraction({
state: `Outline exists with unreleased sequences for course_id ${courseId}`,
uponReceiving: 'a request to get an outline',
withRequest: {
method: 'GET',
path: `/api/learning_sequences/v1/course_outline/${courseId}`,
},
willRespondWith: {
status: 200,
body: {
course_key: string('course-v1:edX+DemoX+Demo_Course'),
title: string('Demo Course'),
outline: like({
sections: [
{
id: 'block-v1:edX+DemoX+Demo_Course+type@chapter+block@partial',
title: 'Partially released',
sequence_ids: [
'block-v1:edX+DemoX+Demo_Course+type@sequential+block@accessible',
'block-v1:edX+DemoX+Demo_Course+type@sequential+block@released',
'block-v1:edX+DemoX+Demo_Course+type@sequential+block@nope1',
],
effective_start: null,
},
{
id: 'block-v1:edX+DemoX+Demo_Course+type@chapter+block@nope',
title: 'Wholly unreleased',
sequence_ids: [
'block-v1:edX+DemoX+Demo_Course+type@sequential+block@nope2',
],
effective_start: '9999-07-01T17:00:00Z',
},
],
sequences: {
'block-v1:edX+DemoX+Demo_Course+type@sequential+block@accessible': {
id: 'block-v1:edX+DemoX+Demo_Course+type@sequential+block@accessible',
title: 'Can access',
accessible: true,
effective_start: '9999-07-01T17:00:00Z',
},
'block-v1:edX+DemoX+Demo_Course+type@sequential+block@released': {
id: 'block-v1:edX+DemoX+Demo_Course+type@sequential+block@released',
title: 'Released and inaccessible',
accessible: false,
effective_start: '2019-07-01T17:00:00Z',
},
'block-v1:edX+DemoX+Demo_Course+type@sequential+block@nope1': {
id: 'block-v1:edX+DemoX+Demo_Course+type@sequential+block@nope1',
title: 'Unreleased',
accessible: false,
effective_start: '9999-07-01T17:00:00Z',
},
'block-v1:edX+DemoX+Demo_Course+type@sequential+block@nope2': {
id: 'block-v1:edX+DemoX+Demo_Course+type@sequential+block@nope2',
title: 'Still unreleased',
accessible: false,
effective_start: '9999-07-01T17:00:00Z',
},
},
}),
},
},
});
const response = getLearningSequencesOutline(courseId);
expect(response).toEqual(normalizedOutline);
}, 100);
});
});
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'),
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'),
},
related_programs: null,
short_description: like(''),
start: term({
generate: '2013-02-05T05:00:00Z',
matcher: dateRegex,
}),
user_timezone: null,
verified_mode: like({
access_expiration_date: term({
generate: '2013-02-05T05:00:00Z',
matcher: dateRegex,
}),
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),
is_staff: boolean(true),
course_access: like({
has_access: 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,
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, certificate_available_date: null,
},
verify_identity_url: null,
verification_status: string('none'),
linkedin_add_to_profile_url: null,
user_needs_integrity_signature: boolean(false),
},
},
});
it('returns normalized course metadata', () => {
const normalizedCourseMetadata = {
accessExpiration: {
expirationDate: '2013-02-05T05:00:00Z',
@@ -337,56 +234,122 @@ describe('Courseware Service', () => {
relatedPrograms: null,
userNeedsIntegritySignature: false,
};
const response = await getCourseMetadata(courseId);
expect(response).toBeTruthy();
expect(response).toEqual(normalizedCourseMetadata);
setTimeout(() => {
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'),
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'),
},
related_programs: null,
short_description: like(''),
start: term({
generate: '2013-02-05T05:00:00Z',
matcher: dateRegex,
}),
user_timezone: null,
verified_mode: like({
access_expiration_date: term({
generate: '2013-02-05T05:00:00Z',
matcher: dateRegex,
}),
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),
is_staff: boolean(true),
course_access: like({
has_access: 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,
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, certificate_available_date: null,
},
verify_identity_url: null,
verification_status: string('none'),
linkedin_add_to_profile_url: null,
user_needs_integrity_signature: boolean(false),
},
},
});
const response = getCourseMetadata(courseId);
expect(response).toBeTruthy();
expect(response).toEqual(normalizedCourseMetadata);
}, 100);
});
});
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),
is_hidden_after_due: 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'),
},
},
});
it('returns normalized sequence metadata ', () => {
const normalizedSequenceMetadata = {
sequence: {
id: 'block-v1:edX+DemoX+Demo_Course+type@sequential+block@basic_questions',
@@ -423,102 +386,154 @@ describe('Courseware Service', () => {
containsContentTypeGatedContent: false,
}],
};
const response = await getSequenceMetadata(sequenceId);
expect(response).toBeTruthy();
expect(response).toEqual(normalizedSequenceMetadata);
setTimeout(() => {
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),
is_hidden_after_due: 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 response = getSequenceMetadata(sequenceId);
expect(response).toBeTruthy();
expect(response).toEqual(normalizedSequenceMetadata);
}, 100);
});
});
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),
setTimeout(() => {
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.
},
},
});
const response = await postSequencePosition(courseId, sequenceId, 0);
expect(response).toBeTruthy();
expect(response).toEqual({ success: true });
willRespondWith: {
status: 200,
body: {
success: boolean(true),
},
},
});
const response = postSequencePosition(courseId, sequenceId, 0);
expect(response).toBeTruthy();
expect(response).toEqual({ success: true });
}, 100);
});
});
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),
setTimeout(() => {
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 },
},
},
});
const response = await getBlockCompletion(courseId, sequenceId, usageId);
expect(response).toBeTruthy();
expect(response).toEqual(true);
willRespondWith: {
status: 200,
body: {
complete: boolean(true),
},
},
});
const response = getBlockCompletion(courseId, sequenceId, usageId);
expect(response).toBeTruthy();
expect(response).toEqual(true);
}, 100);
});
});
describe('When a request to get resume block is made', () => {
it('returns block id, section id and unit id of the resume block', async () => {
await provider.addInteraction({
state: `Resume block exists for course_id ${courseId}`,
uponReceiving: 'a request to get Resume block',
withRequest: {
method: 'GET',
path: `/api/courseware/resume/${courseId}`,
},
willRespondWith: {
status: 200,
body: {
block_id: string('642fadf46d074aabb637f20af320fb31'),
section_id: string('642fadf46d074aabb637f20af320fb87'),
unit_id: string('642fadf46d074aabb637f20af320fb99'),
},
},
});
const camelCaseResponse = {
blockId: '642fadf46d074aabb637f20af320fb31',
sectionId: '642fadf46d074aabb637f20af320fb87',
unitId: '642fadf46d074aabb637f20af320fb99',
};
const response = await getResumeBlock(courseId);
expect(response).toBeTruthy();
expect(response).toEqual(camelCaseResponse);
setTimeout(() => {
provider.addInteraction({
state: `Resume block exists for course_id ${courseId}`,
uponReceiving: 'a request to get Resume block',
withRequest: {
method: 'GET',
path: `/api/courseware/resume/${courseId}`,
},
willRespondWith: {
status: 200,
body: {
block_id: string('642fadf46d074aabb637f20af320fb31'),
section_id: string('642fadf46d074aabb637f20af320fb87'),
unit_id: string('642fadf46d074aabb637f20af320fb99'),
},
},
});
const response = getResumeBlock(courseId);
expect(response).toBeTruthy();
expect(response).toEqual(camelCaseResponse);
}, 100);
});
});
describe('When a request to send activation email is made', () => {
it('returns status code 200', async () => {
await provider.addInteraction({
state: 'A logged-in user may or may not be active',
uponReceiving: 'a request to send activation email',
withRequest: {
method: 'POST',
path: '/api/send_account_activation_email',
},
willRespondWith: {
status: 200,
},
});
const response = await sendActivationEmail();
expect(response).toEqual('');
it('returns status code 200', () => {
setTimeout(() => {
provider.addInteraction({
state: 'A logged-in user may or may not be active',
uponReceiving: 'a request to send activation email',
withRequest: {
method: 'POST',
path: '/api/send_account_activation_email',
},
willRespondWith: {
status: 200,
},
});
const response = sendActivationEmail();
expect(response).toEqual('');
}, 100);
});
});
});

View File

@@ -0,0 +1,16 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`DecodePageRoute should not modify the url if it does not need to be decoded 1`] = `
<div>
PageRoute: {
"computedMatch": {
"path": "/course/:courseId/home",
"url": "/course/course-v1:edX+DemoX+Demo_Course/home",
"isExact": true,
"params": {
"courseId": "course-v1:edX+DemoX+Demo_Course"
}
}
}
</div>
`;

View File

@@ -0,0 +1,49 @@
import PropTypes from 'prop-types';
import { PageRoute } from '@edx/frontend-platform/react';
import React from 'react';
import { useHistory, generatePath } from 'react-router';
export const decodeUrl = (encodedUrl) => {
const decodedUrl = decodeURIComponent(encodedUrl);
if (encodedUrl === decodedUrl) {
return encodedUrl;
}
return decodeUrl(decodedUrl);
};
const DecodePageRoute = (props) => {
const history = useHistory();
if (props.computedMatch) {
const { url, path, params } = props.computedMatch;
Object.keys(params).forEach((param) => {
// only decode params not the entire url.
// it is just to be safe and less prone to errors
params[param] = decodeUrl(params[param]);
});
const newUrl = generatePath(path, params);
// if the url get decoded, reroute to the decoded url
if (newUrl !== url) {
history.replace(newUrl);
}
}
return <PageRoute {...props} />;
};
DecodePageRoute.propTypes = {
computedMatch: PropTypes.shape({
url: PropTypes.string.isRequired,
path: PropTypes.string.isRequired,
// eslint-disable-next-line react/forbid-prop-types
params: PropTypes.any,
}),
};
DecodePageRoute.defaultProps = {
computedMatch: null,
};
export default DecodePageRoute;

View File

@@ -0,0 +1,103 @@
import React from 'react';
import { render } from '@testing-library/react';
import { createMemoryHistory } from 'history';
import { Router, matchPath } from 'react-router';
import DecodePageRoute, { decodeUrl } from '.';
const decodedCourseId = 'course-v1:edX+DemoX+Demo_Course';
const encodedCourseId = encodeURIComponent(decodedCourseId);
const deepEncodedCourseId = (() => {
let path = encodedCourseId;
for (let i = 0; i < 5; i++) {
path = encodeURIComponent(path);
}
return path;
})();
jest.mock('@edx/frontend-platform/react', () => ({
PageRoute: (props) => `PageRoute: ${JSON.stringify(props, null, 2)}`,
}));
const renderPage = (props) => {
const memHistory = createMemoryHistory({
initialEntries: [props?.path],
});
const history = {
...memHistory,
replace: jest.fn(),
};
const { container } = render(
<Router history={history}>
<DecodePageRoute computedMatch={props} />
</Router>,
);
return {
container,
history,
props,
};
};
describe('DecodePageRoute', () => {
it('should not modify the url if it does not need to be decoded', () => {
const props = matchPath(`/course/${decodedCourseId}/home`, {
path: '/course/:courseId/home',
});
const { container, history } = renderPage(props);
expect(props.url).toContain(decodedCourseId);
expect(history.replace).not.toHaveBeenCalled();
expect(container).toMatchSnapshot();
});
it('should decode the url and replace the history if necessary', () => {
const props = matchPath(`/course/${encodedCourseId}/home`, {
path: '/course/:courseId/home',
});
const { history } = renderPage(props);
expect(props.url).not.toContain(decodedCourseId);
expect(props.url).toContain(encodedCourseId);
expect(history.replace.mock.calls[0][0]).toContain(decodedCourseId);
});
it('should decode the url multiple times if necessary', () => {
const props = matchPath(`/course/${deepEncodedCourseId}/home`, {
path: '/course/:courseId/home',
});
const { history } = renderPage(props);
expect(props.url).not.toContain(decodedCourseId);
expect(props.url).toContain(deepEncodedCourseId);
expect(history.replace.mock.calls[0][0]).toContain(decodedCourseId);
});
it('should only decode the url params and not the entire url', () => {
const decodedUnitId = 'some+thing';
const encodedUnitId = encodeURIComponent(decodedUnitId);
const props = matchPath(`/course/${deepEncodedCourseId}/${encodedUnitId}/${encodedUnitId}`, {
path: `/course/:courseId/${encodedUnitId}/:unitId`,
});
const { history } = renderPage(props);
const decodedUrls = history.replace.mock.calls[0][0].split('/');
// unitId get decoded
expect(decodedUrls.pop()).toContain(decodedUnitId);
// path remain encoded
expect(decodedUrls.pop()).toContain(encodedUnitId);
// courseId get decoded
expect(decodedUrls.pop()).toContain(decodedCourseId);
});
});
describe('decodeUrl', () => {
expect(decodeUrl(decodedCourseId)).toEqual(decodedCourseId);
expect(decodeUrl(encodedCourseId)).toEqual(decodedCourseId);
expect(decodeUrl(deepEncodedCourseId)).toEqual(decodedCourseId);
});

View File

@@ -10,16 +10,19 @@ import { getNotices } from './api';
*/
const NoticesProvider = ({ children }) => {
const [isRedirected, setIsRedirected] = useState();
// eslint-disable-next-line react-hooks/exhaustive-deps
useEffect(async () => {
if (getConfig().ENABLE_NOTICES) {
const data = await getNotices();
if (data && data.results && data.results.length > 0) {
const { results } = data;
setIsRedirected(true);
window.location.replace(`${results[0]}?next=${window.location.href}`);
useEffect(() => {
async function getData() {
if (getConfig().ENABLE_NOTICES) {
const data = await getNotices();
if (data && data.results && data.results.length > 0) {
const { results } = data;
setIsRedirected(true);
window.location.replace(`${results[0]}?next=${window.location.href}`);
}
}
}
getData();
}, []);
return (

View File

@@ -114,7 +114,8 @@
"learning.proctoringPanel.onboardingButtonNotOpen": "يفتح الامتحان التحضيري في: {releaseDate}",
"learning.proctoringPanel.reviewRequirementsButton": "مراجعة التعليمات و متطلبات النظام",
"learning.proctoringPanel.onboardingButtonPastDue": "انقضى أجَل الامتحان التحضيري",
"learning.outline.sequence-due": "{description} للتسليم قبل {assignmentDue}",
"learning.outline.sequence-due-date-set": "{description} due {assignmentDue}",
"learning.outline.sequence-due-date-not-set": "{description}",
"progress.certificateStatus.unverifiedBody": "لتوليد شهادة، يجب عليك إكمال التحقق من هويتك. {idVerificationSupportLink}.",
"progress.certificateStatus.downloadableBody": "اعرض إنجازاتك اليوم على لينكد إن أو ضمن سيرتك الذاتية. يمكنك تحميل شهادتك الآن و الوصول إليها في أي وقت من لوحة معلوماتك و ملفك الشخصي.",
"courseCelebration.certificateBody.notAvailable.endDate": "تمت جدولة ظهور الدرجات النهائية و أي شهادات مكتسبة ابتداءً من {endDate}.",

View File

@@ -114,7 +114,8 @@
"learning.proctoringPanel.onboardingButtonNotOpen": "Onboarding Opens: {releaseDate}",
"learning.proctoringPanel.reviewRequirementsButton": "Review instructions and system requirements",
"learning.proctoringPanel.onboardingButtonPastDue": "Onboarding Past Due",
"learning.outline.sequence-due": "{description} due {assignmentDue}",
"learning.outline.sequence-due-date-set": "{description} due {assignmentDue}",
"learning.outline.sequence-due-date-not-set": "{description}",
"progress.certificateStatus.unverifiedBody": "In order to generate a certificate, you must complete ID verification. {idVerificationSupportLink}.",
"progress.certificateStatus.downloadableBody": "Showcase your accomplishment on LinkedIn or your resumé today. You can download your certificate now and access it any time from your Dashboard and Profile.",
"courseCelebration.certificateBody.notAvailable.endDate": "Final grades and any earned certificates are scheduled to be available after {endDate}.",

View File

@@ -114,7 +114,8 @@
"learning.proctoringPanel.onboardingButtonNotOpen": "Apertura de la integración: {releaseDate}",
"learning.proctoringPanel.reviewRequirementsButton": "Revisar las instrucciones y los requisitos del sistema",
"learning.proctoringPanel.onboardingButtonPastDue": "Inducción vencida",
"learning.outline.sequence-due": "Fecha límite para {description}: {assignmentDue}",
"learning.outline.sequence-due-date-set": "Fecha límite para {description}: {assignmentDue}",
"learning.outline.sequence-due-date-not-set": "{description}",
"progress.certificateStatus.unverifiedBody": "Para generar un certificado, debes completar la verificación de identidad. {idVerificationSupportLink}.",
"progress.certificateStatus.downloadableBody": "Muestra tu logro en LinkedIn o en tu currículum. Puedes descargar tu certificado ahora y acceder a él en cualquier momento desde tu panel de estudiante y tu perfil.",
"courseCelebration.certificateBody.notAvailable.endDate": "Las calificaciones finales y los certificados obtenidos están programados para estar disponibles después de {endDate}.",

View File

@@ -114,7 +114,8 @@
"learning.proctoringPanel.onboardingButtonNotOpen": "Ouverture de l'intégration : {releaseDate}",
"learning.proctoringPanel.reviewRequirementsButton": "Examiner les instructions et la configuration système requise",
"learning.proctoringPanel.onboardingButtonPastDue": "Intégration en retard",
"learning.outline.sequence-due": "{description} échéance {assignmentDue}",
"learning.outline.sequence-due-date-set": "{description} due {assignmentDue}",
"learning.outline.sequence-due-date-not-set": "{description}",
"progress.certificateStatus.unverifiedBody": "Afin de générer une attestation, vous devez effectuer une vérification d'identité. {idVerificationSupportLink}.",
"progress.certificateStatus.downloadableBody": "Présentez vos réalisations sur LinkedIn ou votre curriculum vitae aujourd'hui. Vous pouvez télécharger votre certificat maintenant et y accéder à tout moment depuis votre tableau de bord et votre profil.",
"courseCelebration.certificateBody.notAvailable.endDate": "Les notes finales et toutes les attestations obtenues devraient être disponibles après le {endDate}.",

View File

@@ -114,9 +114,10 @@
"learning.proctoringPanel.onboardingButtonNotOpen": "Ouverture de l'intégration : {releaseDate}",
"learning.proctoringPanel.reviewRequirementsButton": "Examiner les instructions et la configuration système requise",
"learning.proctoringPanel.onboardingButtonPastDue": "Intégration en retard",
"learning.outline.sequence-due": "{description} échéance {assignmentDue}",
"learning.outline.sequence-due-date-set": "{description} échéance {assignmentDue}",
"learning.outline.sequence-due-date-not-set": "{description}",
"progress.certificateStatus.unverifiedBody": "Afin de générer une attestation, vous devez effectuer une vérification d'identité. {idVerificationSupportLink}.",
"progress.certificateStatus.downloadableBody": "Présentez vos réalisations sur LinkedIn ou votre curriculum vitae aujourd'hui. Vous pouvez télécharger votre certificat maintenant et y accéder à tout moment depuis votre tableau de bord et votre profil.",
"progress.certificateStatus.downloadableBody": "Présentez vos réalisations sur LinkedIn ou votre curriculum vitae aujourd'hui. Vous pouvez télécharger votre attestation maintenant et y accéder à tout moment depuis votre tableau de bord et votre profil.",
"courseCelebration.certificateBody.notAvailable.endDate": "Les notes finales et toutes les attestations obtenues devraient être disponibles après le {endDate}.",
"progress.certificateStatus.notPassingHeader": "État de l'attestation",
"progress.certificateStatus.notPassingBody": "Pour être admissible à une attestation, vous devez avoir la note de passage.",
@@ -141,7 +142,7 @@
"progress.completion.donut.label": "complété",
"progress.completion.body": "Cela représente la part du contenu du cours que vous avez terminé. Notez que certains contenus peuvent ne pas encore être publiés.",
"progress.completion.tooltip.locked": "Contenu que vous avez terminé.",
"progress.completion.header": "Achèvement du cours",
"progress.completion.header": "Complétion du cours",
"progress.completion.tooltip": "Contenu auquel vous avez accès et que vous n'avez pas terminé.",
"progress.completion.tooltip.complete": "Contenu verrouillé et disponible uniquement pour ceux qui effectuent une mise à niveau.",
"progress.completion.donut.percentComplete": "Vous avez terminé {percent}% du contenu de ce cours.",
@@ -303,7 +304,7 @@
"courseExit.nextButton.endOfCourse": "Suivant (fin du cours)",
"courseExit.profileLink": "Profil",
"courseExit.programs.lastCourse": "Vous avez terminé le dernier cours de {title}!",
"courseCelebration.requestCertificateBodyText": "Pour accéder à votre certificat, demandez-le ci-dessous.",
"courseCelebration.requestCertificateBodyText": "Pour accéder à votre attestation, demandez-la ci-dessous.",
"courseCelebration.requestCertificateButton": "Demander une attestation",
"courseExit.searchOurCatalogLink": "Rechercher dans notre catalogue",
"courseCelebration.shareMessage": "Partagez votre succès sur les réseaux sociaux ou par courriel.",
@@ -398,7 +399,7 @@
"learning.generic.upgradeNotification.pursueAverifiedCertificate": "Obtenir une attestation vérifiée",
"learning.generic.upgradeNotification.code": "Utilisez le code {code} lors du paiement",
"learning.generic.upsell.verifiedCertBullet.verifiedCert": "attestation vérifiée",
"learning.generic.upsell.verifiedCertBullet": "Obtenez un {verifiedCertLink} d'achèvement pour le mettre en valeur sur votre CV",
"learning.generic.upsell.verifiedCertBullet": "Obtenez une {verifiedCertLink} de complétion pour la mettre en valeur sur votre CV",
"learning.generic.upsell.unlockGradedBullet.gradedAssignments": "évaluations notées",
"learning.generic.upsell.unlockGradedBullet": "Débloquer votre accès à toutes les activités du cours, incluant {gradedAssignmentsInBoldText}",
"learning.generic.upsell.fullAccessBullet.fullAccess": "Accès complet",
@@ -413,23 +414,23 @@
"tours.existingUserTour.launchTourCheckpoint.body": "Nous avons récemment ajouté des nouvelles fonctions à l'expérience de cours. Vous avez besoin d'aide à les trouver? Prenez un tour guidé pour en apprendre plus.",
"tours.button.dismiss": "Rejeter",
"tours.button.next": "Suivant",
"tours.button.okay": "Okay",
"tours.button.okay": "D'accord",
"tours.button.beginTour": "Commencer la visite guidée",
"tours.button.launchTour": "Lancer la visite guidée",
"tours.newUserModal.body": "Faisons un tour rapide de {siteName} afin que vous puissiez tirer le meilleur parti de votre cours.",
"tours.newUserModal.title.welcome": "Bienvenu à votre",
"tours.newUserModal.title.welcome": "Bienvenue à votre",
"tours.button.skipForNow": "Ignorer pour l'instant",
"tours.datesCheckpoint.body": "Dates importantes afin de vous maintenir sur la bonne voie.",
"tours.datesCheckpoint.title": "Restez au courant des dates importantes",
"tours.outlineCheckpoint.body": "Vous pouvez explorer les sections du cours en utilisant la table des matières ci-dessous.",
"tours.outlineCheckpoint.title": "Suivez le cours!",
"tours.tabNavigationCheckpoint.body": "Ces onglets peuvent être utilisés pour accéder aux autres ressources de cours, tel que votre progression, le plan de cours, etc.",
"tours.tabNavigationCheckpoint.body": "Ces onglets peuvent être utilisés pour accéder aux autres ressources de cours, telles que votre progression, le plan de cours, etc.",
"tours.tabNavigationCheckpoint.title": "Ressources de cours additionnelles",
"tours.upgradeCheckpoint.body": "Travaillez vers une attestation et obtenez un accès complet au matériel de cours. Mettre à niveau maintenant!",
"tours.upgradeCheckpoint.title": "Débloquez votre cours",
"tours.weeklyGoalsCheckpoint.body": "Paramétrer un objectif encourage à compléter votre cours.",
"tours.weeklyGoalsCheckpoint.title": "Paramétrer un objectif de cours",
"tours.newUserModal.title": "{welcome} {siteName} au cours!",
"tours.newUserModal.title": "{welcome} cours sur {siteName} !",
"learning.effortEstimation.combinedEstimate": "{minutes} + {activities}",
"learning.effortEstimation.activities": "{activityCount, plural, one {# activité} many {# activités} other {# activités}}",
"learning.effortEstimation.minutesAbbreviated": "{minuteCount, plural, one {# min} many {# min} other {# min}}",

View File

@@ -114,7 +114,8 @@
"learning.proctoringPanel.onboardingButtonNotOpen": "Onboarding Opens: {releaseDate}",
"learning.proctoringPanel.reviewRequirementsButton": "Review instructions and system requirements",
"learning.proctoringPanel.onboardingButtonPastDue": "Onboarding Past Due",
"learning.outline.sequence-due": "{description} due {assignmentDue}",
"learning.outline.sequence-due-date-set": "{description} due {assignmentDue}",
"learning.outline.sequence-due-date-not-set": "{description}",
"progress.certificateStatus.unverifiedBody": "In order to generate a certificate, you must complete ID verification. {idVerificationSupportLink}.",
"progress.certificateStatus.downloadableBody": "Showcase your accomplishment on LinkedIn or your resumé today. You can download your certificate now and access it any time from your Dashboard and Profile.",
"courseCelebration.certificateBody.notAvailable.endDate": "Final grades and any earned certificates are scheduled to be available after {endDate}.",

View File

@@ -114,7 +114,8 @@
"learning.proctoringPanel.onboardingButtonNotOpen": "Onboarding Opens: {releaseDate}",
"learning.proctoringPanel.reviewRequirementsButton": "Review instructions and system requirements",
"learning.proctoringPanel.onboardingButtonPastDue": "Onboarding Past Due",
"learning.outline.sequence-due": "{description} due {assignmentDue}",
"learning.outline.sequence-due-date-set": "{description} due {assignmentDue}",
"learning.outline.sequence-due-date-not-set": "{description}",
"progress.certificateStatus.unverifiedBody": "In order to generate a certificate, you must complete ID verification. {idVerificationSupportLink}.",
"progress.certificateStatus.downloadableBody": "Showcase your accomplishment on LinkedIn or your resumé today. You can download your certificate now and access it any time from your Dashboard and Profile.",
"courseCelebration.certificateBody.notAvailable.endDate": "Final grades and any earned certificates are scheduled to be available after {endDate}.",

View File

@@ -114,7 +114,8 @@
"learning.proctoringPanel.onboardingButtonNotOpen": "Onboarding Opens: {releaseDate}",
"learning.proctoringPanel.reviewRequirementsButton": "Review instructions and system requirements",
"learning.proctoringPanel.onboardingButtonPastDue": "Onboarding Past Due",
"learning.outline.sequence-due": "{description} due {assignmentDue}",
"learning.outline.sequence-due-date-set": "{description} due {assignmentDue}",
"learning.outline.sequence-due-date-not-set": "{description}",
"progress.certificateStatus.unverifiedBody": "In order to generate a certificate, you must complete ID verification. {idVerificationSupportLink}.",
"progress.certificateStatus.downloadableBody": "Showcase your accomplishment on LinkedIn or your resumé today. You can download your certificate now and access it any time from your Dashboard and Profile.",
"courseCelebration.certificateBody.notAvailable.endDate": "Final grades and any earned certificates are scheduled to be available after {endDate}.",

View File

@@ -114,7 +114,8 @@
"learning.proctoringPanel.onboardingButtonNotOpen": "Onboarding Opens: {releaseDate}",
"learning.proctoringPanel.reviewRequirementsButton": "Review instructions and system requirements",
"learning.proctoringPanel.onboardingButtonPastDue": "Onboarding Past Due",
"learning.outline.sequence-due": "{description} due {assignmentDue}",
"learning.outline.sequence-due-date-set": "{description} due {assignmentDue}",
"learning.outline.sequence-due-date-not-set": "{description}",
"progress.certificateStatus.unverifiedBody": "In order to generate a certificate, you must complete ID verification. {idVerificationSupportLink}.",
"progress.certificateStatus.downloadableBody": "Showcase your accomplishment on LinkedIn or your resumé today. You can download your certificate now and access it any time from your Dashboard and Profile.",
"courseCelebration.certificateBody.notAvailable.endDate": "Final grades and any earned certificates are scheduled to be available after {endDate}.",

View File

@@ -114,7 +114,8 @@
"learning.proctoringPanel.onboardingButtonNotOpen": "Onboarding Opens: {releaseDate}",
"learning.proctoringPanel.reviewRequirementsButton": "Review instructions and system requirements",
"learning.proctoringPanel.onboardingButtonPastDue": "Onboarding Past Due",
"learning.outline.sequence-due": "{description} due {assignmentDue}",
"learning.outline.sequence-due-date-set": "{description} due {assignmentDue}",
"learning.outline.sequence-due-date-not-set": "{description}",
"progress.certificateStatus.unverifiedBody": "In order to generate a certificate, you must complete ID verification. {idVerificationSupportLink}.",
"progress.certificateStatus.downloadableBody": "Showcase your accomplishment on LinkedIn or your resumé today. You can download your certificate now and access it any time from your Dashboard and Profile.",
"courseCelebration.certificateBody.notAvailable.endDate": "Final grades and any earned certificates are scheduled to be available after {endDate}.",

View File

@@ -3,14 +3,14 @@
"learning.accessExpiration.header": "Audit Access Expires {date}",
"learning.accessExpiration.body": "You lose all access to this course, including your progress, on {date}.",
"instructorToolbar.pageBanner.courseHasExpired": "This learner no longer has access to this course. Their access expired on {date}.",
"learning.accessExpiration.upgradeNow": "Upgrade now",
"learning.accessExpiration.upgradeNow": "马上升级",
"learning.activeEnterprise.alert": " {changeActiveEnterprise}.",
"learning.activeEnterprise.change.alert": "change enterprise now",
"learning.outline.alert.start.short": "Course starts {timeRemaining} at {courseStartTime}.",
"learning.outline.alert.end.long": "This course is ending {timeRemaining} on {courseEndDate}.",
"learning.outline.alert.end.calendar": "Dont forget to add a calendar reminder!",
"instructorToolbar.pageBanner.courseHasNotStarted": "This learner does not yet have access to this course. The course starts on {date}.",
"learning.enrollment.alert": "You must be enrolled in the course to see course content.",
"learning.enrollment.alert": "您必须报读此课程才能查看课程内容。",
"learning.staff.enrollment.alert": "You are viewing this course as staff, and are not enrolled.",
"learning.enrollment.enrollNow.Inline": "Enroll now",
"learning.enrollment.enrollNow.Sentence": "Enroll now.",
@@ -22,17 +22,17 @@
"account-activation.alert.title": "Activate your account so you can log back in",
"learn.sequence.entranceExamTextNotPassing": "To access course materials, you must score {entranceExamMinimumScorePct}% or higher on this exam. Your current score is {entranceExamCurrentScore}%.",
"learn.sequence.entranceExamTextPassed": "Your score is {entranceExamCurrentScore}%. You have passed the entrance exam.",
"learning.dates.badge.completed": "Completed",
"learning.dates.badge.completed": "已完成",
"learning.dates.badge.dueNext": "Due next",
"learning.dates.badge.pastDue": "Past due",
"learning.dates.title": "Important dates",
"learning.dates.badge.today": "Today",
"learning.dates.title": "重要日期",
"learning.dates.badge.today": "今天",
"learning.dates.badge.unreleased": "Not yet released",
"learning.dates.badge.verifiedOnly": "Verified only",
"learning.goals.unsubscribe.contact": "contact support",
"learning.goals.unsubscribe.contact": "请联系技术支持",
"learning.goals.unsubscribe.description": "You will no longer receive email reminders about your goal for {courseTitle}.",
"learning.goals.unsubscribe.errorHeader": "Something went wrong",
"learning.goals.unsubscribe.goToDashboard": "Go to dashboard",
"learning.goals.unsubscribe.goToDashboard": "去控制面板",
"learning.goals.unsubscribe.header": "Youve unsubscribed from goal reminders",
"learning.goals.unsubscribe.loading": "Unsubscribing…",
"learning.goals.unsubscribe.errorDescription": "We were unable to unsubscribe you from goal reminder emails. Please try again later or {contactSupport} for help.",
@@ -52,21 +52,21 @@
"learning.outline.goalButton.screenReader.text": "Casual",
"learning.outline.certificateAlt": "Example Certificate",
"learning.outline.collapseAll": "Collapse all",
"learning.outline.completedAssignment": "Completed",
"learning.outline.completedAssignment": "已完成",
"learning.outline.completedSection": "Completed section",
"learning.outline.dates": "Important dates",
"learning.outline.dates": "重要日期",
"learning.outline.editGoal": "Edit goal",
"learning.outline.expandAll": "Expand all",
"learning.outline.goal": "Goal",
"learning.outline.goalReminderDetail": "If we notice youre not quite at your goal, well send you an email reminder.",
"learning.outline.goalUnsure": "Not sure yet",
"learning.outline.handouts": "Course Handouts",
"learning.outline.incompleteAssignment": "Incomplete",
"learning.outline.goalUnsure": "再想想",
"learning.outline.handouts": "课程讲义",
"learning.outline.incompleteAssignment": "未完成",
"learning.outline.incompleteSection": "Incomplete section",
"learning.outline.goalButton.intense.text": "5 days a week",
"learning.outline.goalButton.intense.title": "Intense",
"learning.outline.learnMore": "Learn More",
"learning.outline.altText.openSection": "Open",
"learning.outline.learnMore": "了解更多",
"learning.outline.altText.openSection": "打开",
"learning.proctoringPanel.header": "This course contains proctored exams",
"learning.outline.goalButton.regular.text": "3 days a week",
"learning.outline.goalButton.regular.title": "Regular",
@@ -79,19 +79,19 @@
"learning.outline.setWeeklyGoalDetail": "Setting a goal motivates you to finish the course. You can always change it later.",
"learning.outline.start": "Start course",
"learning.outline.startBlurb": "Begin your course today",
"learning.outline.tools": "Course Tools",
"learning.outline.tools": "课程工具",
"learning.outline.upgradeButton": "Upgrade ({symbol}{price})",
"learning.outline.upgradeTitle": "Pursue a verified certificate",
"learning.outline.upgradeTitle": "考取认证证书",
"learning.outline.welcomeMessage": "Welcome Message",
"learning.outline.welcomeMessageShowMoreButton": "Show More",
"learning.outline.welcomeMessageShowLessButton": "Show Less",
"learning.outline.goalWelcome": "Welcome to",
"learning.proctoringPanel.status.notStarted": "Not Started",
"learning.proctoringPanel.status.started": "Started",
"learning.proctoringPanel.status.submitted": "Submitted",
"learning.proctoringPanel.status.verified": "Verified",
"learning.proctoringPanel.status.rejected": "Rejected",
"learning.proctoringPanel.status.error": "Error",
"learning.outline.welcomeMessageShowMoreButton": "显示更多",
"learning.outline.welcomeMessageShowLessButton": "查看收起",
"learning.outline.goalWelcome": "欢迎来到",
"learning.proctoringPanel.status.notStarted": "尚未开始",
"learning.proctoringPanel.status.started": "已经开始",
"learning.proctoringPanel.status.submitted": "已提交",
"learning.proctoringPanel.status.verified": "已经过身份认证的",
"learning.proctoringPanel.status.rejected": "拒绝",
"learning.proctoringPanel.status.error": "错误",
"learning.proctoringPanel.status.otherCourseApproved": "Approved in Another Course",
"learning.proctoringPanel.status.expiringSoon": "Expiring Soon",
"learning.proctoringPanel.status.expired": "Expired",
@@ -114,7 +114,8 @@
"learning.proctoringPanel.onboardingButtonNotOpen": "Onboarding Opens: {releaseDate}",
"learning.proctoringPanel.reviewRequirementsButton": "Review instructions and system requirements",
"learning.proctoringPanel.onboardingButtonPastDue": "Onboarding Past Due",
"learning.outline.sequence-due": "{description} due {assignmentDue}",
"learning.outline.sequence-due-date-set": "{description} due {assignmentDue}",
"learning.outline.sequence-due-date-not-set": "{description}",
"progress.certificateStatus.unverifiedBody": "In order to generate a certificate, you must complete ID verification. {idVerificationSupportLink}.",
"progress.certificateStatus.downloadableBody": "Showcase your accomplishment on LinkedIn or your resumé today. You can download your certificate now and access it any time from your Dashboard and Profile.",
"courseCelebration.certificateBody.notAvailable.endDate": "Final grades and any earned certificates are scheduled to be available after {endDate}.",
@@ -132,13 +133,13 @@
"progress.certificateStatus.viewableButton": "View my certificate",
"progress.certificateStatus.notAvailableHeader": "Certificate status",
"progress.certificateBody.notAvailable.endDate": "Final grades and any earned certificates are scheduled to be available after {endDate}.",
"progress.certificateStatus.upgradeHeader": "Earn a certificate",
"progress.certificateStatus.upgradeHeader": "获取证书",
"progress.certificateStatus.upgradeBody": "You are in an audit track and do not qualify for a certificate. In order to work towards a certificate, upgrade your course today.",
"progress.certificateStatus.upgradeButton": "Upgrade now",
"progress.certificateStatus.upgradeButton": "马上升级",
"progress.certificateStatus.unverifiedHomeHeader.v2": "Verify your identity to qualify for a certificate.",
"progress.certificateStatus.unverifiedHomeButton": "Verify my ID",
"progress.certificateStatus.unverifiedHomeBody": "In order to generate a certificate for this course, you must complete the ID verification process.",
"progress.completion.donut.label": "completed",
"progress.completion.donut.label": "完成",
"progress.completion.body": "This represents how much of the course content you have completed. Note that some content may not yet be released.",
"progress.completion.tooltip.locked": "Content that you have completed.",
"progress.completion.header": "Course completion",
@@ -150,11 +151,11 @@
"progress.creditInformation.creditNotEligible": "You are no longer eligible for credit in this course. Learn more about {creditLink}.",
"progress.creditInformation.creditEligible": "\n You have met the requirements for credit in this course. Go to your\n {dashboardLink} to purchase course credit. Or learn more about {creditLink}.",
"progress.creditInformation.creditPartialEligible": "You have not yet met the requirements for credit. Learn more about {creditLink}.",
"progress.creditInformation.completed": "Completed",
"progress.creditInformation.completed": "已完成",
"progress.creditInformation.courseCredit": "course credit",
"progress.creditInformation.minimumGrade": "Minimum grade for credit ({minGrade}%)",
"progress.creditInformation.requirementsHeader": "Requirements for course credit",
"progress.creditInformation.upcoming": "Upcoming",
"progress.creditInformation.upcoming": "即将到来",
"progress.creditInformation.verificationFailed": "Verification failed",
"progress.creditInformation.verificationSubmitted": "Verification submitted",
"progress.ungradedAlert": "For progress on ungraded aspects of the course, view your {outlineLink}.",
@@ -172,18 +173,18 @@
"progress.courseGrade.preview.body.unlockCertificate": "Unlock to view grades and work towards a certificate.",
"progress.courseGrade.partialpreview.body.unlockCertificate": "Unlock to work towards a certificate.",
"progress.courseGrade.preview.body.upgradeDeadlinePassed": "The deadline to upgrade in this course has passed.",
"progress.courseGrade.preview.button.upgrade": "Upgrade now",
"progress.courseGrade.preview.button.upgrade": "马上升级",
"progress.courseGrade.gradeRange.tooltip": "Grade ranges for this course:",
"progress.courseOutline": "Course Outline",
"progress.courseOutline": "课程大纲",
"progress.courseGrade.label.currentGrade": "Your current grade",
"progress.detailedGrades": "Detailed grades",
"progress.detailedGrades.emptyTable": "You currently have no graded problem scores.",
"progress.footnotes.title": "Grade summary footnotes",
"progress.gradeSummary.grade": "Grade",
"progress.gradeSummary.grade": "成绩",
"progress.courseGrade.grades": "Grades",
"progress.courseGrade.gradesAndCredit": "Grades & Credit",
"progress.courseGrade.gradeRange.Tooltip": "Grade range tooltip",
"progress.gradeSummary": "Grade summary",
"progress.gradeSummary": "评分汇总",
"progress.gradeSummary.limitedAccessExplanation": "You have limited access to graded assignments as part of the audit track in this course.",
"progress.gradeSummary.tooltip.alt": "Grade summary tooltip",
"progress.gradeSummary.tooltip.body": "Your course assignment's weight is determined by your instructor. By multiplying your grade by the weight for that assignment type, your weighted grade is calculated. Your weighted grade is what's used to determine if you pass the course.",
@@ -192,33 +193,33 @@
"progress.courseGrade.label.passingGrade": "Passing grade",
"progress.detailedGrades.problemScore.label": "Problem Scores:",
"progress.detailedGrades.problemScore.toggleButton": "Toggle individual problem scores for {subsectionTitle}",
"progress.detailedGrades.overridden": "Section grade has been overridden.",
"progress.score": "Score",
"progress.weight": "Weight",
"progress.detailedGrades.overridden": "这部分的成绩已经被修订。",
"progress.score": "分数",
"progress.weight": "权重",
"progress.weightedGrade": "Weighted grade",
"progress.weightedGradeSummary": "Your current weighted grade summary",
"progress.header": "Your progress",
"progress.header.targetUser": "Course progress for {username}",
"progress.link.studio": "View grading in Studio",
"progress.relatedLinks.datesCard.description": "A schedule view of your course due dates and upcoming assignments.",
"progress.relatedLinks.datesCard.link": "Dates",
"progress.relatedLinks.datesCard.link": "日期",
"progress.relatedLinks.outlineCard.description": "A birds-eye view of your course content.",
"progress.relatedLinks.outlineCard.link": "Course Outline",
"progress.relatedLinks.outlineCard.link": "课程大纲",
"progress.relatedLinks": "Related links",
"datesBanner.suggestedSchedule": "Weve built a suggested schedule to help you stay on track. But dont worry—its flexible so you can learn at your own pace.",
"datesBanner.upgradeToCompleteGradedBanner.header": "Upgrade to unlock",
"datesBanner.upgradeToCompleteGradedBanner.header": "升级解锁",
"datesBanner.upgradeToCompleteGradedBanner.body": "You are auditing this course, which means that you are unable to participate in graded assignments. To complete graded assignments as part of this course, you can upgrade today.",
"datesBanner.upgradeToCompleteGradedBanner.button": "Upgrade now",
"datesBanner.upgradeToCompleteGradedBanner.button": "马上升级",
"datesBanner.upgradeToResetBanner.body": "To keep yourself on track, you can update this schedule and shift the past due assignments into the future. Dont worry—you wont lose any of the progress youve made when you shift your due dates.",
"datesBanner.upgradeToResetBanner.button": "Upgrade to shift due dates",
"datesBanner.resetDatesBanner.header": "It looks like you missed some important deadlines based on our suggested schedule.",
"datesBanner.resetDatesBanner.body": "To keep yourself on track, you can update this schedule and shift the past due assignments into the future. Dont worry—you wont lose any of the progress youve made when you shift your due dates.",
"datesBanner.resetDatesBanner.button": "Shift due dates",
"learn.navigation.course.tabs.label": "Course Material",
"unit.bookmark.button.add.bookmark": "Bookmark this page",
"unit.bookmark.button.remove.bookmark": "Bookmarked",
"learn.navigation.course.tabs.label": "课程资料",
"unit.bookmark.button.add.bookmark": "收藏此页",
"unit.bookmark.button.remove.bookmark": "已收藏",
"learning.celebration.completed": "You just completed the first section of your course.",
"learning.celebration.congrats": "Congratulations!",
"learning.celebration.congrats": "恭喜!",
"learning.celebration.earned": "You earned it!",
"learning.celebration.emailSubject": "I'm on my way to completing {title} online with {platform}!",
"learning.celebration.forward": "Keep going",
@@ -230,38 +231,38 @@
"learning.celebration.setGoal": "Setting a goal can help you {strongText} in your course.",
"calculator.instructions.button.label": "Calculator Instructions",
"calculator.instructions": "For detailed information, see the {expressions_link}.",
"calculator.instructions.support.title": "Help Center",
"calculator.instructions.support.title": "帮助中心",
"calculator.instructions.useful.tips": "Useful tips:",
"calculator.hint1": "Use parentheses () to make expressions clear. You can use parentheses inside other parentheses.",
"calculator.hint2": "Do not use spaces in expressions.",
"calculator.hint3": "For constants, indicate multiplication explicitly (example: 5*c).",
"calculator.hint4": "For affixes, type the number and affix without a space (example: 5c).",
"calculator.hint5": "For functions, type the name of the function, then the expression in parentheses.",
"calculator.instruction.table.to.use.heading": "To Use",
"calculator.instruction.table.type.heading": "Type",
"calculator.instruction.table.examples.heading": "Examples",
"calculator.instruction.table.to.use.numbers": "Numbers",
"calculator.instruction.table.to.use.numbers.type1": "Integers",
"calculator.instruction.table.to.use.numbers.type2": "Fractions",
"calculator.instruction.table.to.use.numbers.type3": "Decimals",
"calculator.instruction.table.to.use.operators": "Operators",
"calculator.hint1": "使用括号()使表达式更明确。您可以在括号内使用括号。",
"calculator.hint2": "不要在表达式中使用空格。",
"calculator.hint3": "对于常数,明确表明乘法(例如:5*c)",
"calculator.hint4": "对于词缀,输入数字和词组,不带空格(例:5c)",
"calculator.hint5": "对于函数,输入函数的名字,然后在括号中输入表达式。",
"calculator.instruction.table.to.use.heading": "来使用",
"calculator.instruction.table.type.heading": "类型",
"calculator.instruction.table.examples.heading": "例子",
"calculator.instruction.table.to.use.numbers": "数字",
"calculator.instruction.table.to.use.numbers.type1": "整数",
"calculator.instruction.table.to.use.numbers.type2": "分数",
"calculator.instruction.table.to.use.numbers.type3": "小数",
"calculator.instruction.table.to.use.operators": "操作",
"calculator.instruction.table.to.use.operators.type1": "(add, subtract, multiply, divide)",
"calculator.instruction.table.to.use.operators.type2": "(raise to a power)",
"calculator.instruction.table.to.use.operators.type3": "(parallel resistors)",
"calculator.instruction.table.to.use.constants": "Constants",
"calculator.instruction.table.to.use.affixes": "Affixes",
"calculator.instruction.table.to.use.constants": "常量",
"calculator.instruction.table.to.use.affixes": "词缀",
"calculator.instruction.table.to.use.affixes.type": "Percent sign (%)",
"calculator.instruction.table.to.use.basic.functions": "Basic functions",
"calculator.instruction.table.to.use.trig.functions": "Trigonometric functions",
"calculator.instruction.table.to.use.scientific.notation": "Scientific notation",
"calculator.instruction.table.to.use.basic.functions": "基本功能",
"calculator.instruction.table.to.use.trig.functions": "三角函数",
"calculator.instruction.table.to.use.scientific.notation": "科学记数法",
"calculator.instruction.table.to.use.scientific.notation.type1": "{exponentSyntax} and the exponent",
"calculator.instruction.table.to.use.scientific.notation.type2": "{notationSyntax} notation",
"calculator.instruction.table.to.use.scientific.notation.type3": "{notationSyntax} and the exponent",
"calculator.button.label": "Calculator",
"calculator.input.field.label": "Calculator Input",
"calculator.submit.button.label": "Calculate",
"calculator.button.label": "计算器",
"calculator.input.field.label": "计算器输入",
"calculator.submit.button.label": "计算",
"calculator.result.field.label": "Calculator Result",
"calculator.result.field.placeholder": "Result",
"calculator.result.field.placeholder": "结果",
"notes.button.show": "Show Notes",
"notes.button.hide": "Hide Notes",
"courseExit.catalogSearchSuggestion": "Looking to learn more? {searchOurCatalogLink} to find more courses and programs to explore.",
@@ -271,7 +272,7 @@
"courseCelebration.certificateBody.upgradable": "Its not too late to upgrade. For {price} you will unlock access to all graded\n assignments in this course. Upon completion, you will receive a verified certificate which is a\n valuable credential to improve your job prospects and advance your career, or highlight your\n certificate in school applications.",
"courseCelebration.upgradeDiscountCodePrompt": "Use code {code} at checkout for {percent}% off!",
"courseCelebration.recommendations.heading": "Keep building your skills with these courses!",
"courseCelebration.recommendations.label": "Course",
"courseCelebration.recommendations.label": "课程",
"courseCelebration.recommendations.formatting.list_join": "{style, select, punctuation {, } conjunction { {sp}and } other { }}",
"courseCelebration.recommendations.browse_catalog": "Explore more courses",
"courseCelebration.recommendations.loading_recommendations": "Loading recommendations",
@@ -282,33 +283,33 @@
"courseCelebration.certificateHeader.notAvailable": "Your grade and certificate status will be available soon.",
"courseCelebration.certificateBody.notAvailable.accessCertificate": "If you have earned a passing grade, your certificate will be automatically issued.",
"courseCelebration.certificateHeader.unverified": "You must complete verification to receive your certificate.",
"courseCelebration.certificateHeader.requestable": "Congratulations, you qualified for a certificate!",
"courseCelebration.certificateHeader.requestable": "恭喜!您已具备获得证书的资格!",
"courseCelebration.certificateHeader.upgradable": "Upgrade to pursue a verified certificate",
"courseCelebration.certificateImage": "Sample certificate",
"courseCelebration.completedCourseHeader": "You have completed your course.",
"courseCelebration.congratulationsHeader": "Congratulations!",
"courseCelebration.congratulationsHeader": "恭喜!",
"courseCelebration.congratulationsImage": "Four people raising their hands in celebration",
"courseExit.courseInProgressDescription": "It looks like there is more content in this course that will be released in the future. Look out for email updates or check back on your course for when this content will be available.",
"courseExit.courseInProgressHeader": "More content is coming soon!",
"courseExit.dashboardLink": "Dashboard",
"courseExit.dashboardLink": "课程面板",
"courseExit.endOfCourseDescription": "Unfortunately, you are not currently eligible for a certificate. You need to receive a passing grade to be eligible for a certificate.",
"courseExit.endOfCourseHeader": "Youve reached the end of the course!",
"courseExit.endOfCourseTitle": "End of Course",
"courseExit.idVerificationSupportLink": "Learn more about ID verification",
"courseCelebration.linkedinAddToProfileButton": "Add to LinkedIn profile",
"courseCelebration.linkedinAddToProfileButton": "添加至LinkedIn用户资料中",
"courseExit.programs.microBachelors.learnMore": "Learn more about how your MicroBachelors credential can be applied for credit.",
"courseExit.programs.microMasters.learnMore": "Learn more about the process of applying MicroMasters certificates to Masters degrees.",
"courseExit.programs.microMasters.mastersMessage": "If youre interested in using your MicroMasters certificate towards a Masters program, you can get started today!",
"learn.sequence.navigation.complete.button": "Complete the course",
"learn.sequence.navigation.complete.button": "完成课程",
"courseExit.nextButton.endOfCourse": "Next (end of course)",
"courseExit.profileLink": "Profile",
"courseExit.profileLink": "个人主页",
"courseExit.programs.lastCourse": "You have completed the last course in {title}!",
"courseCelebration.requestCertificateBodyText": "In order to access your certificate, request it below.",
"courseCelebration.requestCertificateButton": "Request certificate",
"courseExit.searchOurCatalogLink": "Search our catalog",
"courseCelebration.shareMessage": "Share your success on social media or email.",
"courseExit.social.shareCompletionMessage": "I just completed {title} with {platform}!",
"courseExit.upgradeButton": "Upgrade now",
"courseExit.upgradeButton": "马上升级",
"courseExit.upgradeLink": "upgrade now",
"courseCelebration.verificationPending": "Your ID verification is pending and your certificate will be available once approved.",
"courseExit.verifiedCertificateSupportLink": "Learn more about verified certificates",
@@ -319,31 +320,31 @@
"courseExit.viewGradesButton": "View grades",
"courseExit.programCompletion.dashboardMessage": "To view your certificate status, check the Programs section of your {programLink}.",
"courseExit.upgradeFootnote": "Access to this course and its materials are available on your dashboard until {expirationDate}. To extend access, {upgradeLink}.",
"learn.course.license.allRightsReserved.text": "All Rights Reserved",
"learn.course.license.allRightsReserved.text": "保留所有权利",
"learn.course.license.creativeCommons.terms.preamble": "Creative Commons licensed content, with terms as follows:",
"learn.course.license.creativeCommons.terms.by": "Attribution",
"learn.course.license.creativeCommons.terms.nc": "Noncommercial",
"learn.course.license.creativeCommons.terms.nd": "No Derivatives",
"learn.course.license.creativeCommons.terms.sa": "Share Alike",
"learn.course.license.creativeCommons.terms.by": "署名",
"learn.course.license.creativeCommons.terms.nc": "非营利性的",
"learn.course.license.creativeCommons.terms.nd": "无相关衍生作品",
"learn.course.license.creativeCommons.terms.sa": "分享",
"learn.course.license.creativeCommons.terms.zero": "No terms",
"learn.course.license.creativeCommons.text": "Some Rights Reserved",
"learn.breadcrumb.navigation.course.home": "Course",
"learn.course.license.creativeCommons.text": "保留部分权利",
"learn.breadcrumb.navigation.course.home": "课程",
"notification.tray.container": "Notification tray",
"notification.open.button": "Show notification tray",
"notification.close.button": "Close notification tray",
"responsive.close.notification": "Back to course",
"notification.tray.title": "Notifications",
"notification.tray.title": "通知",
"notification.tray.no.message": "You have no new notifications at this time.",
"learn.contentLock.content.locked": "Content Locked",
"learn.contentLock.content.locked": "未解锁内容",
"learn.contentLock.complete.prerequisite": "You must complete the prerequisite: ''{prereqSectionName}'' to access this content.",
"learn.contentLock.goToSection": "Go To Prerequisite Section",
"learn.hiddenAfterDue.gradeAvailable": "If you have completed this assignment, your grade is available on the {progressPage}.",
"learn.hiddenAfterDue.header": "The due date for this assignment has passed.",
"learn.hiddenAfterDue.header": "本次作业已经过了截止日期。",
"learn.hiddenAfterDue.description": "Because the due date has passed, this assignment is no longer available.",
"learn.hiddenAfterDue.progressPage": "progress page",
"learn.honorCode.content": "Honesty and academic integrity are important to {siteName} and the institutions providing courses and programs on the {siteName} site. By clicking “I agree” below, I confirm that I have read, understand, and will abide by the {link} for the {siteName} Site.",
"learn.honorCode.name": "Honor Code",
"learn.honorCode.cancel": "Cancel",
"learn.honorCode.name": "诚信准则",
"learn.honorCode.cancel": "取消",
"learn.honorCode.agree": "I agree",
"learn.lockPaywall.title": "Graded assignments are locked",
"learn.lockPaywall.content": "Upgrade to gain access to locked features like this one and get the most out of your course.",
@@ -357,25 +358,25 @@
"learn.loading.content.lock": "Loading locked content messaging...",
"learn.loading.learning.sequence": "Loading learning sequence...",
"learn.sequence.no.content": "There is no content here.",
"learn.sequence.navigation.next.button": "Next",
"learn.sequence.navigation.next.button": "下一节",
"learn.sequence.navigation.next.up.button": "Next Up: {title}",
"learn.sequence.navigation.previous.button": "Previous",
"learn.sequence.navigation.previous.button": "上一节",
"learn.course.sequence.navigation.mobile.menu": "{current} of {total}",
"learn.sequence.share.button": "Share this content",
"learn.sequence.share.modal.title": "Title",
"learn.sequence.share.modal.title": "标题",
"learn.sequence.share.modal.body": "Copy the link below to share this content.",
"learn.sequence.share.quote": "Here's a fun clip from a class I'm taking on @edXonline.\n",
"discussions.sidebar.title": "Discussions",
"discussions.sidebar.title": "讨论",
"discussions.sidebar.open.button": "Show discussions tray",
"learn.redirect.interstitial.message": "Redirecting...",
"learn.loading.error": "Error: {error}",
"learn.loading.error": "错误: {error}",
"learning.celebration.emailBody": "What are you spending your time learning?",
"learning.social.shareEmail": "Share your progress via email.",
"learning.social.shareService": "Share your progress on {service}.",
"general.altText.close": "Close",
"learning.logistration.register": "register",
"learning.logistration.login": "sign in",
"general.signIn.sentenceCase": "Sign in",
"general.altText.close": "关闭",
"learning.logistration.register": "注册",
"learning.logistration.login": "登录",
"general.signIn.sentenceCase": "登录",
"learn.course.tabs.navigation.overflow.menu": "More...",
"learning.offer.screenReaderPrices": "Original price: {originalPrice}, discount price: {discountedPrice}",
"learning.upgradeButton.screenReaderInlinePrices": "Original price: {originalPrice}",
@@ -395,7 +396,7 @@
"learning.generic.upgradeNotification.accessExpiration": "Upgrade your course today",
"learning.generic.upgradeNotification.accessExpirationUrgent": "Course Access Expiration",
"learning.generic.upgradeNotification.accessExpirationPast": "Course Access Expiration",
"learning.generic.upgradeNotification.pursueAverifiedCertificate": "Pursue a verified certificate",
"learning.generic.upgradeNotification.pursueAverifiedCertificate": "获取认证证书",
"learning.generic.upgradeNotification.code": "Use code {code} at checkout",
"learning.generic.upsell.verifiedCertBullet.verifiedCert": "verified certificate",
"learning.generic.upsell.verifiedCertBullet": "Earn a {verifiedCertLink} of completion to showcase on your resumé",
@@ -411,8 +412,8 @@
"tours.abandonTour.launchTourCheckpoint.body": "Feeling lost? Launch the tour any time for some quick tips to get the most out of the experience.",
"tours.sequenceNavigationCheckpoint.body": "The top bar within your course allows you to easily jump to different sections and shows you whats coming up.",
"tours.existingUserTour.launchTourCheckpoint.body": "Weve recently added a few new features to the course experience. Want some help looking around? Take a tour to learn more.",
"tours.button.dismiss": "Dismiss",
"tours.button.next": "Next",
"tours.button.dismiss": "忽略",
"tours.button.next": "下一节",
"tours.button.okay": "Okay",
"tours.button.beginTour": "Begin tour",
"tours.button.launchTour": "Launch tour",
@@ -434,7 +435,7 @@
"learning.effortEstimation.activities": "{activityCount, plural, one {# activity} other {# activities}}",
"learning.effortEstimation.minutesAbbreviated": "{minuteCount, plural, one {# min} other {# min}}",
"learning.effortEstimation.minutesFull": "{minuteCount, plural, one {# minute} other {# minutes}}",
"learning.streakCelebration.congratulations": "Congratulations!",
"learning.streakCelebration.congratulations": "恭喜!",
"learning.streakCelebration.body": "Keep it up, youre on a roll!",
"learning.streakCelebration.button": "Keep it up",
"learning.streakCelebration.buttonSrOnly": "Close modal and continue",

View File

@@ -37,6 +37,7 @@ import NoticesProvider from './generic/notices';
import PathFixesProvider from './generic/path-fixes';
import LiveTab from './course-home/live-tab/LiveTab';
import CourseAccessErrorPage from './generic/CourseAccessErrorPage';
import DecodePageRoute from './decode-page-route';
subscribe(APP_READY, () => {
ReactDOM.render(
@@ -50,28 +51,28 @@ subscribe(APP_READY, () => {
<Switch>
<PageRoute exact path="/goal-unsubscribe/:token" component={GoalUnsubscribe} />
<PageRoute path="/redirect" component={CoursewareRedirectLandingPage} />
<PageRoute path="/course/:courseId/access-denied" component={CourseAccessErrorPage} />
<PageRoute path="/course/:courseId/home">
<DecodePageRoute path="/course/:courseId/access-denied" component={CourseAccessErrorPage} />
<DecodePageRoute path="/course/:courseId/home">
<TabContainer tab="outline" fetch={fetchOutlineTab} slice="courseHome">
<OutlineTab />
</TabContainer>
</PageRoute>
<PageRoute path="/course/:courseId/live">
<TabContainer tab="live" fetch={fetchLiveTab} slice="courseHome">
</DecodePageRoute>
<DecodePageRoute path="/course/:courseId/live">
<TabContainer tab="lti_live" fetch={fetchLiveTab} slice="courseHome">
<LiveTab />
</TabContainer>
</PageRoute>
<PageRoute path="/course/:courseId/dates">
</DecodePageRoute>
<DecodePageRoute path="/course/:courseId/dates">
<TabContainer tab="dates" fetch={fetchDatesTab} slice="courseHome">
<DatesTab />
</TabContainer>
</PageRoute>
<PageRoute path="/course/:courseId/discussion/:path*">
</DecodePageRoute>
<DecodePageRoute path="/course/:courseId/discussion/:path*">
<TabContainer tab="discussion" fetch={fetchDiscussionTab} slice="courseHome">
<DiscussionTab />
</TabContainer>
</PageRoute>
<PageRoute
</DecodePageRoute>
<DecodePageRoute
path={[
'/course/:courseId/progress/:targetUserId/',
'/course/:courseId/progress',
@@ -86,12 +87,12 @@ subscribe(APP_READY, () => {
</TabContainer>
)}
/>
<PageRoute path="/course/:courseId/course-end">
<DecodePageRoute path="/course/:courseId/course-end">
<TabContainer tab="courseware" fetch={fetchCourse} slice="courseware">
<CourseExit />
</TabContainer>
</PageRoute>
<PageRoute
</DecodePageRoute>
<DecodePageRoute
path={[
'/course/:courseId/:sequenceId/:unitId',
'/course/:courseId/:sequenceId',
@@ -135,6 +136,9 @@ initialize({
TWITTER_HASHTAG: process.env.TWITTER_HASHTAG || null,
TWITTER_URL: process.env.TWITTER_URL || null,
LEGACY_THEME_NAME: process.env.LEGACY_THEME_NAME || null,
EXAMS_BASE_URL: process.env.EXAMS_BASE_URL || null,
PROCTORED_EXAM_FAQ_URL: process.env.PROCTORED_EXAM_FAQ_URL || null,
PROCTORED_EXAM_RULES_URL: process.env.PROCTORED_EXAM_RULES_URL || null,
}, 'LearnerAppConfig');
},
},

View File

@@ -233,7 +233,7 @@ describe('Courseware Tour', () => {
// Wait for the page spinner to be removed, such that we can wait for our main
// content to load before making any assertions.
await waitForElementToBeRemoved(screen.getByRole('status'));
return container;
return Promise.resolve(container);
}
describe('when receiving successful course data', () => {

View File

@@ -137,10 +137,12 @@ export async function initializeTestStore(options = {}, overrideStore = true) {
const discussionConfigUrl = new RegExp(`${getConfig().LMS_BASE_URL}/api/discussion/v1/courses/*`);
courseHomeMetadataUrl = appendBrowserTimezoneToUrl(courseHomeMetadataUrl);
const provider = options?.provider || 'legacy';
axiosMock.onGet(courseMetadataUrl).reply(200, courseMetadata);
axiosMock.onGet(courseHomeMetadataUrl).reply(200, courseHomeMetadata);
axiosMock.onGet(learningSequencesUrlRegExp).reply(200, buildOutlineFromBlocks(courseBlocks));
axiosMock.onGet(discussionConfigUrl).reply(200, { provider: 'legacy' });
axiosMock.onGet(discussionConfigUrl).reply(200, { provider });
sequenceMetadata.forEach(metadata => {
const sequenceMetadataUrl = `${getConfig().LMS_BASE_URL}/api/courseware/sequence/${metadata.item_id}`;
axiosMock.onGet(sequenceMetadataUrl).reply(200, metadata);