From 91a0f5b9489096abff06b9d436c02104c5cbe8cc Mon Sep 17 00:00:00 2001 From: sundasnoreen12 Date: Mon, 8 Jan 2024 16:55:01 +0500 Subject: [PATCH 1/3] test: added test cases for new sidebar --- src/courseware/course/Course.test.jsx | 34 ++-------- .../course/new-sidebar/common/SidebarBase.jsx | 3 +- .../discussions/DiscussionsWidget.test.jsx | 14 ++-- .../notifications/NotificationsWidget.jsx | 2 +- .../NotificationsWidget.test.jsx | 68 ++++++++++++++++++- src/courseware/course/test-utils.jsx | 56 +++++++++++++++ .../__factories__/discussionTopics.factory.js | 7 +- 7 files changed, 144 insertions(+), 40 deletions(-) create mode 100644 src/courseware/course/test-utils.jsx diff --git a/src/courseware/course/Course.test.jsx b/src/courseware/course/Course.test.jsx index 64de33b27a..40dd0da779 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 63e4607ec1..540b8fce87 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 519aa09d6b..84685bf738 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 26ea9ac285..e146fb7ae5 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,33 @@ 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 () => { + 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 +119,39 @@ describe('NotificationsWidget', () => { .toBeInTheDocument(); }); + it.each([ + { + description: 'successfully close the notification widget.', + enabledInContext: true, + testId: + 'notification-widget', + }, + { + description: 'successfully close the sidebar when we have no discussion widget and close the notification widget.', + enabledInContext: false, + testId: 'sidebar-DISCUSSIONS_NOTIFICATIONS', + }, + ])('case: %s', async ({ enabledInContext, testId }) => { + 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 0000000000..84a428e186 --- /dev/null +++ b/src/courseware/course/test-utils.jsx @@ -0,0 +1,56 @@ +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'; + +export const userVerifiedMode = { + accessExpirationDate: null, + currency: 'USD', + currencySymbol: '$', + price: 149, + sku: '8CF08E5', + upgradeUrl: 'http://localhost:18130/basket/add/?sku=8CF08E5', +}; + +const mockData = { + nextSequenceHandler: () => {}, + previousSequenceHandler: () => {}, + unitNavigationHandler: () => {}, +}; + +export 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; +}; diff --git a/src/courseware/data/__factories__/discussionTopics.factory.js b/src/courseware/data/__factories__/discussionTopics.factory.js index 1aeb2b1938..bef086beb4 100644 --- a/src/courseware/data/__factories__/discussionTopics.factory.js +++ b/src/courseware/data/__factories__/discussionTopics.factory.js @@ -11,13 +11,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 })); } From 25a7b71fd3151e09bb2ece829fed506d085fcb42 Mon Sep 17 00:00:00 2001 From: sundasnoreen12 Date: Mon, 8 Jan 2024 18:49:40 +0500 Subject: [PATCH 2/3] test: added factory for verified user --- src/courseware/course/Course.test.jsx | 2 +- .../notifications/NotificationsWidget.test.jsx | 12 ++++++++---- src/courseware/course/test-utils.jsx | 13 +++---------- .../data/__factories__/discussionTopics.factory.js | 7 +++++++ 4 files changed, 19 insertions(+), 15 deletions(-) diff --git a/src/courseware/course/Course.test.jsx b/src/courseware/course/Course.test.jsx index 40dd0da779..ab629700aa 100644 --- a/src/courseware/course/Course.test.jsx +++ b/src/courseware/course/Course.test.jsx @@ -10,7 +10,7 @@ import { import * as celebrationUtils from './celebration/utils'; import { handleNextSectionCelebration } from './celebration'; import Course from './Course'; -import { setupDiscussionSidebar } from './test-utils'; +import setupDiscussionSidebar from './test-utils'; jest.mock('@edx/frontend-platform/analytics'); jest.mock('@edx/frontend-lib-special-exams/dist/data/thunks.js', () => ({ diff --git a/src/courseware/course/new-sidebar/sidebars/discussions-notifications/notifications/NotificationsWidget.test.jsx b/src/courseware/course/new-sidebar/sidebars/discussions-notifications/notifications/NotificationsWidget.test.jsx index d3f5d2aa79..666856805f 100644 --- a/src/courseware/course/new-sidebar/sidebars/discussions-notifications/notifications/NotificationsWidget.test.jsx +++ b/src/courseware/course/new-sidebar/sidebars/discussions-notifications/notifications/NotificationsWidget.test.jsx @@ -16,7 +16,7 @@ import { appendBrowserTimezoneToUrl, executeThunk } from '../../../../../../util import { fetchCourse } from '../../../../../data'; import SidebarContext from '../../../SidebarContext'; import NotificationsWidget from './NotificationsWidget'; -import { setupDiscussionSidebar, userVerifiedMode } from '../../../../test-utils'; +import setupDiscussionSidebar from '../../../../test-utils'; initializeMockApp(); jest.mock('@edx/frontend-platform/analytics'); @@ -53,6 +53,8 @@ describe('NotificationsWidget', () => { }); 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 }); @@ -121,17 +123,19 @@ describe('NotificationsWidget', () => { it.each([ { - description: 'successfully close the notification widget.', + description: 'close the notification widget.', enabledInContext: true, testId: 'notification-widget', }, { - description: 'successfully close the sidebar when we have no discussion widget and close the notification widget.', + description: 'close the sidebar when we have no discussion widget and close the notification widget.', enabledInContext: false, testId: 'sidebar-DISCUSSIONS_NOTIFICATIONS', }, - ])('case: %s', async ({ enabledInContext, testId }) => { + ])('successfully %s', async ({ enabledInContext, testId }) => { + const userVerifiedMode = Factory.build('verifiedMode'); + await setupDiscussionSidebar(userVerifiedMode, enabledInContext); const sidebarButton = screen.getByRole('button', { name: /Show sidebar tray/i }); diff --git a/src/courseware/course/test-utils.jsx b/src/courseware/course/test-utils.jsx index 84a428e186..df7a7cbcc8 100644 --- a/src/courseware/course/test-utils.jsx +++ b/src/courseware/course/test-utils.jsx @@ -10,22 +10,13 @@ import { executeThunk } from '../../utils'; import * as thunks from '../data/thunks'; import Course from './Course'; -export const userVerifiedMode = { - accessExpirationDate: null, - currency: 'USD', - currencySymbol: '$', - price: 149, - sku: '8CF08E5', - upgradeUrl: 'http://localhost:18130/basket/add/?sku=8CF08E5', -}; - const mockData = { nextSequenceHandler: () => {}, previousSequenceHandler: () => {}, unitNavigationHandler: () => {}, }; -export const setupDiscussionSidebar = async (verifiedMode = null, enabledInContext = true) => { +const setupDiscussionSidebar = async (verifiedMode = null, enabledInContext = true) => { const store = await initializeTestStore(); const { courseware, models } = store.getState(); const { courseId, sequenceId } = courseware; @@ -54,3 +45,5 @@ export const setupDiscussionSidebar = async (verifiedMode = null, enabledInConte 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 bef086beb4..47f863e9a4 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') From eee2c0a58774355217211cce4c5680d13ff6a73f Mon Sep 17 00:00:00 2001 From: sundasnoreen12 Date: Wed, 10 Jan 2024 15:49:24 +0500 Subject: [PATCH 3/3] refactor: updated description for notification widget --- .../notifications/NotificationsWidget.test.jsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/courseware/course/new-sidebar/sidebars/discussions-notifications/notifications/NotificationsWidget.test.jsx b/src/courseware/course/new-sidebar/sidebars/discussions-notifications/notifications/NotificationsWidget.test.jsx index 666856805f..2438e82652 100644 --- a/src/courseware/course/new-sidebar/sidebars/discussions-notifications/notifications/NotificationsWidget.test.jsx +++ b/src/courseware/course/new-sidebar/sidebars/discussions-notifications/notifications/NotificationsWidget.test.jsx @@ -126,10 +126,10 @@ describe('NotificationsWidget', () => { description: 'close the notification widget.', enabledInContext: true, testId: - 'notification-widget', + 'notification-widget', }, { - description: 'close the sidebar when we have no discussion widget and close the notification widget.', + description: 'close the sidebar when the notification widget is closed, and the discussion widget is unavailable.', enabledInContext: false, testId: 'sidebar-DISCUSSIONS_NOTIFICATIONS', },