diff --git a/src/course-home/data/__factories__/outlineTabData.factory.js b/src/course-home/data/__factories__/outlineTabData.factory.js
index f5da722a..6cc4faf0 100644
--- a/src/course-home/data/__factories__/outlineTabData.factory.js
+++ b/src/course-home/data/__factories__/outlineTabData.factory.js
@@ -5,7 +5,7 @@ import buildSimpleCourseBlocks from './courseBlocks.factory';
Factory.define('outlineTabData')
.option('courseId', 'course-v1:edX+DemoX+Demo_Course')
.option('host', 'http://localhost:18000')
- .attr('course_expired_html', [], () => '
Course expired
')
+ .option('dateBlocks', [])
.attr('course_tools', ['host', 'courseId'], (host, courseId) => ([{
analytics_id: 'edx.bookmarks',
title: 'Bookmarks',
@@ -17,27 +17,30 @@ Factory.define('outlineTabData')
blocks: courseBlocks.blocks,
};
})
- .attr('course_goals', [], () => ({
- goal_options: [],
- selected_goal: null,
+ .attr('dates_widget', ['dateBlocks'], (dateBlocks) => ({
+ course_date_blocks: dateBlocks,
+ user_timezone: 'UTC',
}))
- .attr('enroll_alert', {
- can_enroll: true,
- extra_text: 'Contact the administrator.',
- })
- .attr('dates_banner_info', {
- content_type_gating_enabled: false,
- missed_gated_content: false,
- missed_deadlines: false,
- })
- .attr('dates_widget', {
- courseDateBlocks: [],
- userTimezone: 'UTC',
- })
- .attr('handouts_html', [], () => '')
- .attr('offer_html', [], () => 'Great offer here
')
.attr('resume_course', ['host', 'courseId'], (host, courseId) => ({
has_visited_course: false,
url: `${host}/courses/${courseId}/jump_to/block-v1:edX+Test+Block@12345abcde`,
}))
- .attr('welcome_message_html', [], () => 'Welcome to this course!
');
+ .attrs({
+ course_expired_html: null,
+ course_goals: {
+ goal_options: [],
+ selected_goal: null,
+ },
+ dates_banner_info: {
+ content_type_gating_enabled: false,
+ missed_gated_content: false,
+ missed_deadlines: false,
+ },
+ enroll_alert: {
+ can_enroll: true,
+ extra_text: 'Contact the administrator.',
+ },
+ handouts_html: '',
+ offer_html: null,
+ welcome_message_html: 'Welcome to this course!
',
+ });
diff --git a/src/course-home/data/__snapshots__/redux.test.js.snap b/src/course-home/data/__snapshots__/redux.test.js.snap
index f88dc485..9aec5144 100644
--- a/src/course-home/data/__snapshots__/redux.test.js.snap
+++ b/src/course-home/data/__snapshots__/redux.test.js.snap
@@ -374,7 +374,7 @@ Object {
},
},
},
- "courseExpiredHtml": "Course expired
",
+ "courseExpiredHtml": null,
"courseGoals": Object {
"goalOptions": Array [],
"selectedGoal": null,
@@ -402,7 +402,7 @@ Object {
"handoutsHtml": "",
"hasEnded": undefined,
"id": "course-v1:edX+DemoX+Demo_Course_1",
- "offerHtml": "Great offer here
",
+ "offerHtml": null,
"resumeCourse": Object {
"hasVisitedCourse": false,
"url": "http://localhost:18000/courses/course-v1:edX+DemoX+Demo_Course/jump_to/block-v1:edX+Test+Block@12345abcde",
diff --git a/src/course-home/outline-tab/OutlineTab.test.jsx b/src/course-home/outline-tab/OutlineTab.test.jsx
index 85b9e798..c3d6998b 100644
--- a/src/course-home/outline-tab/OutlineTab.test.jsx
+++ b/src/course-home/outline-tab/OutlineTab.test.jsx
@@ -21,63 +21,72 @@ jest.mock('@edx/frontend-platform/analytics');
describe('Outline Tab', () => {
let axiosMock;
- const courseMetadataUrl = new RegExp(`${getConfig().LMS_BASE_URL}/api/course_home/v1/course_metadata/*`);
- const goalUrl = new RegExp(`${getConfig().LMS_BASE_URL}/api/course_home/v1/save_course_goal`);
- const outlineUrl = new RegExp(`${getConfig().LMS_BASE_URL}/api/course_home/v1/outline/*`);
+ const courseId = 'course-v1:edX+Test+run';
+ const courseMetadataUrl = `${getConfig().LMS_BASE_URL}/api/course_home/v1/course_metadata/${courseId}`;
+ const enrollmentUrl = `${getConfig().LMS_BASE_URL}/api/enrollment/v1/enrollment`;
+ const goalUrl = `${getConfig().LMS_BASE_URL}/api/course_home/v1/save_course_goal`;
+ const outlineUrl = `${getConfig().LMS_BASE_URL}/api/course_home/v1/outline/${courseId}`;
const store = initializeStore();
+ const defaultMetadata = Factory.build('courseHomeMetadata', { courseId });
+ const defaultTabData = Factory.build('outlineTabData');
- const courseMetadata = Factory.build('courseHomeMetadata');
- const { courseId } = courseMetadata;
- const outlineTabData = Factory.build('outlineTabData');
+ function setMetadata(attributes, options) {
+ const courseMetadata = Factory.build('courseHomeMetadata', { courseId, ...attributes }, options);
+ axiosMock.onGet(courseMetadataUrl).reply(200, courseMetadata);
+ }
+
+ function setTabData(attributes, options) {
+ const outlineTabData = Factory.build('outlineTabData', attributes, options);
+ axiosMock.onGet(outlineUrl).reply(200, outlineTabData);
+ }
+
+ async function fetchAndRender() {
+ await executeThunk(thunks.fetchOutlineTab(courseId), store.dispatch);
+ render(, { store });
+ }
beforeEach(async () => {
axiosMock = new MockAdapter(getAuthenticatedHttpClient());
- axiosMock.onGet(courseMetadataUrl).reply(200, courseMetadata);
- axiosMock.onGet(outlineUrl).reply(200, outlineTabData);
+
+ // Set defaults for network requests
+ axiosMock.onGet(courseMetadataUrl).reply(200, defaultMetadata);
+ axiosMock.onPost(enrollmentUrl).reply(200, {});
axiosMock.onPost(goalUrl).reply(200, { header: 'Success' });
+ axiosMock.onGet(outlineUrl).reply(200, defaultTabData);
+
logUnhandledRequests(axiosMock);
- await executeThunk(thunks.fetchOutlineTab(courseId), store.dispatch);
});
describe('Course Outline', () => {
- it('displays link to start course', () => {
- render(, { store });
+ it('displays link to start course', async () => {
+ await fetchAndRender();
expect(screen.getByRole('link', { name: 'Start Course' })).toBeInTheDocument();
});
it('displays link to resume course', async () => {
- const outlineTabDataHasVisited = Factory.build('outlineTabData', {
- courseId,
+ setTabData({
resume_course: {
has_visited_course: true,
url: `${getConfig().LMS_BASE_URL}/courses/${courseId}/jump_to/block-v1:edX+Test+Block@12345abcde`,
},
});
- axiosMock.onGet(outlineUrl).reply(200, outlineTabDataHasVisited);
- await executeThunk(thunks.fetchOutlineTab(courseId), store.dispatch);
-
- render(, { store });
-
+ await fetchAndRender();
expect(screen.getByRole('link', { name: 'Resume Course' })).toBeInTheDocument();
});
it('expands section that contains resume block', async () => {
- const { courseBlocks } = await buildSimpleCourseBlocks(courseId, outlineTabData.title, { resumeBlock: true });
- const outlineTabDataResumeBlock = Factory.build('outlineTabData', {
- courseId,
+ const { courseBlocks } = await buildSimpleCourseBlocks(courseId, 'Title', { resumeBlock: true });
+ setTabData({
course_blocks: { blocks: courseBlocks.blocks },
});
- axiosMock.onGet(outlineUrl).reply(200, outlineTabDataResumeBlock);
- await executeThunk(thunks.fetchOutlineTab(courseId), store.dispatch);
-
- render(, { store });
+ await fetchAndRender();
const expandedSectionNode = screen.getByRole('button', { name: /Title of Section/ });
expect(expandedSectionNode).toHaveAttribute('aria-expanded', 'true');
});
- it('handles expand/collapse all button click', () => {
- render(, { store });
+ it('handles expand/collapse all button click', async () => {
+ await fetchAndRender();
// Button renders as "Expand All"
const expandButton = screen.getByRole('button', { name: 'Expand All' });
expect(expandButton).toBeInTheDocument();
@@ -96,43 +105,34 @@ describe('Outline Tab', () => {
});
it('displays correct icon for complete assignment', async () => {
- const { courseBlocks } = await buildSimpleCourseBlocks(courseId, outlineTabData.title, { complete: true });
- const outlineTabDataCompleteAssignment = Factory.build('outlineTabData', {
- courseId,
+ const { courseBlocks } = await buildSimpleCourseBlocks(courseId, 'Title', { complete: true });
+ setTabData({
course_blocks: { blocks: courseBlocks.blocks },
});
- axiosMock.onGet(outlineUrl).reply(200, outlineTabDataCompleteAssignment);
- await executeThunk(thunks.fetchOutlineTab(courseId), store.dispatch);
-
- render(, { store });
+ await fetchAndRender();
expect(screen.getByTitle('Completed section')).toBeInTheDocument();
});
it('displays correct icon for incomplete assignment', async () => {
- const { courseBlocks } = await buildSimpleCourseBlocks(courseId, outlineTabData.title, { complete: false });
- const outlineTabDataIncompleteAssignment = Factory.build('outlineTabData', {
- courseId,
+ const { courseBlocks } = await buildSimpleCourseBlocks(courseId, 'Title', { complete: false });
+ setTabData({
course_blocks: { blocks: courseBlocks.blocks },
});
- axiosMock.onGet(outlineUrl).reply(200, outlineTabDataIncompleteAssignment);
- await executeThunk(thunks.fetchOutlineTab(courseId), store.dispatch);
-
- render(, { store });
+ await fetchAndRender();
expect(screen.getByTitle('Incomplete section')).toBeInTheDocument();
});
});
describe('Welcome Message', () => {
- it('does not render show more/less button under 100 words', () => {
- render(, { store });
+ it('does not render show more/less button under 100 words', async () => {
+ await fetchAndRender();
expect(screen.getByTestId('alert-container-welcome')).toBeInTheDocument();
expect(screen.queryByRole('button', { name: 'Show more' })).not.toBeInTheDocument();
});
describe('over 100 words', () => {
beforeEach(async () => {
- const outlineTabDataLongMessage = Factory.build('outlineTabData', {
- courseId,
+ setTabData({
welcome_message_html: ''
+ 'This is a test welcome message that happens to be longer than one hundred words. We hope it will be shortened.'
+ 'This is a test welcome message that happens to be longer than one hundred words. We hope it will be shortened.'
@@ -141,10 +141,7 @@ describe('Outline Tab', () => {
+ 'This is a test welcome message that happens to be longer than one hundred words. We hope it will be shortened.'
+ '
',
});
- axiosMock.onGet(outlineUrl).reply(200, outlineTabDataLongMessage);
- await executeThunk(thunks.fetchOutlineTab(courseId), store.dispatch);
-
- render(, { store });
+ await fetchAndRender();
});
it('shortens message', async () => {
@@ -172,14 +169,8 @@ describe('Outline Tab', () => {
});
it('does not display if no update available', async () => {
- const outlineTabDataSansUpdate = Factory.build('outlineTabData', {
- courseId,
- welcome_message_html: null,
- });
- axiosMock.onGet(outlineUrl).reply(200, outlineTabDataSansUpdate);
- await executeThunk(thunks.fetchOutlineTab(courseId), store.dispatch);
-
- render(, { store });
+ setTabData({ welcome_message_html: null });
+ await fetchAndRender();
expect(screen.queryByTestId('alert-container-welcome')).not.toBeInTheDocument();
});
});
@@ -192,8 +183,8 @@ describe('Outline Tab', () => {
['unsure', 'Not sure yet'],
];
- it('does not render goal widgets if no goals available', () => {
- render(, { store });
+ it('does not render goal widgets if no goals available', async () => {
+ await fetchAndRender();
expect(screen.queryByTestId('course-goal-card')).not.toBeInTheDocument();
expect(screen.queryByLabelText('Goal')).not.toBeInTheDocument();
expect(screen.queryByTestId('edit-goal-selector')).not.toBeInTheDocument();
@@ -201,17 +192,13 @@ describe('Outline Tab', () => {
describe('goal is not set', () => {
beforeEach(async () => {
- const outlineTabDataGoalNotSet = Factory.build('outlineTabData', {
- courseId,
+ setTabData({
course_goals: {
goal_options: goalOptions,
selected_goal: null,
},
});
- axiosMock.onGet(outlineUrl).reply(200, outlineTabDataGoalNotSet);
- await executeThunk(thunks.fetchOutlineTab(courseId), store.dispatch);
-
- render(, { store });
+ await fetchAndRender();
});
it('renders goal card', () => {
@@ -234,18 +221,13 @@ describe('Outline Tab', () => {
describe('goal is set', () => {
beforeEach(async () => {
- const outlineTabDataGoalSet = Factory.build('outlineTabData', {
- courseId,
+ setTabData({
course_goals: {
goal_options: goalOptions,
selected_goal: { text: 'Earn a certificate', key: 'certify' },
},
});
-
- axiosMock.onGet(outlineUrl).reply(200, outlineTabDataGoalSet);
- await executeThunk(thunks.fetchOutlineTab(courseId), store.dispatch);
-
- render(, { store });
+ await fetchAndRender();
});
it('renders edit goal selector', () => {
@@ -279,67 +261,47 @@ describe('Outline Tab', () => {
});
describe('Course Handouts', () => {
- it('renders title when handouts are available', () => {
- render(, { store });
+ it('renders title when handouts are available', async () => {
+ await fetchAndRender();
expect(screen.queryByRole('heading', { name: 'Course Handouts' })).toBeInTheDocument();
});
it('does not display title if no handouts available', async () => {
- const outlineTabDataSansHandout = Factory.build('outlineTabData', {
- courseId,
- handouts_html: null,
- });
- axiosMock.onGet(outlineUrl).reply(200, outlineTabDataSansHandout);
- await executeThunk(thunks.fetchOutlineTab(courseId), store.dispatch);
-
- render(, { store });
+ setTabData({ handouts_html: null });
+ await fetchAndRender();
expect(screen.queryByRole('heading', { name: 'Course Handouts' })).not.toBeInTheDocument();
});
});
describe('Alert List', () => {
describe('Enrollment Alert', () => {
- let extraText;
let alertMessage;
let staffMessage;
beforeEach(() => {
- extraText = outlineTabData.enroll_alert.extra_text;
+ const extraText = defaultTabData.enroll_alert.extra_text;
alertMessage = `You must be enrolled in the course to see course content. ${extraText}`;
staffMessage = 'You are viewing this course as staff, and are not enrolled.';
});
it('does not display enrollment alert for enrolled user', async () => {
- const courseHomeMetadataForEnrolledUser = Factory.build(
- 'courseHomeMetadata', { course_id: courseId, is_enrolled: true },
- { courseTabs: courseMetadata.tabs },
- );
- axiosMock.onGet(courseMetadataUrl).reply(200, courseHomeMetadataForEnrolledUser);
- await executeThunk(thunks.fetchOutlineTab(courseId), store.dispatch);
-
- render(, { store });
-
+ setMetadata({ is_enrolled: true });
+ await fetchAndRender();
expect(screen.queryByText(alertMessage)).not.toBeInTheDocument();
});
it('does not display enrollment button if enrollment is not available', async () => {
- const outlineTabDataCannotEnroll = Factory.build('outlineTabData', {
- courseId,
+ setTabData({
enroll_alert: {
can_enroll: false,
- extra_text: extraText,
},
});
- axiosMock.onGet(outlineUrl).reply(200, outlineTabDataCannotEnroll);
- await executeThunk(thunks.fetchOutlineTab(courseId), store.dispatch);
-
- render(, { store });
-
+ await fetchAndRender();
expect(screen.queryByRole('button', { name: 'Enroll Now' })).not.toBeInTheDocument();
});
it('displays enrollment alert for unenrolled user', async () => {
- render(, { store });
+ await fetchAndRender();
const alert = await screen.findByText(alertMessage);
expect(alert).toHaveAttribute('role', 'alert');
@@ -350,23 +312,8 @@ describe('Outline Tab', () => {
});
it('displays different message for unenrolled staff user', async () => {
- const courseHomeMetadataForUnenrolledStaff = Factory.build(
- 'courseHomeMetadata', { course_id: courseId, is_staff: true },
- { courseTabs: courseMetadata.tabs },
- );
- axiosMock.onGet(courseMetadataUrl).reply(200, courseHomeMetadataForUnenrolledStaff);
- // We need to remove offer_html and course_expired_html to limit the number of alerts we
- // show, which makes this test easier to write. If there's only one, it's easy to query
- // for below.
- const outlineTabDataCannotEnroll = Factory.build('outlineTabData', {
- courseId,
- offer_html: null,
- course_expired_html: null,
- });
- axiosMock.onGet(outlineUrl).reply(200, outlineTabDataCannotEnroll);
- await executeThunk(thunks.fetchOutlineTab(courseId), store.dispatch);
-
- render(, { store });
+ setMetadata({ is_staff: true });
+ await fetchAndRender();
const alert = await screen.findByText(staffMessage);
expect(alert).toHaveAttribute('role', 'alert');
@@ -376,15 +323,12 @@ describe('Outline Tab', () => {
});
it('handles button click', async () => {
- const enrollmentUrl = `${getConfig().LMS_BASE_URL}/api/enrollment/v1/enrollment`;
- axiosMock.reset();
- axiosMock.onPost(enrollmentUrl).reply(200, { });
const { location } = window;
delete window.location;
window.location = {
reload: jest.fn(),
};
- render(, { store });
+ await fetchAndRender();
const button = await screen.findByRole('button', { name: 'Enroll Now' });
fireEvent.click(button);
@@ -398,19 +342,116 @@ describe('Outline Tab', () => {
});
describe('Access Expiration Alert', () => {
- // TODO: Test this alert.
+ // Appears if course_expired_html is provided
+ it('appears', async () => {
+ setTabData({ course_expired_html: 'Course Will Expire, Uh Oh
' });
+ await fetchAndRender();
+ await screen.findByText('Course Will Expire, Uh Oh');
+ });
});
describe('Course Start Alert', () => {
- // TODO: Test this alert.
+ // Only appears if enrolled and before start of course
+ it('appears several days out', async () => {
+ const startDate = new Date();
+ startDate.setDate(startDate.getDate() + 100);
+ setMetadata({ is_enrolled: true });
+ setTabData({}, {
+ dateBlocks: [
+ {
+ date_type: 'course-start-date',
+ date: startDate.toISOString(),
+ title: 'Start',
+ },
+ ],
+ });
+ await fetchAndRender();
+ const node = await screen.findByText('Course starts', { exact: false });
+ expect(node.textContent).toMatch(/.* on .*/); // several days away uses "on" before date
+ });
+
+ it('appears today', async () => {
+ const startDate = new Date();
+ startDate.setHours(startDate.getHours() + 1);
+ setMetadata({ is_enrolled: true });
+ setTabData({}, {
+ dateBlocks: [
+ {
+ date_type: 'course-start-date',
+ date: startDate.toISOString(),
+ title: 'Start',
+ },
+ ],
+ });
+ await fetchAndRender();
+ const node = await screen.findByText('Course starts', { exact: false });
+ expect(node.textContent).toMatch(/.* at .*/); // same day uses "at" before date
+ });
});
describe('Course End Alert', () => {
- // TODO: Test this alert.
+ // Only appears if enrolled and within 14 days before the end of course
+ it('appears several days out', async () => {
+ const endDate = new Date();
+ endDate.setDate(endDate.getDate() + 13);
+ setMetadata({ is_enrolled: true });
+ setTabData({}, {
+ dateBlocks: [
+ {
+ date_type: 'course-end-date',
+ date: endDate.toISOString(),
+ title: 'End',
+ },
+ ],
+ });
+ await fetchAndRender();
+ const node = await screen.findByText('This course is ending', { exact: false });
+ expect(node.textContent).toMatch(/.* on .*/); // several days away uses "on" before date
+ });
+
+ it('appears today', async () => {
+ const endDate = new Date();
+ endDate.setHours(endDate.getHours() + 1);
+ setMetadata({ is_enrolled: true });
+ setTabData({}, {
+ dateBlocks: [
+ {
+ date_type: 'course-end-date',
+ date: endDate.toISOString(),
+ title: 'End',
+ },
+ ],
+ });
+ await fetchAndRender();
+ const node = await screen.findByText('This course is ending', { exact: false });
+ expect(node.textContent).toMatch(/.* at .*/); // same day uses "at" before date
+ });
});
describe('Certificate Available Alert', () => {
- // TODO: Test this alert.
+ // Must satisfy two conditions for alert to appear: enrolled and between course end and cert availability
+ it('appears', async () => {
+ const now = new Date();
+ const yesterday = new Date(now.getFullYear(), now.getMonth(), now.getDate() - 1);
+ const tomorrow = new Date(now.getFullYear(), now.getMonth(), now.getDate() + 1);
+ setMetadata({ is_enrolled: true });
+ setTabData({}, {
+ dateBlocks: [
+ {
+ date_type: 'course-end-date',
+ date: yesterday.toISOString(),
+ title: 'End',
+ },
+ {
+ date_type: 'certificate-available-date',
+ date: tomorrow.toISOString(),
+ title: 'Cert Available',
+ },
+ ],
+ });
+ await fetchAndRender();
+ await screen.findByText('We are working on generating course certificates.');
+ });
});
});
});