Skip to content

Commit 4ebd569

Browse files
feat: added open/close state of discussion sidebar in local storage (#1086)
1 parent 52235eb commit 4ebd569

File tree

4 files changed

+74
-3
lines changed

4 files changed

+74
-3
lines changed

src/courseware/course/Course.test.jsx

Lines changed: 60 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,18 @@
11
import React from 'react';
22
import { Factory } from 'rosie';
3+
import { getConfig } from '@edx/frontend-platform';
4+
import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth';
5+
import MockAdapter from 'axios-mock-adapter';
36
import { breakpoints } from '@edx/paragon';
47
import {
5-
fireEvent, getByRole, initializeTestStore, loadUnit, render, screen, waitFor,
8+
act, fireEvent, getByRole, initializeTestStore, loadUnit, render, screen, waitFor,
69
} from '../../setupTest';
10+
import { buildTopicsFromUnits } from '../data/__factories__/discussionTopics.factory';
711
import { handleNextSectionCelebration } from './celebration';
812
import * as celebrationUtils from './celebration/utils';
913
import Course from './Course';
14+
import { executeThunk } from '../../utils';
15+
import * as thunks from '../data/thunks';
1016

1117
jest.mock('@edx/frontend-platform/analytics');
1218

@@ -43,6 +49,28 @@ describe('Course', () => {
4349
setItemSpy.mockRestore();
4450
});
4551

52+
const setupDiscussionSidebar = async (storageValue = false) => {
53+
localStorage.clear();
54+
const testStore = await initializeTestStore({ provider: 'openedx' });
55+
const state = testStore.getState();
56+
const { courseware: { courseId } } = state;
57+
const axiosMock = new MockAdapter(getAuthenticatedHttpClient());
58+
axiosMock.onGet(`${getConfig().LMS_BASE_URL}/api/discussion/v1/courses/${courseId}`).reply(200, { provider: 'openedx' });
59+
const topicsResponse = buildTopicsFromUnits(state.models.units);
60+
axiosMock.onGet(`${getConfig().LMS_BASE_URL}/api/discussion/v2/course_topics/${courseId}`)
61+
.reply(200, topicsResponse);
62+
63+
await executeThunk(thunks.getCourseDiscussionTopics(courseId), testStore.dispatch);
64+
const [firstUnitId] = Object.keys(state.models.units);
65+
mockData.unitId = firstUnitId;
66+
const [firstSequenceId] = Object.keys(state.models.sequences);
67+
mockData.sequenceId = firstSequenceId;
68+
if (storageValue !== null) {
69+
localStorage.setItem('showDiscussionSidebar', storageValue);
70+
}
71+
await render(<Course {...mockData} />, { store: testStore });
72+
};
73+
4674
it('loads learning sequence', async () => {
4775
render(<Course {...mockData} />);
4876
expect(screen.getByRole('navigation', { name: 'breadcrumb' })).toBeInTheDocument();
@@ -103,6 +131,7 @@ describe('Course', () => {
103131
});
104132

105133
it('displays notification trigger and toggles active class on click', async () => {
134+
localStorage.setItem('showDiscussionSidebar', false);
106135
render(<Course {...mockData} />);
107136

108137
const notificationTrigger = screen.getByRole('button', { name: /Show notification tray/i });
@@ -114,6 +143,7 @@ describe('Course', () => {
114143

115144
it('handles click to open/close notification tray', async () => {
116145
sessionStorage.clear();
146+
localStorage.setItem('showDiscussionSidebar', false);
117147
render(<Course {...mockData} />);
118148
expect(sessionStorage.getItem(`notificationTrayStatus.${mockData.courseId}`)).toBe('"open"');
119149
const notificationShowButton = await screen.findByRole('button', { name: /Show notification tray/i });
@@ -144,6 +174,7 @@ describe('Course', () => {
144174

145175
it('handles sessionStorage from a different course for the notification tray', async () => {
146176
sessionStorage.clear();
177+
localStorage.setItem('showDiscussionSidebar', false);
147178
const courseMetadataSecondCourse = Factory.build('courseMetadata', { id: 'second_course' });
148179

149180
// set sessionStorage for a different course before rendering Course
@@ -186,6 +217,34 @@ describe('Course', () => {
186217
expect(screen.getByText(Object.values(models.sequences)[0].title)).toBeInTheDocument();
187218
});
188219

220+
[
221+
{ value: true, visible: true },
222+
{ value: false, visible: false },
223+
{ value: null, visible: true },
224+
].forEach(async ({ value, visible }) => (
225+
it(`discussion sidebar is ${visible ? 'shown' : 'hidden'} when localstorage value is ${value}`, async () => {
226+
await setupDiscussionSidebar(value);
227+
const element = await waitFor(() => screen.findByTestId('sidebar-DISCUSSIONS'));
228+
if (visible) {
229+
expect(element).not.toHaveClass('d-none');
230+
} else {
231+
expect(element).toHaveClass('d-none');
232+
}
233+
})));
234+
235+
[
236+
{ value: true, result: 'false' },
237+
{ value: false, result: 'true' },
238+
].forEach(async ({ value, result }) => (
239+
it(`Discussion sidebar storage value is ${!value} when sidebar is ${value ? 'closed' : 'open'}`, async () => {
240+
await setupDiscussionSidebar(value);
241+
await act(async () => {
242+
const button = await screen.queryByRole('button', { name: /Show discussions tray/i });
243+
button.click();
244+
});
245+
expect(localStorage.getItem('showDiscussionSidebar')).toBe(result);
246+
})));
247+
189248
it('passes handlers to the sequence', async () => {
190249
const nextSequenceHandler = jest.fn();
191250
const previousSequenceHandler = jest.fn();

src/courseware/course/sidebar/SidebarContextProvider.jsx

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,9 +19,13 @@ const SidebarProvider = ({
1919
const shouldDisplayFullScreen = useWindowSize().width < breakpoints.large.minWidth;
2020
const shouldDisplaySidebarOpen = useWindowSize().width > breakpoints.medium.minWidth;
2121
const showNotificationsOnLoad = getSessionStorage(`notificationTrayStatus.${courseId}`) !== 'closed';
22-
const initialSidebar = (verifiedMode && shouldDisplaySidebarOpen && showNotificationsOnLoad)
22+
const showDiscussionSidebar = localStorage.getItem('showDiscussionSidebar') !== 'false';
23+
const showNotificationSidebar = (verifiedMode && shouldDisplaySidebarOpen && showNotificationsOnLoad)
2324
? SIDEBARS.NOTIFICATIONS.ID
2425
: null;
26+
const initialSidebar = showDiscussionSidebar
27+
? SIDEBARS.DISCUSSIONS.ID
28+
: showNotificationSidebar;
2529
const [currentSidebar, setCurrentSidebar] = useState(initialSidebar);
2630
const [notificationStatus, setNotificationStatus] = useState(getLocalStorage(`notificationStatus.${courseId}`));
2731
const [upgradeNotificationCurrentState, setUpgradeNotificationCurrentState] = useState(getLocalStorage(`upgradeNotificationCurrentState.${courseId}`));
@@ -41,6 +45,11 @@ const SidebarProvider = ({
4145

4246
const toggleSidebar = useCallback((sidebarId) => {
4347
// Switch to new sidebar or hide the current sidebar
48+
if (currentSidebar === SIDEBARS.DISCUSSIONS.ID) {
49+
localStorage.setItem('showDiscussionSidebar', false);
50+
} else if (sidebarId === SIDEBARS.DISCUSSIONS.ID) {
51+
localStorage.setItem('showDiscussionSidebar', true);
52+
}
4453
setCurrentSidebar(sidebarId === currentSidebar ? null : sidebarId);
4554
}, [currentSidebar]);
4655

src/courseware/course/sidebar/common/SidebarBase.jsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ const SidebarBase = ({
4141
'min-vh-100': !shouldDisplayFullScreen,
4242
'd-none': currentSidebar !== sidebarId,
4343
}, className)}
44+
data-testid={`sidebar-${sidebarId}`}
4445
style={{ width: shouldDisplayFullScreen ? '100%' : width }}
4546
aria-label={ariaLabel}
4647
>

src/setupTest.js

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -137,10 +137,12 @@ export async function initializeTestStore(options = {}, overrideStore = true) {
137137
const discussionConfigUrl = new RegExp(`${getConfig().LMS_BASE_URL}/api/discussion/v1/courses/*`);
138138
courseHomeMetadataUrl = appendBrowserTimezoneToUrl(courseHomeMetadataUrl);
139139

140+
const provider = options?.provider || 'legacy';
141+
140142
axiosMock.onGet(courseMetadataUrl).reply(200, courseMetadata);
141143
axiosMock.onGet(courseHomeMetadataUrl).reply(200, courseHomeMetadata);
142144
axiosMock.onGet(learningSequencesUrlRegExp).reply(200, buildOutlineFromBlocks(courseBlocks));
143-
axiosMock.onGet(discussionConfigUrl).reply(200, { provider: 'legacy' });
145+
axiosMock.onGet(discussionConfigUrl).reply(200, { provider });
144146
sequenceMetadata.forEach(metadata => {
145147
const sequenceMetadataUrl = `${getConfig().LMS_BASE_URL}/api/courseware/sequence/${metadata.item_id}`;
146148
axiosMock.onGet(sequenceMetadataUrl).reply(200, metadata);

0 commit comments

Comments
 (0)