diff --git a/src/courseware/course/Course.jsx b/src/courseware/course/Course.jsx index a33e8291..6a0310b9 100644 --- a/src/courseware/course/Course.jsx +++ b/src/courseware/course/Course.jsx @@ -61,20 +61,20 @@ function Course({ if (notificationTrayVisible) { setNotificationTray(false); } else { setNotificationTray(true); } }; - if (!getLocalStorage('notificationStatus')) { - setLocalStorage('notificationStatus', 'active'); // Show red dot on notificationTrigger until seen + if (!getLocalStorage(`notificationStatus.${courseId}`)) { + setLocalStorage(`notificationStatus.${courseId}`, 'active'); // Show red dot on notificationTrigger until seen } - if (!getLocalStorage('upgradeNotificationCurrentState')) { - setLocalStorage('upgradeNotificationCurrentState', 'initialize'); + if (!getLocalStorage(`upgradeNotificationCurrentState.${courseId}`)) { + setLocalStorage(`upgradeNotificationCurrentState.${courseId}`, 'initialize'); } - const [notificationStatus, setNotificationStatus] = useState(getLocalStorage('notificationStatus')); - const [upgradeNotificationCurrentState, setupgradeNotificationCurrentState] = useState(getLocalStorage('upgradeNotificationCurrentState')); + const [notificationStatus, setNotificationStatus] = useState(getLocalStorage(`notificationStatus.${courseId}`)); + const [upgradeNotificationCurrentState, setupgradeNotificationCurrentState] = useState(getLocalStorage(`upgradeNotificationCurrentState.${courseId}`)); const onNotificationSeen = () => { setNotificationStatus('inactive'); - setLocalStorage('notificationStatus', 'inactive'); + setLocalStorage(`notificationStatus.${courseId}`, 'inactive'); }; /** [MM-P2P] Experiment */ @@ -98,6 +98,7 @@ function Course({ { shouldDisplayNotificationTrigger ? ( { let mockData; - // let mockDataSameState; - // let mockDataDifferentState; + let getItemSpy; + let setItemSpy; const courseMetadata = Factory.build('courseMetadata'); beforeEach(async () => { await initializeTestStore({ courseMetadata, excludeFetchCourse: true, excludeFetchSequence: true }); mockData = { + courseId: courseMetadata.id, toggleNotificationTray: () => {}, isNotificationTrayVisible: () => {}, - notificationStatus: 'active', + notificationStatus: 'inactive', setNotificationStatus: () => {}, upgradeNotificationCurrentState: 'FPDdaysLeft', }; + // Jest does not support calls to localStorage, spying on localStorage's prototype directly instead + getItemSpy = jest.spyOn(Object.getPrototypeOf(window.localStorage), 'getItem'); + setItemSpy = jest.spyOn(Object.getPrototypeOf(window.localStorage), 'setItem'); }); - it('renders notification trigger icon with red dot when notificationStatus is active', async () => { - const { container } = render(); - expect(container).toBeInTheDocument(); - const buttonIcon = container.querySelectorAll('svg'); - expect(buttonIcon).toHaveLength(1); - expect(screen.getByTestId('notification-dot')).toBeInTheDocument(); - }); - - it('renders notification trigger icon WITHOUT red dot 3 seconds later', async () => { - const { container } = render(); - expect(container).toBeInTheDocument(); - jest.useFakeTimers(); - setTimeout(() => { - expect(screen.queryByRole('notification-dot')).not.toBeInTheDocument(); - }, 3000); - jest.runAllTimers(); - }); - - it('renders notification trigger icon WITHOUT red dot within the same phase', async () => { - const { container } = render( - , - ); - expect(container).toBeInTheDocument(); - const buttonIcon = container.querySelectorAll('svg'); - expect(buttonIcon).toHaveLength(1); - expect(screen.queryByRole('notification-dot')).not.toBeInTheDocument(); + afterAll(() => { + getItemSpy.mockRestore(); + setItemSpy.mockRestore(); }); it('handles onClick event toggling the notification tray', async () => { @@ -65,15 +45,85 @@ describe('Notification Trigger', () => { expect(toggleNotificationTray).toHaveBeenCalledTimes(1); }); - // rendering NotificationTrigger has the effect of calling UpdateUpgradeNotificationLastSeen() - // Verify that local storage was updated accordingly - it('we make the right updates when rendering a new phase (before -> after)', async () => { + it('renders notification trigger icon with red dot when notificationStatus is active', async () => { + const { container } = render(); + expect(container).toBeInTheDocument(); + const buttonIcon = container.querySelectorAll('svg'); + expect(buttonIcon).toHaveLength(1); + expect(screen.getByTestId('notification-dot')).toBeInTheDocument(); + }); + + it('renders notification trigger icon WITHOUT red dot 3 seconds later', async () => { + const { container } = render(); + expect(container).toBeInTheDocument(); + expect(screen.getByTestId('notification-dot')).toBeInTheDocument(); + jest.useFakeTimers(); + setTimeout(() => { + expect(localStorage.setItem).toHaveBeenCalledTimes(2); + expect(screen.queryByRole('notification-dot')).not.toBeInTheDocument(); + }, 3000); + jest.runAllTimers(); + }); + + it('renders notification trigger icon WITHOUT red dot within the same phase', async () => { const { container } = render( - , + , + ); + expect(container).toBeInTheDocument(); + expect(localStorage.getItem).toHaveBeenCalledWith(`upgradeNotificationLastSeen.${mockData.courseId}`); + expect(localStorage.getItem(`upgradeNotificationLastSeen.${mockData.courseId}`)).toBe('"sameState"'); + const buttonIcon = container.querySelectorAll('svg'); + expect(buttonIcon).toHaveLength(1); + expect(screen.queryByRole('notification-dot')).not.toBeInTheDocument(); + }); + + // Rendering NotificationTrigger has the effect of calling UpdateUpgradeNotificationLastSeen(), + // if upgradeNotificationLastSeen is different than upgradeNotificationCurrentState + // it should update localStorage accordingly + it('makes the right updates when rendering a new phase from an UpgradeNotification change (before -> after)', async () => { + const { container } = render( + , ); expect(container).toBeInTheDocument(); - expect(getLocalStorage('notificationStatus')).toBe('active'); - expect(getLocalStorage('upgradeNotificationLastSeen')).toBe('after'); + // verify localStorage get/set are called with correct arguments + expect(localStorage.getItem).toHaveBeenCalledWith(`upgradeNotificationLastSeen.${mockData.courseId}`); + expect(localStorage.setItem).toHaveBeenCalledWith(`notificationStatus.${mockData.courseId}`, '"active"'); + expect(localStorage.setItem).toHaveBeenCalledWith(`upgradeNotificationLastSeen.${mockData.courseId}`, '"after"'); + + // verify localStorage is updated accordingly + expect(localStorage.getItem(`upgradeNotificationLastSeen.${mockData.courseId}`)).toBe('"after"'); + expect(localStorage.getItem(`notificationStatus.${mockData.courseId}`)).toBe('"active"'); + }); + + it('handles localStorage from a different course', async () => { + const courseMetadataSecondCourse = Factory.build('courseMetadata'); + // set localStorage for a different course before rendering NotificationTrigger + localStorage.setItem(`upgradeNotificationLastSeen.${courseMetadataSecondCourse.id}`, '"accessDateView"'); + localStorage.setItem(`notificationStatus.${courseMetadataSecondCourse.id}`, '"inactive"'); + + const { container } = render( + , + ); + expect(container).toBeInTheDocument(); + // Verify localStorage was updated for the original course + expect(localStorage.getItem(`upgradeNotificationLastSeen.${mockData.courseId}`)).toBe('"after"'); + expect(localStorage.getItem(`notificationStatus.${mockData.courseId}`)).toBe('"active"'); + + // Verify the second course localStorage was not changed + expect(localStorage.getItem(`upgradeNotificationLastSeen.${courseMetadataSecondCourse.id}`)).toBe('"accessDateView"'); + expect(localStorage.getItem(`notificationStatus.${courseMetadataSecondCourse.id}`)).toBe('"inactive"'); }); }); diff --git a/src/courseware/course/sequence/Sequence.jsx b/src/courseware/course/sequence/Sequence.jsx index c072e4df..df13404c 100644 --- a/src/courseware/course/sequence/Sequence.jsx +++ b/src/courseware/course/sequence/Sequence.jsx @@ -182,6 +182,7 @@ function Sequence({ {shouldDisplayNotificationTrigger ? ( = 24) { // setupgradeNotificationCurrentState is available in NotificationTray (not course home) if (setupgradeNotificationCurrentState) { if (type === 'access') { setupgradeNotificationCurrentState('accessDaysLeft'); - setLocalStorage('upgradeNotificationCurrentState', 'accessDaysLeft'); + setLocalStorage(`upgradeNotificationCurrentState.${courseId}`, 'accessDaysLeft'); } if (type === 'offer') { setupgradeNotificationCurrentState('FPDdaysLeft'); - setLocalStorage('upgradeNotificationCurrentState', 'FPDdaysLeft'); + setLocalStorage(`upgradeNotificationCurrentState.${courseId}`, 'FPDdaysLeft'); } } expirationText = ( @@ -125,11 +127,11 @@ function ExpirationCountdown({ hoursToExpiration, setupgradeNotificationCurrentS if (setupgradeNotificationCurrentState) { if (type === 'access') { setupgradeNotificationCurrentState('accessHoursLeft'); - setLocalStorage('upgradeNotificationCurrentState', 'accessHoursLeft'); + setLocalStorage(`upgradeNotificationCurrentState.${courseId}`, 'accessHoursLeft'); } if (type === 'offer') { setupgradeNotificationCurrentState('FPDHoursLeft'); - setLocalStorage('upgradeNotificationCurrentState', 'FPDHoursLeft'); + setLocalStorage(`upgradeNotificationCurrentState.${courseId}`, 'FPDHoursLeft'); } } expirationText = ( @@ -148,11 +150,11 @@ function ExpirationCountdown({ hoursToExpiration, setupgradeNotificationCurrentS if (setupgradeNotificationCurrentState) { if (type === 'access') { setupgradeNotificationCurrentState('accessLastHour'); - setLocalStorage('upgradeNotificationCurrentState', 'accessLastHour'); + setLocalStorage(`upgradeNotificationCurrentState.${courseId}`, 'accessLastHour'); } if (type === 'offer') { setupgradeNotificationCurrentState('FPDLastHour'); - setLocalStorage('upgradeNotificationCurrentState', 'FPDLastHour'); + setLocalStorage(`upgradeNotificationCurrentState.${courseId}`, 'FPDLastHour'); } } expirationText = ( @@ -166,6 +168,7 @@ function ExpirationCountdown({ hoursToExpiration, setupgradeNotificationCurrentS } ExpirationCountdown.propTypes = { + courseId: PropTypes.string.isRequired, hoursToExpiration: PropTypes.number.isRequired, setupgradeNotificationCurrentState: PropTypes.func, type: PropTypes.string, @@ -175,10 +178,12 @@ ExpirationCountdown.defaultProps = { type: null, }; -function AccessExpirationDateBanner({ accessExpirationDate, timezoneFormatArgs, setupgradeNotificationCurrentState }) { +function AccessExpirationDateBanner({ + courseId, accessExpirationDate, timezoneFormatArgs, setupgradeNotificationCurrentState, +}) { if (setupgradeNotificationCurrentState) { setupgradeNotificationCurrentState('accessDateView'); - setLocalStorage('upgradeNotificationCurrentState', 'accessDateView'); + setLocalStorage(`upgradeNotificationCurrentState.${courseId}`, 'accessDateView'); } return (
@@ -202,6 +207,7 @@ function AccessExpirationDateBanner({ accessExpirationDate, timezoneFormatArgs, } AccessExpirationDateBanner.propTypes = { + courseId: PropTypes.string.isRequired, accessExpirationDate: PropTypes.PropTypes.instanceOf(Date).isRequired, timezoneFormatArgs: PropTypes.shape({ timeZone: PropTypes.string, @@ -298,6 +304,7 @@ function UpgradeNotification({ ); expirationBanner = (