diff --git a/src/courseware/course/Course.test.jsx b/src/courseware/course/Course.test.jsx index 4eeb4f2060..5f936feef8 100644 --- a/src/courseware/course/Course.test.jsx +++ b/src/courseware/course/Course.test.jsx @@ -1,12 +1,18 @@ 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 { - fireEvent, getByRole, initializeTestStore, loadUnit, render, screen, waitFor, + 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 Course from './Course'; +import { executeThunk } from '../../utils'; +import * as thunks from '../data/thunks'; jest.mock('@edx/frontend-platform/analytics'); @@ -43,6 +49,28 @@ describe('Course', () => { setItemSpy.mockRestore(); }); + const setupDiscussionSidebar = async (storageValue = false) => { + localStorage.clear(); + const testStore = await initializeTestStore({ provider: 'openedx' }); + const state = testStore.getState(); + const { courseware: { courseId } } = state; + const axiosMock = new MockAdapter(getAuthenticatedHttpClient()); + axiosMock.onGet(`${getConfig().LMS_BASE_URL}/api/discussion/v1/courses/${courseId}`).reply(200, { provider: 'openedx' }); + const topicsResponse = buildTopicsFromUnits(state.models.units); + axiosMock.onGet(`${getConfig().LMS_BASE_URL}/api/discussion/v2/course_topics/${courseId}`) + .reply(200, topicsResponse); + + await executeThunk(thunks.getCourseDiscussionTopics(courseId), testStore.dispatch); + const [firstUnitId] = Object.keys(state.models.units); + mockData.unitId = firstUnitId; + const [firstSequenceId] = Object.keys(state.models.sequences); + mockData.sequenceId = firstSequenceId; + if (storageValue !== null) { + localStorage.setItem('showDiscussionSidebar', storageValue); + } + await render(, { store: testStore }); + }; + it('loads learning sequence', async () => { render(); expect(screen.getByRole('navigation', { name: 'breadcrumb' })).toBeInTheDocument(); @@ -103,6 +131,7 @@ describe('Course', () => { }); it('displays notification trigger and toggles active class on click', async () => { + localStorage.setItem('showDiscussionSidebar', false); render(); const notificationTrigger = screen.getByRole('button', { name: /Show notification tray/i }); @@ -114,6 +143,7 @@ describe('Course', () => { it('handles click to open/close notification tray', async () => { sessionStorage.clear(); + localStorage.setItem('showDiscussionSidebar', false); render(); expect(sessionStorage.getItem(`notificationTrayStatus.${mockData.courseId}`)).toBe('"open"'); const notificationShowButton = await screen.findByRole('button', { name: /Show notification tray/i }); @@ -144,6 +174,7 @@ describe('Course', () => { it('handles sessionStorage from a different course for the notification tray', async () => { sessionStorage.clear(); + localStorage.setItem('showDiscussionSidebar', false); const courseMetadataSecondCourse = Factory.build('courseMetadata', { id: 'second_course' }); // set sessionStorage for a different course before rendering Course @@ -186,6 +217,34 @@ describe('Course', () => { expect(screen.getByText(Object.values(models.sequences)[0].title)).toBeInTheDocument(); }); + [ + { value: true, visible: true }, + { value: false, visible: false }, + { value: null, visible: true }, + ].forEach(async ({ value, visible }) => ( + it(`discussion sidebar is ${visible ? 'shown' : 'hidden'} when localstorage value is ${value}`, async () => { + await setupDiscussionSidebar(value); + const element = await waitFor(() => screen.findByTestId('sidebar-DISCUSSIONS')); + if (visible) { + expect(element).not.toHaveClass('d-none'); + } else { + expect(element).toHaveClass('d-none'); + } + }))); + + [ + { value: true, result: 'false' }, + { value: false, result: 'true' }, + ].forEach(async ({ value, result }) => ( + it(`Discussion sidebar storage value is ${!value} when sidebar is ${value ? 'closed' : 'open'}`, async () => { + await setupDiscussionSidebar(value); + await act(async () => { + const button = await screen.queryByRole('button', { name: /Show discussions tray/i }); + button.click(); + }); + expect(localStorage.getItem('showDiscussionSidebar')).toBe(result); + }))); + it('passes handlers to the sequence', async () => { const nextSequenceHandler = jest.fn(); const previousSequenceHandler = jest.fn(); diff --git a/src/courseware/course/sidebar/SidebarContextProvider.jsx b/src/courseware/course/sidebar/SidebarContextProvider.jsx index a2cde920c5..c9a3b88324 100644 --- a/src/courseware/course/sidebar/SidebarContextProvider.jsx +++ b/src/courseware/course/sidebar/SidebarContextProvider.jsx @@ -19,9 +19,13 @@ const SidebarProvider = ({ const shouldDisplayFullScreen = useWindowSize().width < breakpoints.large.minWidth; const shouldDisplaySidebarOpen = useWindowSize().width > breakpoints.medium.minWidth; const showNotificationsOnLoad = getSessionStorage(`notificationTrayStatus.${courseId}`) !== 'closed'; - const initialSidebar = (verifiedMode && shouldDisplaySidebarOpen && showNotificationsOnLoad) + const showDiscussionSidebar = localStorage.getItem('showDiscussionSidebar') !== 'false'; + const showNotificationSidebar = (verifiedMode && shouldDisplaySidebarOpen && showNotificationsOnLoad) ? SIDEBARS.NOTIFICATIONS.ID : null; + const initialSidebar = showDiscussionSidebar + ? SIDEBARS.DISCUSSIONS.ID + : showNotificationSidebar; const [currentSidebar, setCurrentSidebar] = useState(initialSidebar); const [notificationStatus, setNotificationStatus] = useState(getLocalStorage(`notificationStatus.${courseId}`)); const [upgradeNotificationCurrentState, setUpgradeNotificationCurrentState] = useState(getLocalStorage(`upgradeNotificationCurrentState.${courseId}`)); @@ -41,6 +45,11 @@ const SidebarProvider = ({ const toggleSidebar = useCallback((sidebarId) => { // Switch to new sidebar or hide the current sidebar + if (currentSidebar === SIDEBARS.DISCUSSIONS.ID) { + localStorage.setItem('showDiscussionSidebar', false); + } else if (sidebarId === SIDEBARS.DISCUSSIONS.ID) { + localStorage.setItem('showDiscussionSidebar', true); + } setCurrentSidebar(sidebarId === currentSidebar ? null : sidebarId); }, [currentSidebar]); diff --git a/src/courseware/course/sidebar/common/SidebarBase.jsx b/src/courseware/course/sidebar/common/SidebarBase.jsx index 2e2e44fbed..3e581ca89e 100644 --- a/src/courseware/course/sidebar/common/SidebarBase.jsx +++ b/src/courseware/course/sidebar/common/SidebarBase.jsx @@ -41,6 +41,7 @@ const SidebarBase = ({ 'min-vh-100': !shouldDisplayFullScreen, 'd-none': currentSidebar !== sidebarId, }, className)} + data-testid={`sidebar-${sidebarId}`} style={{ width: shouldDisplayFullScreen ? '100%' : width }} aria-label={ariaLabel} > diff --git a/src/setupTest.js b/src/setupTest.js index 955440b3de..82aa7226e0 100755 --- a/src/setupTest.js +++ b/src/setupTest.js @@ -137,10 +137,12 @@ export async function initializeTestStore(options = {}, overrideStore = true) { const discussionConfigUrl = new RegExp(`${getConfig().LMS_BASE_URL}/api/discussion/v1/courses/*`); courseHomeMetadataUrl = appendBrowserTimezoneToUrl(courseHomeMetadataUrl); + const provider = options?.provider || 'legacy'; + axiosMock.onGet(courseMetadataUrl).reply(200, courseMetadata); axiosMock.onGet(courseHomeMetadataUrl).reply(200, courseHomeMetadata); axiosMock.onGet(learningSequencesUrlRegExp).reply(200, buildOutlineFromBlocks(courseBlocks)); - axiosMock.onGet(discussionConfigUrl).reply(200, { provider: 'legacy' }); + axiosMock.onGet(discussionConfigUrl).reply(200, { provider }); sequenceMetadata.forEach(metadata => { const sequenceMetadataUrl = `${getConfig().LMS_BASE_URL}/api/courseware/sequence/${metadata.item_id}`; axiosMock.onGet(sequenceMetadataUrl).reply(200, metadata);