From 2bf4f2a0b5c1d56fc241337ad4f90ce5720cdfd2 Mon Sep 17 00:00:00 2001 From: julianajlk Date: Tue, 21 Dec 2021 13:50:07 -0500 Subject: [PATCH] feat: Add NotificationTray persistence by course (#772) REV-2424 --- src/courseware/course/Course.jsx | 26 ++++++-- src/courseware/course/Course.test.jsx | 59 +++++++++++++++++++ src/courseware/course/sequence/Sequence.jsx | 6 +- .../course/sequence/Sequence.test.jsx | 15 +++++ .../SequenceNavigation.jsx | 8 +-- src/data/sessionStorage.js | 34 +++++++++++ 6 files changed, 137 insertions(+), 11 deletions(-) create mode 100644 src/data/sessionStorage.js diff --git a/src/courseware/course/Course.jsx b/src/courseware/course/Course.jsx index 6a0310b9..7da85b51 100644 --- a/src/courseware/course/Course.jsx +++ b/src/courseware/course/Course.jsx @@ -16,6 +16,7 @@ import NotificationTrigger from './NotificationTrigger'; import { useModel } from '../../generic/model-store'; import useWindowSize, { responsiveBreakpoints } from '../../generic/tabs/useWindowSize'; import { getLocalStorage, setLocalStorage } from '../../data/localStorage'; +import { getSessionStorage, setSessionStorage } from '../../data/sessionStorage'; /** [MM-P2P] Experiment */ import { initCoursewareMMP2P, MMP2PBlockModal } from '../../experiments/mm-p2p'; @@ -50,15 +51,32 @@ function Course({ courseId, sequenceId, unitId, celebrateFirstSection, dispatch, celebrations, ); - const shouldDisplayNotificationTrigger = useWindowSize().width >= responsiveBreakpoints.small.minWidth; + // Responsive breakpoints for showing the notification button/tray + const shouldDisplayNotificationTriggerInCourse = useWindowSize().width >= responsiveBreakpoints.small.minWidth; + const shouldDisplayNotificationTrayOpenOnLoad = useWindowSize().width > responsiveBreakpoints.medium.minWidth; - const shouldDisplayNotificationTrayOpen = useWindowSize().width > responsiveBreakpoints.medium.minWidth; + // Course specific notification tray open/closed persistance by browser session + if (!getSessionStorage(`notificationTrayStatus.${courseId}`)) { + if (shouldDisplayNotificationTrayOpenOnLoad) { + setSessionStorage(`notificationTrayStatus.${courseId}`, 'open'); + } else { + // responsive version displays the tray closed on initial load, set the sessionStorage to closed + setSessionStorage(`notificationTrayStatus.${courseId}`, 'closed'); + } + } const [notificationTrayVisible, setNotificationTray] = verifiedMode - && shouldDisplayNotificationTrayOpen ? useState(true) : useState(false); + && shouldDisplayNotificationTrayOpenOnLoad && getSessionStorage(`notificationTrayStatus.${courseId}`) !== 'closed' ? useState(true) : useState(false); + const isNotificationTrayVisible = () => notificationTrayVisible && setNotificationTray; + const toggleNotificationTray = () => { if (notificationTrayVisible) { setNotificationTray(false); } else { setNotificationTray(true); } + if (getSessionStorage(`notificationTrayStatus.${courseId}`) === 'open') { + setSessionStorage(`notificationTrayStatus.${courseId}`, 'closed'); + } else { + setSessionStorage(`notificationTrayStatus.${courseId}`, 'open'); + } }; if (!getLocalStorage(`notificationStatus.${courseId}`)) { @@ -96,7 +114,7 @@ function Course({ mmp2p={MMP2P} /> - { shouldDisplayNotificationTrigger ? ( + { shouldDisplayNotificationTriggerInCourse ? ( () =>
); jest.mock('../../generic/tabs/useWindowSize'); useWindowSize.mockReturnValue({ width: 1200 }); @@ -17,6 +18,8 @@ celebrationUtils.recordFirstSectionCelebration = recordFirstSectionCelebration; describe('Course', () => { let store; + let getItemSpy; + let setItemSpy; const mockData = { nextSequenceHandler: () => {}, previousSequenceHandler: () => {}, @@ -32,6 +35,13 @@ describe('Course', () => { sequenceId, unitId: Object.values(models.units)[0].id, }); + getItemSpy = jest.spyOn(Object.getPrototypeOf(window.sessionStorage), 'getItem'); + setItemSpy = jest.spyOn(Object.getPrototypeOf(window.sessionStorage), 'setItem'); + }); + + afterAll(() => { + getItemSpy.mockRestore(); + setItemSpy.mockRestore(); }); it('loads learning sequence', async () => { @@ -91,6 +101,55 @@ describe('Course', () => { expect(notificationTrigger).not.toHaveClass('trigger-active'); }); + it('handles click to open/close notification tray', async () => { + sessionStorage.clear(); + render(); + expect(sessionStorage.getItem(`notificationTrayStatus.${mockData.courseId}`)).toBe('"open"'); + const notificationShowButton = await screen.findByRole('button', { name: /Show notification tray/i }); + expect(screen.queryByTestId('NotificationTray')).toBeInTheDocument(); + fireEvent.click(notificationShowButton); + expect(sessionStorage.getItem(`notificationTrayStatus.${mockData.courseId}`)).toBe('"closed"'); + expect(screen.queryByTestId('NotificationTray')).not.toBeInTheDocument(); + }); + + it('handles reload persisting notification tray status', async () => { + sessionStorage.clear(); + render(); + const notificationShowButton = await screen.findByRole('button', { name: /Show notification tray/i }); + fireEvent.click(notificationShowButton); + expect(sessionStorage.getItem(`notificationTrayStatus.${mockData.courseId}`)).toBe('"closed"'); + + // Mock reload window, this doesn't happen in the Course component, + // calling the reload to check if the tray remains closed + const { location } = window; + delete window.location; + window.location = { reload: jest.fn() }; + window.location.reload(); + expect(window.location.reload).toHaveBeenCalled(); + window.location = location; + expect(sessionStorage.getItem(`notificationTrayStatus.${mockData.courseId}`)).toBe('"closed"'); + expect(screen.queryByTestId('NotificationTray')).not.toBeInTheDocument(); + }); + + it('handles sessionStorage from a different course for the notification tray', async () => { + sessionStorage.clear(); + const courseMetadataSecondCourse = Factory.build('courseMetadata'); + + // set sessionStorage for a different course before rendering Course + sessionStorage.setItem(`notificationTrayStatus.${courseMetadataSecondCourse.id}`, '"open"'); + + render(); + expect(sessionStorage.getItem(`notificationTrayStatus.${mockData.courseId}`)).toBe('"open"'); + const notificationShowButton = await screen.findByRole('button', { name: /Show notification tray/i }); + fireEvent.click(notificationShowButton); + + // Verify sessionStorage was updated for the original course + expect(sessionStorage.getItem(`notificationTrayStatus.${mockData.courseId}`)).toBe('"closed"'); + + // Verify the second course sessionStorage was not changed + expect(sessionStorage.getItem(`notificationTrayStatus.${courseMetadataSecondCourse.id}`)).toBe('"open"'); + }); + it('renders course breadcrumbs as expected', async () => { const courseMetadata = Factory.build('courseMetadata'); const unitBlocks = Array.from({ length: 3 }).map(() => Factory.build( diff --git a/src/courseware/course/sequence/Sequence.jsx b/src/courseware/course/sequence/Sequence.jsx index df13404c..fcf461f0 100644 --- a/src/courseware/course/sequence/Sequence.jsx +++ b/src/courseware/course/sequence/Sequence.jsx @@ -52,7 +52,7 @@ function Sequence({ const sequence = useModel('sequences', sequenceId); const unit = useModel('units', unitId); const sequenceStatus = useSelector(state => state.courseware.sequenceStatus); - const shouldDisplayNotificationTrigger = useWindowSize().width < responsiveBreakpoints.small.minWidth; + const shouldDisplayNotificationTriggerInSequence = useWindowSize().width < responsiveBreakpoints.small.minWidth; const handleNext = () => { const nextIndex = sequence.unitIds.indexOf(unitId) + 1; @@ -156,7 +156,7 @@ function Sequence({ const defaultContent = (
-
+
goToCourseExitPage()} /> - {shouldDisplayNotificationTrigger ? ( + {shouldDisplayNotificationTriggerInSequence ? ( { unitNavigationHandler: () => {}, nextSequenceHandler: () => {}, previousSequenceHandler: () => {}, + toggleNotificationTray: () => {}, + setNotificationStatus: () => {}, }; }); @@ -390,6 +392,19 @@ describe('Sequence', () => { expect(await screen.findByText('Notifications')).toBeInTheDocument(); }); + it('handles click on notification tray close button', async () => { + const toggleNotificationTray = jest.fn(); + const testData = { + ...mockData, + toggleNotificationTray, + notificationTrayVisible: true, + }; + render(); + const notificationCloseIconButton = await screen.findByRole('button', { name: /Close notification tray/i }); + fireEvent.click(notificationCloseIconButton); + expect(toggleNotificationTray).toHaveBeenCalledTimes(1); + }); + it('does not render notification tray in sequence by default if in responsive view', async () => { useWindowSize.mockReturnValue({ width: 991 }); const { container } = render(); diff --git a/src/courseware/course/sequence/sequence-navigation/SequenceNavigation.jsx b/src/courseware/course/sequence/sequence-navigation/SequenceNavigation.jsx index 337e921e..3ce8394b 100644 --- a/src/courseware/course/sequence/sequence-navigation/SequenceNavigation.jsx +++ b/src/courseware/course/sequence/sequence-navigation/SequenceNavigation.jsx @@ -44,7 +44,7 @@ function SequenceNavigation({ sequence.gatedContent !== undefined && sequence.gatedContent.gated ) : undefined; - const shouldDisplayNotificationTrigger = useWindowSize().width < responsiveBreakpoints.small.minWidth; + const shouldDisplayNotificationTriggerInSequence = useWindowSize().width < responsiveBreakpoints.small.minWidth; const renderUnitButtons = () => { if (isLocked) { @@ -76,7 +76,7 @@ function SequenceNavigation({ return ( ); }; @@ -84,9 +84,9 @@ function SequenceNavigation({ const prevArrow = isRtl(getLocale()) ? ChevronRight : ChevronLeft; return sequenceStatus === LOADED && ( -