Compare commits
5 Commits
open-relea
...
ilee2u/bug
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
55dd7a84c6 | ||
|
|
91ced0050d | ||
|
|
fa4cd420bf | ||
|
|
bc87466e6c | ||
|
|
6c4d2fc88a |
3
.env
3
.env
@@ -15,7 +15,6 @@ ECOMMERCE_BASE_URL=''
|
||||
ENABLE_JUMPNAV='true'
|
||||
ENABLE_NOTICES=''
|
||||
ENTERPRISE_LEARNER_PORTAL_HOSTNAME=''
|
||||
EXAMS_BASE_URL=''
|
||||
FAVICON_URL=''
|
||||
IGNORED_ERROR_REGEX=''
|
||||
INSIGHTS_BASE_URL=''
|
||||
@@ -29,8 +28,6 @@ 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=''
|
||||
|
||||
@@ -15,7 +15,6 @@ 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'
|
||||
@@ -29,8 +28,6 @@ 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=''
|
||||
|
||||
@@ -15,7 +15,6 @@ 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'
|
||||
@@ -29,8 +28,6 @@ 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=''
|
||||
|
||||
@@ -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 }}
|
||||
@@ -1,20 +0,0 @@
|
||||
# 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
|
||||
|
||||
2
.github/workflows/lockfileversion-check.yml
vendored
2
.github/workflows/lockfileversion-check.yml
vendored
@@ -10,4 +10,4 @@ on:
|
||||
|
||||
jobs:
|
||||
version-check:
|
||||
uses: openedx/.github/.github/workflows/lockfileversion-check-v3.yml@master
|
||||
uses: openedx/.github/.github/workflows/lockfileversion-check.yml@master
|
||||
|
||||
12
.github/workflows/self-assign-issue.yml
vendored
12
.github/workflows/self-assign-issue.yml
vendored
@@ -1,12 +0,0 @@
|
||||
# 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
|
||||
7
.github/workflows/validate.yml
vendored
7
.github/workflows/validate.yml
vendored
@@ -9,13 +9,14 @@ 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: ${{ env.NODE_VER }}
|
||||
node-version: ${{ matrix.node }}
|
||||
- run: make validate.ci
|
||||
- name: Upload coverage
|
||||
uses: codecov/codecov-action@v3
|
||||
|
||||
40837
package-lock.json
generated
40837
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
25
package.json
25
package.json
@@ -17,7 +17,8 @@
|
||||
"prepare": "husky install",
|
||||
"snapshot": "fedx-scripts jest --updateSnapshot",
|
||||
"start": "fedx-scripts webpack-dev-server --progress",
|
||||
"test": "fedx-scripts jest --coverage --passWithNoTests"
|
||||
"test": "fedx-scripts jest --coverage --passWithNoTests",
|
||||
"testx": "fedx-scripts jest -t 'Outline Tab' --coverage --passWithNoTests"
|
||||
},
|
||||
"author": "edX",
|
||||
"license": "AGPL-3.0",
|
||||
@@ -29,12 +30,12 @@
|
||||
"url": "https://github.com/openedx/frontend-app-learning/issues"
|
||||
},
|
||||
"dependencies": {
|
||||
"@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",
|
||||
"@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",
|
||||
"@fortawesome/fontawesome-svg-core": "1.3.0",
|
||||
"@fortawesome/free-brands-svg-icons": "5.15.4",
|
||||
"@fortawesome/free-regular-svg-icons": "5.15.4",
|
||||
@@ -44,18 +45,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.3",
|
||||
"query-string": "^7.1.1",
|
||||
"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",
|
||||
@@ -64,7 +65,7 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@edx/browserslist-config": "1.1.1",
|
||||
"@edx/frontend-build": "^12.8.27",
|
||||
"@edx/frontend-build": "^12.4.15",
|
||||
"@edx/reactifex": "2.1.1",
|
||||
"@pact-foundation/pact": "9.17.3",
|
||||
"@testing-library/jest-dom": "5.16.5",
|
||||
@@ -74,7 +75,7 @@
|
||||
"copy-webpack-plugin": "^11.0.0",
|
||||
"es-check": "6.2.1",
|
||||
"husky": "7.0.4",
|
||||
"jest": "29.5.0",
|
||||
"jest": "27.5.1",
|
||||
"rosie": "2.1.0"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -39,186 +39,183 @@ 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 () => {
|
||||
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'),
|
||||
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,
|
||||
},
|
||||
},
|
||||
});
|
||||
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',
|
||||
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: 'Demonstration Course',
|
||||
username: 'edx',
|
||||
};
|
||||
const response = getCourseHomeCourseMetadata(courseId, 'outline');
|
||||
expect(response).toBeTruthy();
|
||||
expect(response).toEqual(normalizedTabData);
|
||||
}, 100);
|
||||
}),
|
||||
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);
|
||||
});
|
||||
});
|
||||
|
||||
describe('When a request to fetch dates tab is made', () => {
|
||||
it('returns course date blocks for a course_id', async () => {
|
||||
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,
|
||||
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,
|
||||
complete: null,
|
||||
date: '2013-02-05T05:00:00Z',
|
||||
dateType: 'verified-upgrade-deadline',
|
||||
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.',
|
||||
learnerHasAccess: true,
|
||||
learner_has_access: true,
|
||||
link: `${getConfig().ECOMMERCE_BASE_URL}/basket/add/?sku=8CF08E5`,
|
||||
linkText: 'Upgrade to Verified Certificate',
|
||||
link_text: 'Upgrade to Verified Certificate',
|
||||
title: 'Verification Upgrade Deadline',
|
||||
extraInfo: null,
|
||||
firstComponentBlockId: '',
|
||||
},
|
||||
],
|
||||
hasEnded: false,
|
||||
learnerIsFullAccess: true,
|
||||
userTimezone: null,
|
||||
};
|
||||
const response = getDatesTabData(courseId);
|
||||
expect(response).toBeTruthy();
|
||||
expect(response).toEqual(camelCaseResponse);
|
||||
}, 100);
|
||||
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);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -38,21 +38,21 @@ function getBadgeListAndColor(date, intl, item, items) {
|
||||
message: messages.today,
|
||||
shownForDay: isToday,
|
||||
bg: 'bg-warning-300',
|
||||
className: 'text-dark',
|
||||
className: 'text-black',
|
||||
},
|
||||
{
|
||||
message: messages.completed,
|
||||
shownForDay: assignments.length && assignments.every(isComplete),
|
||||
shownForItem: x => isLearnerAssignment(x) && isComplete(x),
|
||||
bg: 'bg-light-500',
|
||||
className: 'text-dark',
|
||||
className: 'text-black',
|
||||
},
|
||||
{
|
||||
message: messages.pastDue,
|
||||
shownForDay: assignments.length && assignments.every(isPastDue),
|
||||
shownForItem: x => isLearnerAssignment(x) && isPastDue(x),
|
||||
bg: 'bg-dark-200',
|
||||
className: 'text-dark',
|
||||
className: 'text-white',
|
||||
},
|
||||
{
|
||||
message: messages.dueNext,
|
||||
|
||||
@@ -9,9 +9,8 @@ const LmsHtmlFragment = ({
|
||||
title,
|
||||
...rest
|
||||
}) => {
|
||||
const direction = document.documentElement?.getAttribute('dir') || 'ltr';
|
||||
const wholePage = `
|
||||
<html dir="${direction}">
|
||||
<html>
|
||||
<head>
|
||||
<base href="${getConfig().LMS_BASE_URL}" target="_parent">
|
||||
<link rel="stylesheet" href="/static/${getConfig().LEGACY_THEME_NAME ? `${getConfig().LEGACY_THEME_NAME}/` : ''}css/bootstrap/lms-main.css">
|
||||
|
||||
@@ -23,6 +23,8 @@ 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');
|
||||
|
||||
@@ -119,11 +121,11 @@ describe('Outline Tab', () => {
|
||||
|
||||
// Click to expand section
|
||||
userEvent.click(expandButton);
|
||||
await waitFor(() => expect(collapsedSectionNode).toHaveAttribute('aria-expanded', 'true'));
|
||||
expect(collapsedSectionNode).toHaveAttribute('aria-expanded', 'true');
|
||||
|
||||
// Click to collapse section
|
||||
userEvent.click(expandButton);
|
||||
await waitFor(() => expect(collapsedSectionNode).toHaveAttribute('aria-expanded', 'false'));
|
||||
expect(collapsedSectionNode).toHaveAttribute('aria-expanded', 'false');
|
||||
});
|
||||
|
||||
it('displays correct icon for complete assignment', async () => {
|
||||
@@ -154,6 +156,49 @@ 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', () => {
|
||||
|
||||
@@ -11,7 +11,7 @@ import MockAdapter from 'axios-mock-adapter';
|
||||
|
||||
import { UserMessagesProvider } from '../generic/user-messages';
|
||||
import tabMessages from '../tab-page/messages';
|
||||
import { initializeMockApp, waitFor } from '../setupTest';
|
||||
import { initializeMockApp } 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 waitFor(() => loadContainer());
|
||||
const container = await 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 waitFor(() => loadContainer());
|
||||
const container = await 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 waitFor(() => loadContainer());
|
||||
const container = await 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 waitFor(() => loadContainer());
|
||||
const container = await loadContainer();
|
||||
|
||||
assertLoadedHeader(container);
|
||||
assertSequenceNavigation(container);
|
||||
@@ -378,7 +378,7 @@ describe('CoursewareContainer', () => {
|
||||
setUpMockRequests({ sequenceMetadatas: [sequenceMetadata] });
|
||||
|
||||
history.push(`/course/${courseId}/${sequenceBlock.id}`);
|
||||
const container = await waitFor(() => loadContainer());
|
||||
const container = await 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 waitFor(() => loadContainer());
|
||||
const container = await loadContainer();
|
||||
|
||||
assertLoadedHeader(container);
|
||||
assertSequenceNavigation(container);
|
||||
@@ -411,7 +411,7 @@ describe('CoursewareContainer', () => {
|
||||
});
|
||||
|
||||
history.push(`/course/${courseId}/${sequenceBlock.id}/${unitBlocks[0].id}`);
|
||||
const container = await waitFor(() => loadContainer());
|
||||
const container = await loadContainer();
|
||||
|
||||
const sequenceNavButtons = container.querySelectorAll('nav.sequence-navigation button');
|
||||
const sequenceNextButton = sequenceNavButtons[4];
|
||||
|
||||
@@ -7,8 +7,6 @@ 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 (
|
||||
@@ -23,7 +21,7 @@ const CoursewareRedirectLandingPage = () => {
|
||||
/>
|
||||
|
||||
<Switch>
|
||||
<DecodePageRoute
|
||||
<PageRoute
|
||||
path={`${path}/survey/:courseId`}
|
||||
render={({ match }) => {
|
||||
global.location.assign(`${getConfig().LMS_BASE_URL}/courses/${match.params.courseId}/survey`);
|
||||
@@ -42,7 +40,7 @@ const CoursewareRedirectLandingPage = () => {
|
||||
global.location.assign(`${getConfig().LMS_BASE_URL}${consentPath}`);
|
||||
}}
|
||||
/>
|
||||
<DecodePageRoute
|
||||
<PageRoute
|
||||
path={`${path}/home/:courseId`}
|
||||
render={({ match }) => {
|
||||
global.location.assign(`/course/${match.params.courseId}/home`);
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import React, { useState } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { Helmet } from 'react-helmet';
|
||||
import { useDispatch } from 'react-redux';
|
||||
@@ -43,8 +43,10 @@ const Course = ({
|
||||
|
||||
// Below the tabs, above the breadcrumbs alerts (appearing in the order listed here)
|
||||
const dispatch = useDispatch();
|
||||
|
||||
const [firstSectionCelebrationOpen, setFirstSectionCelebrationOpen] = useState(false);
|
||||
const celebrateFirstSection = celebrations && celebrations.firstSection;
|
||||
const [firstSectionCelebrationOpen, setFirstSectionCelebrationOpen] = useState(
|
||||
shouldCelebrateOnSectionLoad(courseId, sequenceId, celebrateFirstSection, dispatch, celebrations),
|
||||
);
|
||||
// If streakLengthToCelebrate is populated, that modal takes precedence. Wait til the next load to display
|
||||
// the weekly goal celebration modal.
|
||||
const [weeklyGoalCelebrationOpen, setWeeklyGoalCelebrationOpen] = useState(
|
||||
@@ -66,17 +68,6 @@ const Course = ({
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
const celebrateFirstSection = celebrations && celebrations.firstSection;
|
||||
setFirstSectionCelebrationOpen(shouldCelebrateOnSectionLoad(
|
||||
courseId,
|
||||
sequenceId,
|
||||
celebrateFirstSection,
|
||||
dispatch,
|
||||
celebrations,
|
||||
));
|
||||
}, [sequenceId]);
|
||||
|
||||
return (
|
||||
<SidebarProvider courseId={courseId} unitId={unitId}>
|
||||
<Helmet>
|
||||
|
||||
@@ -1,18 +1,12 @@
|
||||
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 {
|
||||
act, fireEvent, getByRole, initializeTestStore, loadUnit, render, screen, waitFor,
|
||||
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');
|
||||
|
||||
@@ -49,28 +43,6 @@ 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();
|
||||
@@ -131,7 +103,6 @@ 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 });
|
||||
@@ -143,14 +114,13 @@ 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 })).not.toHaveClass('d-none');
|
||||
expect(screen.queryByRole('region', { name: /notification tray/i })).toBeInTheDocument();
|
||||
fireEvent.click(notificationShowButton);
|
||||
expect(sessionStorage.getItem(`notificationTrayStatus.${mockData.courseId}`)).toBe('"closed"');
|
||||
expect(screen.queryByRole('region', { name: /notification tray/i })).toHaveClass('d-none');
|
||||
expect(screen.queryByRole('region', { name: /notification tray/i })).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('handles reload persisting notification tray status', async () => {
|
||||
@@ -174,7 +144,6 @@ 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
|
||||
@@ -217,34 +186,6 @@ 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();
|
||||
|
||||
@@ -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,8 +32,7 @@ describe('Bookmark Button', () => {
|
||||
mockData.unitId = nonBookmarkedUnitBlock.id;
|
||||
|
||||
axiosMock = new MockAdapter(getAuthenticatedHttpClient());
|
||||
const bookmarkUrl = getBookmarksBaseUrl();
|
||||
|
||||
const bookmarkUrl = `${getConfig().LMS_BASE_URL}/api/bookmarks/v1/bookmarks/`;
|
||||
axiosMock.onPost(bookmarkUrl).reply(200, { });
|
||||
|
||||
const bookmarkDeleteUrlRegExp = new RegExp(`${bookmarkUrl}*,*`);
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
import { getConfig } from '@edx/frontend-platform';
|
||||
import { getAuthenticatedHttpClient, getAuthenticatedUser } from '@edx/frontend-platform/auth';
|
||||
|
||||
export const getBookmarksBaseUrl = () => `${getConfig().LMS_BASE_URL}/api/bookmarks/v1/bookmarks/`;
|
||||
const bookmarksBaseUrl = `${getConfig().LMS_BASE_URL}/api/bookmarks/v1/bookmarks/`;
|
||||
|
||||
export async function createBookmark(usageId) {
|
||||
return getAuthenticatedHttpClient().post(getBookmarksBaseUrl(), { usage_id: usageId });
|
||||
return getAuthenticatedHttpClient().post(bookmarksBaseUrl, { usage_id: usageId });
|
||||
}
|
||||
|
||||
export async function deleteBookmark(usageId) {
|
||||
const { username } = getAuthenticatedUser();
|
||||
return getAuthenticatedHttpClient().delete(`${getBookmarksBaseUrl()}${username},${usageId}/`);
|
||||
return getAuthenticatedHttpClient().delete(`${bookmarksBaseUrl}${username},${usageId}/`);
|
||||
}
|
||||
|
||||
@@ -9,7 +9,6 @@ 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';
|
||||
@@ -20,13 +19,12 @@ import { useModel } from '../../../generic/model-store';
|
||||
const CelebrationModal = ({
|
||||
courseId, intl, isOpen, onClose, ...rest
|
||||
}) => {
|
||||
const { org, celebrations } = useModel('courseHomeMeta', courseId);
|
||||
const dispatch = useDispatch();
|
||||
const { org } = useModel('courseHomeMeta', courseId);
|
||||
const wideScreen = useWindowSize().width >= breakpoints.small.minWidth;
|
||||
|
||||
useEffect(() => {
|
||||
if (isOpen) {
|
||||
recordFirstSectionCelebration(org, courseId, celebrations, dispatch);
|
||||
recordFirstSectionCelebration(org, courseId);
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [isOpen]);
|
||||
|
||||
@@ -15,20 +15,9 @@ function handleNextSectionCelebration(sequenceId, nextSequenceId) {
|
||||
});
|
||||
}
|
||||
|
||||
function recordFirstSectionCelebration(org, courseId, celebrations, dispatch) {
|
||||
function recordFirstSectionCelebration(org, courseId) {
|
||||
// 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();
|
||||
|
||||
@@ -1,15 +0,0 @@
|
||||
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();
|
||||
});
|
||||
});
|
||||
@@ -1,5 +1,6 @@
|
||||
.content-tools {
|
||||
position: fixed;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
z-index: 100;
|
||||
|
||||
@@ -144,29 +144,27 @@ const Sequence = ({
|
||||
};
|
||||
|
||||
const defaultContent = (
|
||||
<div className="sequence-container d-inline-flex flex-row w-100">
|
||||
<div className="sequence-container d-inline-flex flex-row">
|
||||
<div className={classNames('sequence w-100', { 'position-relative': shouldDisplayNotificationTriggerInSequence })}>
|
||||
<div className="sequence-navigation-container">
|
||||
<SequenceNavigation
|
||||
sequenceId={sequenceId}
|
||||
unitId={unitId}
|
||||
className="mb-4"
|
||||
nextSequenceHandler={() => {
|
||||
logEvent('edx.ui.lms.sequence.next_selected', 'top');
|
||||
handleNext();
|
||||
}}
|
||||
onNavigate={(destinationUnitId) => {
|
||||
logEvent('edx.ui.lms.sequence.tab_selected', 'top', destinationUnitId);
|
||||
handleNavigate(destinationUnitId);
|
||||
}}
|
||||
previousSequenceHandler={() => {
|
||||
logEvent('edx.ui.lms.sequence.previous_selected', 'top');
|
||||
handlePrevious();
|
||||
}}
|
||||
goToCourseExitPage={() => goToCourseExitPage()}
|
||||
/>
|
||||
{shouldDisplayNotificationTriggerInSequence && <SidebarTriggers />}
|
||||
</div>
|
||||
<SequenceNavigation
|
||||
sequenceId={sequenceId}
|
||||
unitId={unitId}
|
||||
className="mb-4"
|
||||
nextSequenceHandler={() => {
|
||||
logEvent('edx.ui.lms.sequence.next_selected', 'top');
|
||||
handleNext();
|
||||
}}
|
||||
onNavigate={(destinationUnitId) => {
|
||||
logEvent('edx.ui.lms.sequence.tab_selected', 'top', destinationUnitId);
|
||||
handleNavigate(destinationUnitId);
|
||||
}}
|
||||
previousSequenceHandler={() => {
|
||||
logEvent('edx.ui.lms.sequence.previous_selected', 'top');
|
||||
handlePrevious();
|
||||
}}
|
||||
goToCourseExitPage={() => goToCourseExitPage()}
|
||||
/>
|
||||
{shouldDisplayNotificationTriggerInSequence && <SidebarTriggers />}
|
||||
|
||||
<div className="unit-container flex-grow-1">
|
||||
<SequenceContent
|
||||
|
||||
@@ -74,8 +74,8 @@ describe('Sequence', () => {
|
||||
);
|
||||
|
||||
await waitFor(() => expect(screen.queryByText('Loading locked content messaging...')).toBeInTheDocument());
|
||||
// `Previous`, `Active`, `Next`, `Prerequisite` and `Close Tray` buttons.
|
||||
expect(screen.getAllByRole('button').length).toEqual(5);
|
||||
// `Previous`, `Active`, `Next` and `Prerequisite` buttons.
|
||||
expect(screen.getAllByRole('button').length).toEqual(4);
|
||||
|
||||
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(4 + unitBlocks.length);
|
||||
expect(screen.getAllByRole('button')).toHaveLength(3 + unitBlocks.length);
|
||||
|
||||
loadUnit();
|
||||
await waitFor(() => expect(screen.queryByText('Loading learning sequence...')).not.toBeInTheDocument());
|
||||
|
||||
@@ -93,7 +93,6 @@ 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);
|
||||
@@ -121,13 +120,6 @@ 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) {
|
||||
@@ -137,16 +129,12 @@ 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, setWindowTopOffset, windowTopOffset]);
|
||||
}, [id, setIframeHeight, hasLoaded, iframeHeight, setHasLoaded, onLoaded]);
|
||||
useEventListener('message', receiveMessage);
|
||||
useEffect(() => {
|
||||
sendUrlHashToFrame(document.getElementById('unit-iframe'));
|
||||
|
||||
@@ -129,21 +129,6 @@ 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' };
|
||||
|
||||
@@ -80,7 +80,7 @@ const SequenceNavigation = ({
|
||||
const prevArrow = isRtl(getLocale()) ? ChevronRight : ChevronLeft;
|
||||
|
||||
return sequenceStatus === LOADED && (
|
||||
<nav id="courseware-sequenceNavigation" className={classNames('sequence-navigation', className, { 'mr-2': shouldDisplayNotificationTriggerInSequence })}>
|
||||
<nav id="courseware-sequenceNavigation" className={classNames('sequence-navigation', className)} style={{ width: shouldDisplayNotificationTriggerInSequence ? '90%' : null }}>
|
||||
<Button variant="link" className="previous-btn" onClick={previousSequenceHandler} disabled={isFirstUnit} iconBefore={prevArrow}>
|
||||
{shouldDisplayNotificationTriggerInSequence ? null : intl.formatMessage(messages.previousButton)}
|
||||
</Button>
|
||||
|
||||
@@ -1,15 +1,18 @@
|
||||
import React from 'react';
|
||||
import { SIDEBAR_ORDER, SIDEBARS } from './sidebars';
|
||||
import React, { useContext } from 'react';
|
||||
import SidebarContext from './SidebarContext';
|
||||
import { SIDEBARS } from './sidebars';
|
||||
|
||||
const Sidebar = () => (
|
||||
<>
|
||||
{
|
||||
SIDEBAR_ORDER.map((sideBarId) => {
|
||||
const SidebarToRender = SIDEBARS[sideBarId].Sidebar;
|
||||
return <SidebarToRender />;
|
||||
})
|
||||
}
|
||||
</>
|
||||
);
|
||||
const Sidebar = () => {
|
||||
const {
|
||||
currentSidebar,
|
||||
} = useContext(SidebarContext);
|
||||
if (!currentSidebar) {
|
||||
return null;
|
||||
}
|
||||
const CurrentSidebar = SIDEBARS[currentSidebar].Sidebar;
|
||||
return (
|
||||
<CurrentSidebar />
|
||||
);
|
||||
};
|
||||
|
||||
export default Sidebar;
|
||||
|
||||
@@ -19,17 +19,9 @@ const SidebarProvider = ({
|
||||
const shouldDisplayFullScreen = useWindowSize().width < breakpoints.large.minWidth;
|
||||
const shouldDisplaySidebarOpen = useWindowSize().width > breakpoints.medium.minWidth;
|
||||
const showNotificationsOnLoad = getSessionStorage(`notificationTrayStatus.${courseId}`) !== 'closed';
|
||||
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)
|
||||
const initialSidebar = (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}`));
|
||||
@@ -49,11 +41,6 @@ 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]);
|
||||
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import classNames from 'classnames';
|
||||
import React, { useContext } from 'react';
|
||||
import { breakpoints, useWindowSize } from '@edx/paragon';
|
||||
import SidebarContext from './SidebarContext';
|
||||
import { SIDEBAR_ORDER, SIDEBARS } from './sidebars';
|
||||
|
||||
@@ -9,9 +8,6 @@ const SidebarTriggers = () => {
|
||||
toggleSidebar,
|
||||
currentSidebar,
|
||||
} = useContext(SidebarContext);
|
||||
|
||||
const isMobileView = useWindowSize().width < breakpoints.small.minWidth;
|
||||
|
||||
return (
|
||||
<div className="d-flex ml-auto">
|
||||
{SIDEBAR_ORDER.map((sidebarId) => {
|
||||
@@ -19,7 +15,7 @@ const SidebarTriggers = () => {
|
||||
const isActive = sidebarId === currentSidebar;
|
||||
return (
|
||||
<div
|
||||
className={classNames({ 'mt-3': !isMobileView, 'border-primary-700': isActive })}
|
||||
className={classNames('mt-3', { 'border-primary-700': isActive })}
|
||||
style={{ borderBottom: isActive ? '2px solid' : null }}
|
||||
key={sidebarId}
|
||||
>
|
||||
|
||||
@@ -34,14 +34,12 @@ const SidebarBase = ({
|
||||
|
||||
useEventListener('message', receiveMessage);
|
||||
|
||||
return (
|
||||
return currentSidebar === sidebarId && (
|
||||
<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}
|
||||
>
|
||||
|
||||
@@ -8,7 +8,7 @@ const SidebarTriggerBase = ({
|
||||
children,
|
||||
}) => (
|
||||
<button
|
||||
className="border border-light-400 bg-transparent align-items-center align-content-center d-flex notification-btn"
|
||||
className="border border-light-400 bg-transparent align-items-center align-content-center d-flex"
|
||||
type="button"
|
||||
onClick={onClick}
|
||||
aria-label={ariaLabel}
|
||||
|
||||
@@ -34,8 +34,6 @@ 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>
|
||||
);
|
||||
|
||||
@@ -45,6 +45,25 @@ 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': {
|
||||
@@ -57,32 +76,74 @@ describe('Courseware Service', () => {
|
||||
sections: {},
|
||||
sequences: {},
|
||||
};
|
||||
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);
|
||||
const response = await getLearningSequencesOutline(courseId);
|
||||
expect(response).toEqual(normalizedOutline);
|
||||
});
|
||||
|
||||
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': {
|
||||
@@ -118,78 +179,120 @@ describe('Courseware Service', () => {
|
||||
},
|
||||
},
|
||||
};
|
||||
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);
|
||||
const response = await getLearningSequencesOutline(courseId);
|
||||
expect(response).toEqual(normalizedOutline);
|
||||
});
|
||||
});
|
||||
|
||||
describe('When a request to get course metadata is made', () => {
|
||||
it('returns normalized course metadata', () => {
|
||||
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),
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const normalizedCourseMetadata = {
|
||||
accessExpiration: {
|
||||
expirationDate: '2013-02-05T05:00:00Z',
|
||||
@@ -234,122 +337,56 @@ describe('Courseware Service', () => {
|
||||
relatedPrograms: null,
|
||||
userNeedsIntegritySignature: false,
|
||||
};
|
||||
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);
|
||||
const response = await getCourseMetadata(courseId);
|
||||
expect(response).toBeTruthy();
|
||||
expect(response).toEqual(normalizedCourseMetadata);
|
||||
});
|
||||
});
|
||||
|
||||
describe('When a request to get sequence metadata is made', () => {
|
||||
it('returns normalized sequence metadata ', () => {
|
||||
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'),
|
||||
},
|
||||
},
|
||||
});
|
||||
const normalizedSequenceMetadata = {
|
||||
sequence: {
|
||||
id: 'block-v1:edX+DemoX+Demo_Course+type@sequential+block@basic_questions',
|
||||
@@ -386,154 +423,102 @@ describe('Courseware Service', () => {
|
||||
containsContentTypeGatedContent: false,
|
||||
}],
|
||||
};
|
||||
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);
|
||||
const response = await getSequenceMetadata(sequenceId);
|
||||
expect(response).toBeTruthy();
|
||||
expect(response).toEqual(normalizedSequenceMetadata);
|
||||
});
|
||||
});
|
||||
|
||||
describe('When a request to set sequence position against Unit Index is made', () => {
|
||||
it('returns if the request was success or failure', async () => {
|
||||
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.
|
||||
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),
|
||||
},
|
||||
willRespondWith: {
|
||||
status: 200,
|
||||
body: {
|
||||
success: boolean(true),
|
||||
},
|
||||
},
|
||||
});
|
||||
const response = postSequencePosition(courseId, sequenceId, 0);
|
||||
expect(response).toBeTruthy();
|
||||
expect(response).toEqual({ success: true });
|
||||
}, 100);
|
||||
},
|
||||
});
|
||||
const response = await postSequencePosition(courseId, sequenceId, 0);
|
||||
expect(response).toBeTruthy();
|
||||
expect(response).toEqual({ success: true });
|
||||
});
|
||||
});
|
||||
|
||||
describe('When a request to get completion block is made', () => {
|
||||
it('returns the completion status', async () => {
|
||||
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 },
|
||||
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),
|
||||
},
|
||||
willRespondWith: {
|
||||
status: 200,
|
||||
body: {
|
||||
complete: boolean(true),
|
||||
},
|
||||
},
|
||||
});
|
||||
const response = getBlockCompletion(courseId, sequenceId, usageId);
|
||||
expect(response).toBeTruthy();
|
||||
expect(response).toEqual(true);
|
||||
}, 100);
|
||||
},
|
||||
});
|
||||
const response = await getBlockCompletion(courseId, sequenceId, usageId);
|
||||
expect(response).toBeTruthy();
|
||||
expect(response).toEqual(true);
|
||||
});
|
||||
});
|
||||
|
||||
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',
|
||||
};
|
||||
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);
|
||||
const response = await getResumeBlock(courseId);
|
||||
expect(response).toBeTruthy();
|
||||
expect(response).toEqual(camelCaseResponse);
|
||||
});
|
||||
});
|
||||
|
||||
describe('When a request to send activation email is made', () => {
|
||||
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);
|
||||
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('');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,16 +0,0 @@
|
||||
// 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>
|
||||
`;
|
||||
@@ -1,49 +0,0 @@
|
||||
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;
|
||||
@@ -1,103 +0,0 @@
|
||||
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);
|
||||
});
|
||||
@@ -10,19 +10,16 @@ import { getNotices } from './api';
|
||||
*/
|
||||
const NoticesProvider = ({ children }) => {
|
||||
const [isRedirected, setIsRedirected] = useState();
|
||||
|
||||
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}`);
|
||||
}
|
||||
// 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}`);
|
||||
}
|
||||
}
|
||||
getData();
|
||||
}, []);
|
||||
|
||||
return (
|
||||
|
||||
@@ -6,7 +6,6 @@ const invisibleStyle = {
|
||||
left: 0,
|
||||
pointerEvents: 'none',
|
||||
visibility: 'hidden',
|
||||
maxWidth: '100%',
|
||||
};
|
||||
|
||||
/**
|
||||
|
||||
@@ -22,8 +22,9 @@
|
||||
// An additional Font Awesome stylesheet is imported by Braze in
|
||||
// stage/production but not devstack.
|
||||
.upgrade-notification-ul.fa-ul {
|
||||
padding: 0.875rem 1.25rem 0;
|
||||
margin: 0 0 1rem 2.5rem;
|
||||
padding-left: 1.25rem;
|
||||
padding-top: 0.875rem;
|
||||
padding-right: 1.25rem;
|
||||
}
|
||||
|
||||
.upgrade-notification-text {
|
||||
|
||||
@@ -114,8 +114,7 @@
|
||||
"learning.proctoringPanel.onboardingButtonNotOpen": "يفتح الامتحان التحضيري في: {releaseDate}",
|
||||
"learning.proctoringPanel.reviewRequirementsButton": "مراجعة التعليمات و متطلبات النظام",
|
||||
"learning.proctoringPanel.onboardingButtonPastDue": "انقضى أجَل الامتحان التحضيري",
|
||||
"learning.outline.sequence-due-date-set": "{description} due {assignmentDue}",
|
||||
"learning.outline.sequence-due-date-not-set": "{description}",
|
||||
"learning.outline.sequence-due": "{description} للتسليم قبل {assignmentDue}",
|
||||
"progress.certificateStatus.unverifiedBody": "لتوليد شهادة، يجب عليك إكمال التحقق من هويتك. {idVerificationSupportLink}.",
|
||||
"progress.certificateStatus.downloadableBody": "اعرض إنجازاتك اليوم على لينكد إن أو ضمن سيرتك الذاتية. يمكنك تحميل شهادتك الآن و الوصول إليها في أي وقت من لوحة معلوماتك و ملفك الشخصي.",
|
||||
"courseCelebration.certificateBody.notAvailable.endDate": "تمت جدولة ظهور الدرجات النهائية و أي شهادات مكتسبة ابتداءً من {endDate}.",
|
||||
|
||||
@@ -114,8 +114,7 @@
|
||||
"learning.proctoringPanel.onboardingButtonNotOpen": "Onboarding Opens: {releaseDate}",
|
||||
"learning.proctoringPanel.reviewRequirementsButton": "Review instructions and system requirements",
|
||||
"learning.proctoringPanel.onboardingButtonPastDue": "Onboarding Past Due",
|
||||
"learning.outline.sequence-due-date-set": "{description} due {assignmentDue}",
|
||||
"learning.outline.sequence-due-date-not-set": "{description}",
|
||||
"learning.outline.sequence-due": "{description} due {assignmentDue}",
|
||||
"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}.",
|
||||
|
||||
@@ -114,8 +114,7 @@
|
||||
"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-date-set": "Fecha límite para {description}: {assignmentDue}",
|
||||
"learning.outline.sequence-due-date-not-set": "{description}",
|
||||
"learning.outline.sequence-due": "Fecha límite para {description}: {assignmentDue}",
|
||||
"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}.",
|
||||
|
||||
@@ -114,8 +114,7 @@
|
||||
"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-date-set": "{description} due {assignmentDue}",
|
||||
"learning.outline.sequence-due-date-not-set": "{description}",
|
||||
"learning.outline.sequence-due": "{description} échéance {assignmentDue}",
|
||||
"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}.",
|
||||
|
||||
@@ -114,10 +114,9 @@
|
||||
"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-date-set": "{description} échéance {assignmentDue}",
|
||||
"learning.outline.sequence-due-date-not-set": "{description}",
|
||||
"learning.outline.sequence-due": "{description} échéance {assignmentDue}",
|
||||
"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 attestation 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 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}.",
|
||||
"progress.certificateStatus.notPassingHeader": "État de l'attestation",
|
||||
"progress.certificateStatus.notPassingBody": "Pour être admissible à une attestation, vous devez avoir la note de passage.",
|
||||
@@ -142,7 +141,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": "Complétion du cours",
|
||||
"progress.completion.header": "Achèvement 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.",
|
||||
@@ -304,7 +303,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 attestation, demandez-la ci-dessous.",
|
||||
"courseCelebration.requestCertificateBodyText": "Pour accéder à votre certificat, demandez-le 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.",
|
||||
@@ -399,7 +398,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 une {verifiedCertLink} de complétion pour la mettre en valeur sur votre CV",
|
||||
"learning.generic.upsell.verifiedCertBullet": "Obtenez un {verifiedCertLink} d'achèvement pour le 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",
|
||||
@@ -414,23 +413,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": "D'accord",
|
||||
"tours.button.okay": "Okay",
|
||||
"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": "Bienvenue à votre",
|
||||
"tours.newUserModal.title.welcome": "Bienvenu à 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, telles 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, tel 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} cours sur {siteName} !",
|
||||
"tours.newUserModal.title": "{welcome} {siteName} au cours!",
|
||||
"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}}",
|
||||
|
||||
@@ -114,8 +114,7 @@
|
||||
"learning.proctoringPanel.onboardingButtonNotOpen": "Onboarding Opens: {releaseDate}",
|
||||
"learning.proctoringPanel.reviewRequirementsButton": "Review instructions and system requirements",
|
||||
"learning.proctoringPanel.onboardingButtonPastDue": "Onboarding Past Due",
|
||||
"learning.outline.sequence-due-date-set": "{description} due {assignmentDue}",
|
||||
"learning.outline.sequence-due-date-not-set": "{description}",
|
||||
"learning.outline.sequence-due": "{description} due {assignmentDue}",
|
||||
"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}.",
|
||||
|
||||
@@ -114,8 +114,7 @@
|
||||
"learning.proctoringPanel.onboardingButtonNotOpen": "Onboarding Opens: {releaseDate}",
|
||||
"learning.proctoringPanel.reviewRequirementsButton": "Review instructions and system requirements",
|
||||
"learning.proctoringPanel.onboardingButtonPastDue": "Onboarding Past Due",
|
||||
"learning.outline.sequence-due-date-set": "{description} due {assignmentDue}",
|
||||
"learning.outline.sequence-due-date-not-set": "{description}",
|
||||
"learning.outline.sequence-due": "{description} due {assignmentDue}",
|
||||
"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}.",
|
||||
|
||||
@@ -114,8 +114,7 @@
|
||||
"learning.proctoringPanel.onboardingButtonNotOpen": "Onboarding Opens: {releaseDate}",
|
||||
"learning.proctoringPanel.reviewRequirementsButton": "Review instructions and system requirements",
|
||||
"learning.proctoringPanel.onboardingButtonPastDue": "Onboarding Past Due",
|
||||
"learning.outline.sequence-due-date-set": "{description} due {assignmentDue}",
|
||||
"learning.outline.sequence-due-date-not-set": "{description}",
|
||||
"learning.outline.sequence-due": "{description} due {assignmentDue}",
|
||||
"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}.",
|
||||
|
||||
@@ -114,8 +114,7 @@
|
||||
"learning.proctoringPanel.onboardingButtonNotOpen": "Onboarding Opens: {releaseDate}",
|
||||
"learning.proctoringPanel.reviewRequirementsButton": "Review instructions and system requirements",
|
||||
"learning.proctoringPanel.onboardingButtonPastDue": "Onboarding Past Due",
|
||||
"learning.outline.sequence-due-date-set": "{description} due {assignmentDue}",
|
||||
"learning.outline.sequence-due-date-not-set": "{description}",
|
||||
"learning.outline.sequence-due": "{description} due {assignmentDue}",
|
||||
"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}.",
|
||||
|
||||
@@ -114,8 +114,7 @@
|
||||
"learning.proctoringPanel.onboardingButtonNotOpen": "Onboarding Opens: {releaseDate}",
|
||||
"learning.proctoringPanel.reviewRequirementsButton": "Review instructions and system requirements",
|
||||
"learning.proctoringPanel.onboardingButtonPastDue": "Onboarding Past Due",
|
||||
"learning.outline.sequence-due-date-set": "{description} due {assignmentDue}",
|
||||
"learning.outline.sequence-due-date-not-set": "{description}",
|
||||
"learning.outline.sequence-due": "{description} due {assignmentDue}",
|
||||
"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}.",
|
||||
|
||||
@@ -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": "马上升级",
|
||||
"learning.accessExpiration.upgradeNow": "Upgrade now",
|
||||
"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": "Don’t 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": "您必须报读此课程才能查看课程内容。",
|
||||
"learning.enrollment.alert": "You must be enrolled in the course to see course content.",
|
||||
"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": "已完成",
|
||||
"learning.dates.badge.completed": "Completed",
|
||||
"learning.dates.badge.dueNext": "Due next",
|
||||
"learning.dates.badge.pastDue": "Past due",
|
||||
"learning.dates.title": "重要日期",
|
||||
"learning.dates.badge.today": "今天",
|
||||
"learning.dates.title": "Important dates",
|
||||
"learning.dates.badge.today": "Today",
|
||||
"learning.dates.badge.unreleased": "Not yet released",
|
||||
"learning.dates.badge.verifiedOnly": "Verified only",
|
||||
"learning.goals.unsubscribe.contact": "请联系技术支持",
|
||||
"learning.goals.unsubscribe.contact": "contact support",
|
||||
"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": "去控制面板",
|
||||
"learning.goals.unsubscribe.goToDashboard": "Go to dashboard",
|
||||
"learning.goals.unsubscribe.header": "You’ve 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": "已完成",
|
||||
"learning.outline.completedAssignment": "Completed",
|
||||
"learning.outline.completedSection": "Completed section",
|
||||
"learning.outline.dates": "重要日期",
|
||||
"learning.outline.dates": "Important dates",
|
||||
"learning.outline.editGoal": "Edit goal",
|
||||
"learning.outline.expandAll": "Expand all",
|
||||
"learning.outline.goal": "Goal",
|
||||
"learning.outline.goalReminderDetail": "If we notice you’re not quite at your goal, we’ll send you an email reminder.",
|
||||
"learning.outline.goalUnsure": "再想想",
|
||||
"learning.outline.handouts": "课程讲义",
|
||||
"learning.outline.incompleteAssignment": "未完成",
|
||||
"learning.outline.goalUnsure": "Not sure yet",
|
||||
"learning.outline.handouts": "Course Handouts",
|
||||
"learning.outline.incompleteAssignment": "Incomplete",
|
||||
"learning.outline.incompleteSection": "Incomplete section",
|
||||
"learning.outline.goalButton.intense.text": "5 days a week",
|
||||
"learning.outline.goalButton.intense.title": "Intense",
|
||||
"learning.outline.learnMore": "了解更多",
|
||||
"learning.outline.altText.openSection": "打开",
|
||||
"learning.outline.learnMore": "Learn More",
|
||||
"learning.outline.altText.openSection": "Open",
|
||||
"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": "课程工具",
|
||||
"learning.outline.tools": "Course Tools",
|
||||
"learning.outline.upgradeButton": "Upgrade ({symbol}{price})",
|
||||
"learning.outline.upgradeTitle": "考取认证证书",
|
||||
"learning.outline.upgradeTitle": "Pursue a verified certificate",
|
||||
"learning.outline.welcomeMessage": "Welcome Message",
|
||||
"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.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.proctoringPanel.status.otherCourseApproved": "Approved in Another Course",
|
||||
"learning.proctoringPanel.status.expiringSoon": "Expiring Soon",
|
||||
"learning.proctoringPanel.status.expired": "Expired",
|
||||
@@ -114,8 +114,7 @@
|
||||
"learning.proctoringPanel.onboardingButtonNotOpen": "Onboarding Opens: {releaseDate}",
|
||||
"learning.proctoringPanel.reviewRequirementsButton": "Review instructions and system requirements",
|
||||
"learning.proctoringPanel.onboardingButtonPastDue": "Onboarding Past Due",
|
||||
"learning.outline.sequence-due-date-set": "{description} due {assignmentDue}",
|
||||
"learning.outline.sequence-due-date-not-set": "{description}",
|
||||
"learning.outline.sequence-due": "{description} due {assignmentDue}",
|
||||
"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}.",
|
||||
@@ -133,13 +132,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": "获取证书",
|
||||
"progress.certificateStatus.upgradeHeader": "Earn a certificate",
|
||||
"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": "马上升级",
|
||||
"progress.certificateStatus.upgradeButton": "Upgrade now",
|
||||
"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": "完成",
|
||||
"progress.completion.donut.label": "completed",
|
||||
"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",
|
||||
@@ -151,11 +150,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": "已完成",
|
||||
"progress.creditInformation.completed": "Completed",
|
||||
"progress.creditInformation.courseCredit": "course credit",
|
||||
"progress.creditInformation.minimumGrade": "Minimum grade for credit ({minGrade}%)",
|
||||
"progress.creditInformation.requirementsHeader": "Requirements for course credit",
|
||||
"progress.creditInformation.upcoming": "即将到来",
|
||||
"progress.creditInformation.upcoming": "Upcoming",
|
||||
"progress.creditInformation.verificationFailed": "Verification failed",
|
||||
"progress.creditInformation.verificationSubmitted": "Verification submitted",
|
||||
"progress.ungradedAlert": "For progress on ungraded aspects of the course, view your {outlineLink}.",
|
||||
@@ -173,18 +172,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": "马上升级",
|
||||
"progress.courseGrade.preview.button.upgrade": "Upgrade now",
|
||||
"progress.courseGrade.gradeRange.tooltip": "Grade ranges for this course:",
|
||||
"progress.courseOutline": "课程大纲",
|
||||
"progress.courseOutline": "Course Outline",
|
||||
"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": "成绩",
|
||||
"progress.gradeSummary.grade": "Grade",
|
||||
"progress.courseGrade.grades": "Grades",
|
||||
"progress.courseGrade.gradesAndCredit": "Grades & Credit",
|
||||
"progress.courseGrade.gradeRange.Tooltip": "Grade range tooltip",
|
||||
"progress.gradeSummary": "评分汇总",
|
||||
"progress.gradeSummary": "Grade summary",
|
||||
"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.",
|
||||
@@ -193,33 +192,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": "这部分的成绩已经被修订。",
|
||||
"progress.score": "分数",
|
||||
"progress.weight": "权重",
|
||||
"progress.detailedGrades.overridden": "Section grade has been overridden.",
|
||||
"progress.score": "Score",
|
||||
"progress.weight": "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": "日期",
|
||||
"progress.relatedLinks.datesCard.link": "Dates",
|
||||
"progress.relatedLinks.outlineCard.description": "A birds-eye view of your course content.",
|
||||
"progress.relatedLinks.outlineCard.link": "课程大纲",
|
||||
"progress.relatedLinks.outlineCard.link": "Course Outline",
|
||||
"progress.relatedLinks": "Related links",
|
||||
"datesBanner.suggestedSchedule": "We’ve built a suggested schedule to help you stay on track. But don’t worry—it’s flexible so you can learn at your own pace.",
|
||||
"datesBanner.upgradeToCompleteGradedBanner.header": "升级解锁",
|
||||
"datesBanner.upgradeToCompleteGradedBanner.header": "Upgrade to unlock",
|
||||
"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": "马上升级",
|
||||
"datesBanner.upgradeToCompleteGradedBanner.button": "Upgrade now",
|
||||
"datesBanner.upgradeToResetBanner.body": "To keep yourself on track, you can update this schedule and shift the past due assignments into the future. Don’t worry—you won’t lose any of the progress you’ve 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. Don’t worry—you won’t lose any of the progress you’ve made when you shift your due dates.",
|
||||
"datesBanner.resetDatesBanner.button": "Shift due dates",
|
||||
"learn.navigation.course.tabs.label": "课程资料",
|
||||
"unit.bookmark.button.add.bookmark": "收藏此页",
|
||||
"unit.bookmark.button.remove.bookmark": "已收藏",
|
||||
"learn.navigation.course.tabs.label": "Course Material",
|
||||
"unit.bookmark.button.add.bookmark": "Bookmark this page",
|
||||
"unit.bookmark.button.remove.bookmark": "Bookmarked",
|
||||
"learning.celebration.completed": "You just completed the first section of your course.",
|
||||
"learning.celebration.congrats": "恭喜!",
|
||||
"learning.celebration.congrats": "Congratulations!",
|
||||
"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",
|
||||
@@ -231,38 +230,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": "帮助中心",
|
||||
"calculator.instructions.support.title": "Help Center",
|
||||
"calculator.instructions.useful.tips": "Useful tips:",
|
||||
"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.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.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": "常量",
|
||||
"calculator.instruction.table.to.use.affixes": "词缀",
|
||||
"calculator.instruction.table.to.use.constants": "Constants",
|
||||
"calculator.instruction.table.to.use.affixes": "Affixes",
|
||||
"calculator.instruction.table.to.use.affixes.type": "Percent sign (%)",
|
||||
"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.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.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.input.field.label": "计算器输入",
|
||||
"calculator.submit.button.label": "计算",
|
||||
"calculator.button.label": "Calculator",
|
||||
"calculator.input.field.label": "Calculator Input",
|
||||
"calculator.submit.button.label": "Calculate",
|
||||
"calculator.result.field.label": "Calculator Result",
|
||||
"calculator.result.field.placeholder": "结果",
|
||||
"calculator.result.field.placeholder": "Result",
|
||||
"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.",
|
||||
@@ -272,7 +271,7 @@
|
||||
"courseCelebration.certificateBody.upgradable": "It’s 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": "课程",
|
||||
"courseCelebration.recommendations.label": "Course",
|
||||
"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",
|
||||
@@ -283,33 +282,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": "恭喜!您已具备获得证书的资格!",
|
||||
"courseCelebration.certificateHeader.requestable": "Congratulations, you qualified for a certificate!",
|
||||
"courseCelebration.certificateHeader.upgradable": "Upgrade to pursue a verified certificate",
|
||||
"courseCelebration.certificateImage": "Sample certificate",
|
||||
"courseCelebration.completedCourseHeader": "You have completed your course.",
|
||||
"courseCelebration.congratulationsHeader": "恭喜!",
|
||||
"courseCelebration.congratulationsHeader": "Congratulations!",
|
||||
"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": "课程面板",
|
||||
"courseExit.dashboardLink": "Dashboard",
|
||||
"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": "You’ve reached the end of the course!",
|
||||
"courseExit.endOfCourseTitle": "End of Course",
|
||||
"courseExit.idVerificationSupportLink": "Learn more about ID verification",
|
||||
"courseCelebration.linkedinAddToProfileButton": "添加至LinkedIn用户资料中",
|
||||
"courseCelebration.linkedinAddToProfileButton": "Add to LinkedIn profile",
|
||||
"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 Master’s degrees.",
|
||||
"courseExit.programs.microMasters.mastersMessage": "If you’re interested in using your MicroMasters certificate towards a Master’s program, you can get started today!",
|
||||
"learn.sequence.navigation.complete.button": "完成课程",
|
||||
"learn.sequence.navigation.complete.button": "Complete the course",
|
||||
"courseExit.nextButton.endOfCourse": "Next (end of course)",
|
||||
"courseExit.profileLink": "个人主页",
|
||||
"courseExit.profileLink": "Profile",
|
||||
"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": "马上升级",
|
||||
"courseExit.upgradeButton": "Upgrade now",
|
||||
"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",
|
||||
@@ -320,31 +319,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": "保留所有权利",
|
||||
"learn.course.license.allRightsReserved.text": "All Rights Reserved",
|
||||
"learn.course.license.creativeCommons.terms.preamble": "Creative Commons licensed content, with terms as follows:",
|
||||
"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.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.zero": "No terms",
|
||||
"learn.course.license.creativeCommons.text": "保留部分权利",
|
||||
"learn.breadcrumb.navigation.course.home": "课程",
|
||||
"learn.course.license.creativeCommons.text": "Some Rights Reserved",
|
||||
"learn.breadcrumb.navigation.course.home": "Course",
|
||||
"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": "通知",
|
||||
"notification.tray.title": "Notifications",
|
||||
"notification.tray.no.message": "You have no new notifications at this time.",
|
||||
"learn.contentLock.content.locked": "未解锁内容",
|
||||
"learn.contentLock.content.locked": "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": "本次作业已经过了截止日期。",
|
||||
"learn.hiddenAfterDue.header": "The due date for this assignment has passed.",
|
||||
"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": "诚信准则",
|
||||
"learn.honorCode.cancel": "取消",
|
||||
"learn.honorCode.name": "Honor Code",
|
||||
"learn.honorCode.cancel": "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.",
|
||||
@@ -358,25 +357,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": "下一节",
|
||||
"learn.sequence.navigation.next.button": "Next",
|
||||
"learn.sequence.navigation.next.up.button": "Next Up: {title}",
|
||||
"learn.sequence.navigation.previous.button": "上一节",
|
||||
"learn.sequence.navigation.previous.button": "Previous",
|
||||
"learn.course.sequence.navigation.mobile.menu": "{current} of {total}",
|
||||
"learn.sequence.share.button": "Share this content",
|
||||
"learn.sequence.share.modal.title": "标题",
|
||||
"learn.sequence.share.modal.title": "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.sidebar.title": "Discussions",
|
||||
"discussions.sidebar.open.button": "Show discussions tray",
|
||||
"learn.redirect.interstitial.message": "Redirecting...",
|
||||
"learn.loading.error": "错误: {error}",
|
||||
"learn.loading.error": "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": "关闭",
|
||||
"learning.logistration.register": "注册",
|
||||
"learning.logistration.login": "登录",
|
||||
"general.signIn.sentenceCase": "登录",
|
||||
"general.altText.close": "Close",
|
||||
"learning.logistration.register": "register",
|
||||
"learning.logistration.login": "sign in",
|
||||
"general.signIn.sentenceCase": "Sign in",
|
||||
"learn.course.tabs.navigation.overflow.menu": "More...",
|
||||
"learning.offer.screenReaderPrices": "Original price: {originalPrice}, discount price: {discountedPrice}",
|
||||
"learning.upgradeButton.screenReaderInlinePrices": "Original price: {originalPrice}",
|
||||
@@ -396,7 +395,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": "获取认证证书",
|
||||
"learning.generic.upgradeNotification.pursueAverifiedCertificate": "Pursue a verified certificate",
|
||||
"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é",
|
||||
@@ -412,8 +411,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 what’s coming up.",
|
||||
"tours.existingUserTour.launchTourCheckpoint.body": "We’ve recently added a few new features to the course experience. Want some help looking around? Take a tour to learn more.",
|
||||
"tours.button.dismiss": "忽略",
|
||||
"tours.button.next": "下一节",
|
||||
"tours.button.dismiss": "Dismiss",
|
||||
"tours.button.next": "Next",
|
||||
"tours.button.okay": "Okay",
|
||||
"tours.button.beginTour": "Begin tour",
|
||||
"tours.button.launchTour": "Launch tour",
|
||||
@@ -435,7 +434,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": "恭喜!",
|
||||
"learning.streakCelebration.congratulations": "Congratulations!",
|
||||
"learning.streakCelebration.body": "Keep it up, you’re on a roll!",
|
||||
"learning.streakCelebration.button": "Keep it up",
|
||||
"learning.streakCelebration.buttonSrOnly": "Close modal and continue",
|
||||
|
||||
@@ -37,7 +37,6 @@ 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(
|
||||
@@ -51,28 +50,28 @@ subscribe(APP_READY, () => {
|
||||
<Switch>
|
||||
<PageRoute exact path="/goal-unsubscribe/:token" component={GoalUnsubscribe} />
|
||||
<PageRoute path="/redirect" component={CoursewareRedirectLandingPage} />
|
||||
<DecodePageRoute path="/course/:courseId/access-denied" component={CourseAccessErrorPage} />
|
||||
<DecodePageRoute path="/course/:courseId/home">
|
||||
<PageRoute path="/course/:courseId/access-denied" component={CourseAccessErrorPage} />
|
||||
<PageRoute path="/course/:courseId/home">
|
||||
<TabContainer tab="outline" fetch={fetchOutlineTab} slice="courseHome">
|
||||
<OutlineTab />
|
||||
</TabContainer>
|
||||
</DecodePageRoute>
|
||||
<DecodePageRoute path="/course/:courseId/live">
|
||||
<TabContainer tab="lti_live" fetch={fetchLiveTab} slice="courseHome">
|
||||
</PageRoute>
|
||||
<PageRoute path="/course/:courseId/live">
|
||||
<TabContainer tab="live" fetch={fetchLiveTab} slice="courseHome">
|
||||
<LiveTab />
|
||||
</TabContainer>
|
||||
</DecodePageRoute>
|
||||
<DecodePageRoute path="/course/:courseId/dates">
|
||||
</PageRoute>
|
||||
<PageRoute path="/course/:courseId/dates">
|
||||
<TabContainer tab="dates" fetch={fetchDatesTab} slice="courseHome">
|
||||
<DatesTab />
|
||||
</TabContainer>
|
||||
</DecodePageRoute>
|
||||
<DecodePageRoute path="/course/:courseId/discussion/:path*">
|
||||
</PageRoute>
|
||||
<PageRoute path="/course/:courseId/discussion/:path*">
|
||||
<TabContainer tab="discussion" fetch={fetchDiscussionTab} slice="courseHome">
|
||||
<DiscussionTab />
|
||||
</TabContainer>
|
||||
</DecodePageRoute>
|
||||
<DecodePageRoute
|
||||
</PageRoute>
|
||||
<PageRoute
|
||||
path={[
|
||||
'/course/:courseId/progress/:targetUserId/',
|
||||
'/course/:courseId/progress',
|
||||
@@ -87,12 +86,12 @@ subscribe(APP_READY, () => {
|
||||
</TabContainer>
|
||||
)}
|
||||
/>
|
||||
<DecodePageRoute path="/course/:courseId/course-end">
|
||||
<PageRoute path="/course/:courseId/course-end">
|
||||
<TabContainer tab="courseware" fetch={fetchCourse} slice="courseware">
|
||||
<CourseExit />
|
||||
</TabContainer>
|
||||
</DecodePageRoute>
|
||||
<DecodePageRoute
|
||||
</PageRoute>
|
||||
<PageRoute
|
||||
path={[
|
||||
'/course/:courseId/:sequenceId/:unitId',
|
||||
'/course/:courseId/:sequenceId',
|
||||
@@ -136,9 +135,6 @@ 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');
|
||||
},
|
||||
},
|
||||
|
||||
@@ -77,10 +77,6 @@
|
||||
}
|
||||
}
|
||||
|
||||
.pgn__menu-select .pgn__menu-select-popup {
|
||||
position: static;
|
||||
}
|
||||
|
||||
.sequence-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
@@ -90,6 +86,7 @@
|
||||
// On mobile, the unit container will be responsible
|
||||
// for container padding.
|
||||
@media (min-width: map-get($grid-breakpoints, "sm")) {
|
||||
width: 100%;
|
||||
margin-right: auto;
|
||||
margin-left: auto;
|
||||
}
|
||||
@@ -102,24 +99,8 @@
|
||||
}
|
||||
}
|
||||
|
||||
.sequence-navigation-container {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.notification-btn {
|
||||
@media (max-width: -1 + map-get($grid-breakpoints, "sm")) {
|
||||
height: 3rem;
|
||||
}
|
||||
}
|
||||
|
||||
.sequence-navigation {
|
||||
display: flex;
|
||||
flex-grow: 1;
|
||||
|
||||
@media (max-width: -1 + map-get($grid-breakpoints, "sm")) {
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
@media (min-width: map-get($grid-breakpoints, "sm")) {
|
||||
margin: -1px -1px 0;
|
||||
@@ -187,10 +168,9 @@
|
||||
}
|
||||
|
||||
.sequence-navigation-tabs {
|
||||
overflow: auto;
|
||||
.btn {
|
||||
flex-basis: 100%;
|
||||
min-width: 3rem;
|
||||
min-width: 2rem;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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 Promise.resolve(container);
|
||||
return container;
|
||||
}
|
||||
|
||||
describe('when receiving successful course data', () => {
|
||||
|
||||
@@ -137,12 +137,10 @@ 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 });
|
||||
axiosMock.onGet(discussionConfigUrl).reply(200, { provider: 'legacy' });
|
||||
sequenceMetadata.forEach(metadata => {
|
||||
const sequenceMetadataUrl = `${getConfig().LMS_BASE_URL}/api/courseware/sequence/${metadata.item_id}`;
|
||||
axiosMock.onGet(sequenceMetadataUrl).reply(200, metadata);
|
||||
|
||||
Reference in New Issue
Block a user