Compare commits
1 Commits
open-relea
...
schen/cour
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0834baee05 |
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
|
||||
|
||||
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
|
||||
|
||||
40626
package-lock.json
generated
40626
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
12
package.json
12
package.json
@@ -30,10 +30,10 @@
|
||||
},
|
||||
"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/frontend-component-footer": "11.6.3",
|
||||
"@edx/frontend-component-header": "3.6.4",
|
||||
"@edx/frontend-lib-special-exams": "~2.8.0",
|
||||
"@edx/frontend-platform": "3.4.1",
|
||||
"@edx/paragon": "20.28.4",
|
||||
"@fortawesome/fontawesome-svg-core": "1.3.0",
|
||||
"@fortawesome/free-brands-svg-icons": "5.15.4",
|
||||
@@ -64,7 +64,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 +74,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"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -420,3 +420,10 @@ export async function unsubscribeFromCourseGoal(token) {
|
||||
return getAuthenticatedHttpClient().post(url.href)
|
||||
.then(res => camelCaseObject(res));
|
||||
}
|
||||
|
||||
export async function searchCourseContentFromAPI(courseId, searchKeyword) {
|
||||
const url = new URL(`${getConfig().LMS_BASE_URL}/search/${courseId}`);
|
||||
const formData = `search_string=${searchKeyword}&page_size=20&page_index=0`;
|
||||
return getAuthenticatedHttpClient().post(url.href, formData)
|
||||
.then(res => camelCaseObject(res));
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -12,6 +12,7 @@ import {
|
||||
postDismissWelcomeMessage,
|
||||
postRequestCert,
|
||||
getLiveTabIframe,
|
||||
searchCourseContentFromAPI,
|
||||
} from './api';
|
||||
|
||||
import {
|
||||
@@ -139,3 +140,18 @@ export function processEvent(eventData, getTabData) {
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export function searchCourseContent(courseId, searchKeyword) {
|
||||
return async (dispatch) => {
|
||||
searchCourseContentFromAPI(courseId, searchKeyword).then(response => {
|
||||
const { data } = response;
|
||||
dispatch(addModel({
|
||||
modelType: 'contentSearchResults',
|
||||
model: {
|
||||
id: courseId,
|
||||
...data,
|
||||
},
|
||||
}));
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
@@ -28,6 +28,7 @@ import { useModel } from '../../generic/model-store';
|
||||
import WelcomeMessage from './widgets/WelcomeMessage';
|
||||
import ProctoringInfoPanel from './widgets/ProctoringInfoPanel';
|
||||
import AccountActivationAlert from '../../alerts/logistration-alert/AccountActivationAlert';
|
||||
import CoursewareSearch from './widgets/CoursewareSearch';
|
||||
|
||||
const OutlineTab = ({ intl }) => {
|
||||
const {
|
||||
@@ -157,6 +158,7 @@ const OutlineTab = ({ intl }) => {
|
||||
)}
|
||||
<StartOrResumeCourseCard />
|
||||
<WelcomeMessage courseId={courseId} />
|
||||
<CoursewareSearch courseId={courseId} />
|
||||
{rootCourseId && (
|
||||
<>
|
||||
<div className="row w-100 m-0 mb-3 justify-content-end">
|
||||
|
||||
@@ -119,11 +119,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 () => {
|
||||
|
||||
@@ -331,6 +331,16 @@ const messages = defineMessages({
|
||||
defaultMessage: 'Onboarding Past Due',
|
||||
description: 'Text that show when the deadline of proctortrack onboarding exam has passed, it appears on button that start the onboarding exam however for this case the button is disabled for obvious reason',
|
||||
},
|
||||
coursewareSearchInputLabel: {
|
||||
id: 'learning.coursewareSearch.inputLabel',
|
||||
defaultMessage: 'Course Content Search',
|
||||
description: 'Search input label',
|
||||
},
|
||||
coursewareSearchButtonLabel: {
|
||||
id: 'learning.coursewareSearch.buttonLabel',
|
||||
defaultMessage: 'Search',
|
||||
description: 'Search button label',
|
||||
},
|
||||
});
|
||||
|
||||
export default messages;
|
||||
|
||||
109
src/course-home/outline-tab/widgets/CoursewareSearch.jsx
Normal file
109
src/course-home/outline-tab/widgets/CoursewareSearch.jsx
Normal file
@@ -0,0 +1,109 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import {
|
||||
Button,
|
||||
Form,
|
||||
Container,
|
||||
Hyperlink,
|
||||
Layout,
|
||||
} from '@edx/paragon';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import { sendTrackingLogEvent } from '@edx/frontend-platform/analytics';
|
||||
import { getConfig } from '@edx/frontend-platform';
|
||||
import messages from '../messages';
|
||||
import { useModel, updateModel } from '../../../generic/model-store';
|
||||
import { searchCourseContent } from '../../data/thunks';
|
||||
|
||||
const CoursewareSearch = ({ courseId, intl }) => {
|
||||
const {
|
||||
org,
|
||||
} = useModel('courseHomeMeta', courseId);
|
||||
|
||||
const eventProperties = {
|
||||
org_key: org,
|
||||
courserun_key: courseId,
|
||||
};
|
||||
|
||||
const dispatch = useDispatch();
|
||||
|
||||
const {
|
||||
results,
|
||||
took,
|
||||
} = useModel('contentSearchResults', courseId);
|
||||
|
||||
const [searchKeyword, setSearchKeyword] = useState('');
|
||||
|
||||
useEffect(() => {
|
||||
if (!searchKeyword) {
|
||||
dispatch(updateModel({
|
||||
modelType: 'contentSearchResults',
|
||||
model: {
|
||||
id: courseId,
|
||||
results: [],
|
||||
took: false,
|
||||
},
|
||||
}));
|
||||
}
|
||||
}, [searchKeyword]);
|
||||
|
||||
const searchClick = () => {
|
||||
sendTrackingLogEvent('edx.course.home.courseware_search.clicked', {
|
||||
...eventProperties,
|
||||
event_type: 'search',
|
||||
keyword: searchKeyword,
|
||||
});
|
||||
dispatch(searchCourseContent(courseId, searchKeyword));
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Form.Group>
|
||||
<Form.Control
|
||||
className="float-left w-75"
|
||||
floatingLabel={intl.formatMessage(messages.coursewareSearchInputLabel)}
|
||||
value={searchKeyword}
|
||||
onChange={(e) => setSearchKeyword(e.target.value)}
|
||||
/>
|
||||
<Button
|
||||
variant="primary"
|
||||
className="float-left"
|
||||
onClick={() => searchClick()}
|
||||
>
|
||||
{intl.formatMessage(messages.coursewareSearchButtonLabel)}
|
||||
</Button>
|
||||
<div className="clearfix" />
|
||||
</Form.Group>
|
||||
{(took && results.length === 0) && (
|
||||
<Container size="xl">
|
||||
{
|
||||
`Could not find any component matching "${searchKeyword}"`
|
||||
}
|
||||
</Container>
|
||||
)}
|
||||
{(took && results.length > 0) && results.map(resultItem => (
|
||||
<Container
|
||||
size="xl"
|
||||
>
|
||||
<Layout>
|
||||
<Layout.Element>
|
||||
<Hyperlink
|
||||
destination={`${getConfig().LMS_BASE_URL}${resultItem.data.url}`}
|
||||
>
|
||||
{ resultItem.data.location.join('/') }
|
||||
</Hyperlink>
|
||||
</Layout.Element>
|
||||
</Layout>
|
||||
</Container>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
CoursewareSearch.propTypes = {
|
||||
courseId: PropTypes.string.isRequired,
|
||||
intl: intlShape.isRequired,
|
||||
};
|
||||
|
||||
export default injectIntl(CoursewareSearch);
|
||||
@@ -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];
|
||||
|
||||
@@ -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}/`);
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
.content-tools {
|
||||
position: fixed;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
z-index: 100;
|
||||
|
||||
@@ -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' };
|
||||
|
||||
@@ -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('');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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', () => {
|
||||
|
||||
Reference in New Issue
Block a user