Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -30,3 +30,5 @@ module.config.js
src/i18n/messages/

env.config.jsx

webpack.dev-tutor.config.js
4 changes: 4 additions & 0 deletions src/courseware/course/Course.test.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,10 @@ jest.mock(
}),
);

jest.mock('@src/data/sessionStorage', () => ({
getSessionStorage: jest.fn().mockReturnValue(null),
}));

const recordFirstSectionCelebration = jest.fn();
// eslint-disable-next-line no-import-assign
celebrationUtils.recordFirstSectionCelebration = recordFirstSectionCelebration;
Expand Down
7 changes: 5 additions & 2 deletions src/courseware/course/sidebar/SidebarContextProvider.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ import { useDispatch } from 'react-redux';
import { useSearchParams } from 'react-router-dom';

import { useModel } from '@src/generic/model-store';
import { getSessionStorage } from '@src/data/sessionStorage';
import { WIDGETS } from '@src/constants';

import SidebarContext from './SidebarContext';
import {
Expand Down Expand Up @@ -68,14 +70,15 @@ const SidebarProvider = ({
}, [getAvailableWidgets]);

// Calculate initial sidebar with priority cascade
const initialSidebar = useInitialSidebar({
const initialSidebarByPriority = useInitialSidebar({
courseId,
shouldDisplayFullScreen,
isInitiallySidebarOpen,
getFirstAvailablePanel,
getAvailableWidgets,
});

const isNotificationTrayOpen = getSessionStorage(`notificationTrayStatus.${courseId}`) === 'open';
const initialSidebar = isNotificationTrayOpen ? WIDGETS.NOTIFICATIONS : initialSidebarByPriority;
const [currentSidebar, setCurrentSidebar] = useState(initialSidebar);

// Track if user has manually toggled sidebar within current unit
Expand Down
14 changes: 14 additions & 0 deletions src/courseware/course/sidebar/SidebarContextProvider.test.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import {
render, screen, fireEvent, act,
} from '@testing-library/react';
import { MemoryRouter } from 'react-router-dom';
import { getSessionStorage } from '@src/data/sessionStorage';
import SidebarContext from './SidebarContext';
import SidebarProvider from './SidebarContextProvider';

Expand All @@ -20,6 +21,10 @@ jest.mock('@src/generic/model-store', () => ({
}),
}));

jest.mock('@src/data/sessionStorage', () => ({
getSessionStorage: jest.fn(() => null),
}));

jest.mock('@openedx/paragon', () => {
const actual = jest.requireActual('@openedx/paragon');
return {
Expand Down Expand Up @@ -105,6 +110,7 @@ describe('SidebarContextProvider', () => {
const storage = jest.requireMock('./utils/storage');
storage.getSidebarId.mockReturnValue(null);
storage.isOutlineSidebarCollapsed.mockReturnValue(false);
getSessionStorage.mockReturnValue(null);
});

describe('context values provided', () => {
Expand All @@ -120,6 +126,14 @@ describe('SidebarContextProvider', () => {
expect(screen.getByTestId('available-ids').textContent).toBe('DISCUSSIONS,NOTES');
});

it('opens the legacy notification tray when session storage marks it open', () => {
getSessionStorage.mockReturnValue('open');

renderProvider();

expect(screen.getByTestId('current-sidebar').textContent).toBe('UPGRADE');
});

it('excludes a widget from availableSidebarIds when isAvailable returns false', () => {
const { getEnabledWidgets } = jest.requireMock('./defaultWidgets');
getEnabledWidgets.mockReturnValueOnce([
Expand Down
22 changes: 16 additions & 6 deletions src/courseware/course/sidebar/common/SidebarBase.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,10 @@ import { ArrowBackIos, Close } from '@openedx/paragon/icons';
import classNames from 'classnames';
import PropTypes from 'prop-types';
import { useCallback, useContext } from 'react';

import { useEventListener } from '@src/generic/hooks';
import { WIDGETS } from '@src/constants';
import { setSessionStorage } from '@src/data/sessionStorage';
import messages from '../../messages';
import SidebarContext from '../SidebarContext';

Expand All @@ -19,18 +22,25 @@ const SidebarBase = ({
}) => {
const intl = useIntl();
const {
courseId,
toggleSidebar,
shouldDisplayFullScreen,
currentSidebar,
} = useContext(SidebarContext);

const handleCloseSidebar = useCallback(() => {
toggleSidebar(null);
if (sidebarId === WIDGETS.NOTIFICATIONS) {
setSessionStorage(`notificationTrayStatus.${courseId}`, 'closed');
}
}, [courseId, sidebarId, toggleSidebar]);

const receiveMessage = useCallback(({ data }) => {
const { type } = data;
if (type === 'learning.events.sidebar.close') {
toggleSidebar(null);
handleCloseSidebar();
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [sidebarId, toggleSidebar]);
}, [handleCloseSidebar]);

useEventListener('message', receiveMessage);

Expand All @@ -49,8 +59,8 @@ const SidebarBase = ({
{shouldDisplayFullScreen ? (
<div
className="pt-2 pb-2.5 border-bottom border-light-400 d-flex align-items-center ml-2"
onClick={() => toggleSidebar(null)}
onKeyDown={() => toggleSidebar(null)}
onClick={handleCloseSidebar}
onKeyDown={handleCloseSidebar}
role="button"
tabIndex="0"
>
Expand All @@ -71,7 +81,7 @@ const SidebarBase = ({
src={Close}
size="sm"
iconAs={Icon}
onClick={() => toggleSidebar(null)}
onClick={handleCloseSidebar}
variant="primary"
alt={intl.formatMessage(messages.closeSidebarTrigger)}
/>
Expand Down
16 changes: 16 additions & 0 deletions src/courseware/course/sidebar/common/SidebarBase.test.jsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,20 @@
import React from 'react';
import { IntlProvider } from '@edx/frontend-platform/i18n';
import { render, screen, fireEvent } from '@testing-library/react';
import { WIDGETS } from '@src/constants';
import { setSessionStorage } from '@src/data/sessionStorage';
import SidebarContext from '../SidebarContext';
import SidebarBase from './SidebarBase';

const mockToggleSidebar = jest.fn();
const courseId = 'course-test-123';

jest.mock('@src/data/sessionStorage', () => ({
setSessionStorage: jest.fn(),
}));

const defaultContextValue = {
courseId,
toggleSidebar: mockToggleSidebar,
shouldDisplayFullScreen: false,
currentSidebar: 'TEST_SIDEBAR',
Expand Down Expand Up @@ -82,6 +90,14 @@ describe('SidebarBase', () => {
expect(mockToggleSidebar).toHaveBeenCalledWith(null);
});

it('marks the notification tray as closed when closing the upgrade sidebar', () => {
renderSidebarBase({ sidebarId: WIDGETS.NOTIFICATIONS }, { currentSidebar: WIDGETS.NOTIFICATIONS });

fireEvent.click(screen.getByRole('button', { name: /close sidebar/i }));

expect(setSessionStorage).toHaveBeenCalledWith(`notificationTrayStatus.${courseId}`, 'closed');
});

it('does not render the back-to-course button', () => {
renderSidebarBase();

Expand Down
20 changes: 18 additions & 2 deletions src/widgets/upgrade/src/UpgradeTrigger.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import PropTypes from 'prop-types';
import SidebarContext from '@src/courseware/course/sidebar/SidebarContext';
import SidebarTriggerBase from '@src/courseware/course/sidebar/common/TriggerBase';
import { getLocalStorage, setLocalStorage } from '@src/data/localStorage';
import { getSessionStorage, setSessionStorage } from '@src/data/sessionStorage';
import { useUpgradeWidgetContext } from './UpgradeWidgetContext';
import UpgradeIcon from './UpgradeIcon';
import messages from './messages';
Expand All @@ -12,13 +13,21 @@ export const ID = 'UPGRADE';

const UpgradeTrigger = ({ onClick }) => {
const intl = useIntl();
const { courseId } = useContext(SidebarContext);
const { courseId, currentSidebar, toggleSidebar } = useContext(SidebarContext);
const {
upgradeWidgetStatus,
setUpgradeWidgetStatus,
upgradeCurrentState,
} = useUpgradeWidgetContext();

useEffect(() => {
const isNotificationTrayOpen = getSessionStorage(`notificationTrayStatus.${courseId}`) === 'open';

if (isNotificationTrayOpen && !currentSidebar && toggleSidebar) {
toggleSidebar(ID);
}
}, [courseId, currentSidebar, toggleSidebar]);

useEffect(() => {
if (!upgradeCurrentState) {
return;
Expand All @@ -30,8 +39,15 @@ const UpgradeTrigger = ({ onClick }) => {
}
}, [upgradeCurrentState, courseId, setUpgradeWidgetStatus]);

const handleClick = () => {
const isNotificationTrayOpen = getSessionStorage(`notificationTrayStatus.${courseId}`) === 'open';

setSessionStorage(`notificationTrayStatus.${courseId}`, isNotificationTrayOpen ? 'closed' : 'open');
onClick();
};

return (
<SidebarTriggerBase onClick={onClick} ariaLabel={intl.formatMessage(messages.openUpgradeTrigger)}>
<SidebarTriggerBase onClick={handleClick} ariaLabel={intl.formatMessage(messages.openUpgradeTrigger)}>
<UpgradeIcon status={upgradeWidgetStatus} upgradeColor="bg-danger-500" />
</SidebarTriggerBase>
);
Expand Down
49 changes: 49 additions & 0 deletions src/widgets/upgrade/src/UpgradeTrigger.test.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { IntlProvider } from '@edx/frontend-platform/i18n';
import { render, screen, fireEvent } from '@testing-library/react';
import SidebarContext from '@src/courseware/course/sidebar/SidebarContext';
import * as localStorageModule from '@src/data/localStorage';
import * as sessionStorageModule from '@src/data/sessionStorage';
import { UpgradeWidgetProvider } from './UpgradeWidgetContext';
import UpgradeTrigger from './UpgradeTrigger';

Expand All @@ -11,6 +12,11 @@ jest.mock('@src/data/localStorage', () => ({
setLocalStorage: jest.fn(),
}));

jest.mock('@src/data/sessionStorage', () => ({
getSessionStorage: jest.fn(() => null),
setSessionStorage: jest.fn(),
}));

const courseId = 'course-test-123';

function renderTrigger(sidebarContextOverrides = {}, localStorageOverride = null) {
Expand All @@ -35,6 +41,7 @@ function renderTrigger(sidebarContextOverrides = {}, localStorageOverride = null
describe('UpgradeTrigger', () => {
beforeEach(() => {
jest.clearAllMocks();
sessionStorageModule.getSessionStorage.mockReturnValue(null);
});

it('renders a button with the correct aria-label', () => {
Expand Down Expand Up @@ -66,6 +73,48 @@ describe('UpgradeTrigger', () => {
expect(onClick).toHaveBeenCalledTimes(1);
});

it('marks the notification tray as open when the trigger opens the panel', () => {
const onClick = jest.fn();
render(
<IntlProvider locale="en">
<SidebarContext.Provider value={{ courseId }}>
<UpgradeWidgetProvider>
<UpgradeTrigger onClick={onClick} />
</UpgradeWidgetProvider>
</SidebarContext.Provider>
</IntlProvider>,
);

fireEvent.click(screen.getByRole('button', { name: /show upgrade panel/i }));

expect(sessionStorageModule.setSessionStorage).toHaveBeenCalledWith(
`notificationTrayStatus.${courseId}`,
'open',
);
expect(onClick).toHaveBeenCalledTimes(1);
});

it('marks the notification tray as closed when the trigger closes the panel', () => {
sessionStorageModule.getSessionStorage.mockReturnValue('open');
renderTrigger({ currentSidebar: 'UPGRADE' });

fireEvent.click(screen.getByRole('button', { name: /show upgrade panel/i }));

expect(sessionStorageModule.setSessionStorage).toHaveBeenCalledWith(
`notificationTrayStatus.${courseId}`,
'closed',
);
});

it('reopens the upgrade panel when notification tray status is open', () => {
const toggleSidebar = jest.fn();
sessionStorageModule.getSessionStorage.mockReturnValue('open');

renderTrigger({ currentSidebar: null, toggleSidebar });

expect(toggleSidebar).toHaveBeenCalledWith('UPGRADE');
});

it('shows status dot when upgradeWidgetStatus is "active"', () => {
// Default status from localStorage is null, so provider defaults to "active"
renderTrigger();
Expand Down
Loading