diff --git a/src/courseware/course/Course.test.jsx b/src/courseware/course/Course.test.jsx index 64de33b2..ab629700 100644 --- a/src/courseware/course/Course.test.jsx +++ b/src/courseware/course/Course.test.jsx @@ -1,18 +1,16 @@ 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, } from '../../setupTest'; -import { buildTopicsFromUnits } from '../data/__factories__/discussionTopics.factory'; -import { handleNextSectionCelebration } from './celebration'; import * as celebrationUtils from './celebration/utils'; +import { handleNextSectionCelebration } from './celebration'; import Course from './Course'; -import { executeThunk } from '../../utils'; -import * as thunks from '../data/thunks'; +import setupDiscussionSidebar from './test-utils'; jest.mock('@edx/frontend-platform/analytics'); jest.mock('@edx/frontend-lib-special-exams/dist/data/thunks.js', () => ({ @@ -51,26 +49,6 @@ describe('Course', () => { setItemSpy.mockRestore(); }); - const setupDiscussionSidebar = async () => { - const courseHomeMetadata = Factory.build('courseHomeMetadata', { verified_mode: null }); - const testStore = await initializeTestStore({ provider: 'openedx', courseHomeMetadata }); - 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; - - await render(, { store: testStore, wrapWithRouter: true }); - }; - it('loads learning sequence', async () => { render(, { wrapWithRouter: true }); expect(screen.getByRole('navigation', { name: 'breadcrumb' })).toBeInTheDocument(); @@ -183,7 +161,7 @@ describe('Course', () => { }); it('handles click to open/close notification tray', async () => { - render(, { wrapWithRouter: true }); + await setupDiscussionSidebar(); const notificationShowButton = await screen.findByRole('button', { name: /Show notification tray/i }); expect(screen.queryByRole('region', { name: /notification tray/i })).toHaveClass('d-none'); fireEvent.click(notificationShowButton); diff --git a/src/courseware/course/new-sidebar/common/SidebarBase.jsx b/src/courseware/course/new-sidebar/common/SidebarBase.jsx index 63e4607e..540b8fce 100644 --- a/src/courseware/course/new-sidebar/common/SidebarBase.jsx +++ b/src/courseware/course/new-sidebar/common/SidebarBase.jsx @@ -91,7 +91,7 @@ const SidebarBase = ({ }; SidebarBase.propTypes = { - title: PropTypes.string.isRequired, + title: PropTypes.string, ariaLabel: PropTypes.string.isRequired, sidebarId: PropTypes.string.isRequired, className: PropTypes.string, @@ -103,6 +103,7 @@ SidebarBase.propTypes = { }; SidebarBase.defaultProps = { + title: '', width: '50rem', allowFullHeight: false, showTitleBar: true, diff --git a/src/courseware/course/new-sidebar/sidebars/discussions-notifications/discussions/DiscussionsWidget.test.jsx b/src/courseware/course/new-sidebar/sidebars/discussions-notifications/discussions/DiscussionsWidget.test.jsx index 519aa09d..84685bf7 100644 --- a/src/courseware/course/new-sidebar/sidebars/discussions-notifications/discussions/DiscussionsWidget.test.jsx +++ b/src/courseware/course/new-sidebar/sidebars/discussions-notifications/discussions/DiscussionsWidget.test.jsx @@ -12,6 +12,7 @@ import { executeThunk } from '../../../../../../utils'; import { buildTopicsFromUnits } from '../../../../../data/__factories__/discussionTopics.factory'; import { getCourseDiscussionTopics } from '../../../../../data/thunks'; import SidebarContext from '../../../SidebarContext'; +import DiscussionsNotificationsSidebar from '../DiscussionsNotificationsSidebar'; import DiscussionsWidget from './DiscussionsWidget'; initializeMockApp(); @@ -51,24 +52,29 @@ describe('DiscussionsWidget', () => { await executeThunk(getCourseDiscussionTopics(courseId), store.dispatch); }); - function renderWithProvider(testData = {}) { + function renderWithProvider(Component, testData = {}) { const { container } = render( - + , ); return container; } it('should show up if unit discussions associated with it', async () => { - renderWithProvider(); + renderWithProvider(DiscussionsWidget); expect(screen.queryByTitle('Discussions')).toBeInTheDocument(); expect(screen.queryByTitle('Discussions')) .toHaveAttribute('src', `http://localhost:2002/${courseId}/category/${unitId}?inContextSidebar`); }); it('should show nothing if unit has no discussions associated with it', async () => { - renderWithProvider({ isDiscussionbarAvailable: false }); + renderWithProvider(DiscussionsWidget, { isDiscussionbarAvailable: false }); expect(screen.queryByTitle('Discussions')).not.toBeInTheDocument(); }); + + it('should display the Back to course button on small screens.', async () => { + renderWithProvider(DiscussionsNotificationsSidebar, { shouldDisplayFullScreen: true }); + expect(screen.queryByText('Back to course')).toBeInTheDocument(); + }); }); diff --git a/src/courseware/course/new-sidebar/sidebars/discussions-notifications/notifications/NotificationsWidget.jsx b/src/courseware/course/new-sidebar/sidebars/discussions-notifications/notifications/NotificationsWidget.jsx index 26ea9ac2..e146fb7a 100644 --- a/src/courseware/course/new-sidebar/sidebars/discussions-notifications/notifications/NotificationsWidget.jsx +++ b/src/courseware/course/new-sidebar/sidebars/discussions-notifications/notifications/NotificationsWidget.jsx @@ -38,7 +38,7 @@ const NotificationsWidget = () => { if (hideNotificationbar || !isNotificationbarAvailable) { return null; } return ( -
+
{ let axiosMock; let store; const ID = 'NEWSIDEBAR'; - const defaultMetadata = Factory.build('courseMetadata'); const courseId = defaultMetadata.id; let courseMetadataUrl = `${getConfig().LMS_BASE_URL}/api/courseware/course/${defaultMetadata.id}`; @@ -47,6 +49,35 @@ describe('NotificationsWidget', () => { axiosMock = new MockAdapter(getAuthenticatedHttpClient()); axiosMock.onGet(courseMetadataUrl).reply(200, defaultMetadata); axiosMock.onGet(courseHomeMetadataUrl).reply(200, courseHomeMetadata); + mergeConfig({ ENABLE_NEW_SIDEBAR: 'true' }, 'Custom app config'); + }); + + it('successfully Open/Hide sidebar tray.', async () => { + const userVerifiedMode = Factory.build('verifiedMode'); + + await setupDiscussionSidebar(userVerifiedMode); + + const sidebarButton = await screen.getByRole('button', { name: /Show sidebar tray/i }); + + await act(async () => { + fireEvent.click(sidebarButton); + }); + + await waitFor(async () => { + expect(screen.queryByTestId('sidebar-DISCUSSIONS_NOTIFICATIONS')).toBeInTheDocument(); + expect(screen.queryByTestId('notification-widget')).toBeInTheDocument(); + expect(screen.queryByTitle('Discussions')).toBeInTheDocument(); + }); + + await act(async () => { + fireEvent.click(sidebarButton); + }); + + await waitFor(async () => { + expect(screen.queryByTestId('sidebar-DISCUSSIONS_NOTIFICATIONS')).not.toBeInTheDocument(); + expect(screen.queryByTestId('notification-widget')).not.toBeInTheDocument(); + expect(screen.queryByTitle('Discussions')).not.toBeInTheDocument(); + }); }); it('renders upgrade card', async () => { @@ -90,6 +121,41 @@ describe('NotificationsWidget', () => { .toBeInTheDocument(); }); + it.each([ + { + description: 'close the notification widget.', + enabledInContext: true, + testId: + 'notification-widget', + }, + { + description: 'close the sidebar when the notification widget is closed, and the discussion widget is unavailable.', + enabledInContext: false, + testId: 'sidebar-DISCUSSIONS_NOTIFICATIONS', + }, + ])('successfully %s', async ({ enabledInContext, testId }) => { + const userVerifiedMode = Factory.build('verifiedMode'); + + await setupDiscussionSidebar(userVerifiedMode, enabledInContext); + + const sidebarButton = screen.getByRole('button', { name: /Show sidebar tray/i }); + + await act(async () => { + fireEvent.click(sidebarButton); + }); + + const notificationWidget = await waitFor(() => screen.getByTestId('notification-widget')); + const closeNotificationButton = within(notificationWidget).getByRole('button', { name: /Close/i }); + + await act(async () => { + fireEvent.click(closeNotificationButton); + }); + + await waitFor(() => { + expect(screen.queryByTestId(testId)).not.toBeInTheDocument(); + }); + }); + it('marks notification as seen 3 seconds later', async () => { jest.useFakeTimers(); const onNotificationSeen = jest.fn(); diff --git a/src/courseware/course/test-utils.jsx b/src/courseware/course/test-utils.jsx new file mode 100644 index 00000000..df7a7cbc --- /dev/null +++ b/src/courseware/course/test-utils.jsx @@ -0,0 +1,49 @@ +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 { initializeTestStore, render } from '../../setupTest'; +import { buildTopicsFromUnits } from '../data/__factories__/discussionTopics.factory'; +import { executeThunk } from '../../utils'; +import * as thunks from '../data/thunks'; +import Course from './Course'; + +const mockData = { + nextSequenceHandler: () => {}, + previousSequenceHandler: () => {}, + unitNavigationHandler: () => {}, +}; + +const setupDiscussionSidebar = async (verifiedMode = null, enabledInContext = true) => { + const store = await initializeTestStore(); + const { courseware, models } = store.getState(); + const { courseId, sequenceId } = courseware; + Object.assign(mockData, { + courseId, + sequenceId, + unitId: Object.values(models.units)[0].id, + }); + global.innerWidth = breakpoints.extraLarge.minWidth; + + const courseHomeMetadata = Factory.build('courseHomeMetadata', { verified_mode: verifiedMode }); + const testStore = await initializeTestStore({ provider: 'openedx', courseHomeMetadata }); + const state = testStore.getState(); + 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, enabledInContext); + 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; + + const wrapper = await render(, { store: testStore, wrapWithRouter: true }); + return wrapper; +}; + +export default setupDiscussionSidebar; diff --git a/src/courseware/data/__factories__/discussionTopics.factory.js b/src/courseware/data/__factories__/discussionTopics.factory.js index 1aeb2b19..47f863e9 100644 --- a/src/courseware/data/__factories__/discussionTopics.factory.js +++ b/src/courseware/data/__factories__/discussionTopics.factory.js @@ -1,6 +1,13 @@ /* eslint-disable import/prefer-default-export */ import { Factory } from 'rosie'; // eslint-disable-line import/no-extraneous-dependencies +Factory.define('verifiedMode') + .attr('currency', 'USD') + .attr('currencySymbol', '$') + .attr('price', '$149') + .attr('sku', '8CF08E5') + .attr('upgradeUrl', 'http://localhost:18130/basket/add/?sku=8CF08E5'); + Factory.define('discussionTopic') .option('topicPrefix', null, '') .option('courseId', null, 'course-v1:edX+DemoX+Demo_Course') @@ -11,13 +18,14 @@ Factory.define('discussionTopic') ['id', 'courseId'], (idx, id, courseId) => `block-v1:${courseId.replace('course-v1:', '')}+type@vertical+block@${id}`, ) - .attr('enabled_in_context', null, true) + .attr('enabled_in_context', ['enabled_in_context'], (enabledInContext) => Boolean(enabledInContext)) + .attr('thread_counts', [], { discussion: 0, question: 0, }); // Given a pre-build units state, build topics from it. -export function buildTopicsFromUnits(units) { - return Object.values(units).map(unit => Factory.build('discussionTopic', { usage_key: unit.id })); +export function buildTopicsFromUnits(units, enabledInContext = true) { + return Object.values(units).map(unit => Factory.build('discussionTopic', { usage_key: unit.id, enabled_in_context: enabledInContext })); }