feat: Update notification feature to be course specific (#742)

REV-2360
This commit is contained in:
julianajlk
2021-11-30 09:25:14 -05:00
committed by GitHub
parent c2b46d50a8
commit 8c43de9fc0
5 changed files with 118 additions and 56 deletions

View File

@@ -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 ? (
<NotificationTrigger
courseId={courseId}
toggleNotificationTray={toggleNotificationTray}
isNotificationTrayVisible={isNotificationTrayVisible}
notificationStatus={notificationStatus}

View File

@@ -8,7 +8,7 @@ import NotificationIcon from './NotificationIcon';
import messages from './messages';
function NotificationTrigger({
intl, toggleNotificationTray, isNotificationTrayVisible, notificationStatus, setNotificationStatus,
courseId, intl, toggleNotificationTray, isNotificationTrayVisible, notificationStatus, setNotificationStatus,
upgradeNotificationCurrentState,
}) {
/* Re-show a red dot beside the notification trigger for each of the 7 UpgradeNotification stages
@@ -16,10 +16,10 @@ function NotificationTrigger({
compare with the last state they've seen, and if it's different then set dot back to red */
function UpdateUpgradeNotificationLastSeen() {
if (upgradeNotificationCurrentState) {
if (getLocalStorage('upgradeNotificationLastSeen') !== upgradeNotificationCurrentState) {
if (getLocalStorage(`upgradeNotificationLastSeen.${courseId}`) !== upgradeNotificationCurrentState) {
setNotificationStatus('active');
setLocalStorage('notificationStatus', 'active');
setLocalStorage('upgradeNotificationLastSeen', upgradeNotificationCurrentState);
setLocalStorage(`notificationStatus.${courseId}`, 'active');
setLocalStorage(`upgradeNotificationLastSeen.${courseId}`, upgradeNotificationCurrentState);
}
}
}
@@ -39,6 +39,7 @@ function NotificationTrigger({
}
NotificationTrigger.propTypes = {
courseId: PropTypes.string.isRequired,
intl: intlShape.isRequired,
toggleNotificationTray: PropTypes.func.isRequired,
notificationStatus: PropTypes.string.isRequired,

View File

@@ -4,51 +4,31 @@ import {
render, initializeTestStore, screen, fireEvent,
} from '../../setupTest';
import NotificationTrigger from './NotificationTrigger';
import { getLocalStorage } from '../../data/localStorage';
describe('Notification Trigger', () => {
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(<NotificationTrigger {...mockData} />);
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(<NotificationTrigger {...mockData} />);
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(
<NotificationTrigger {...mockData} upgradeNotificationCurrentState="sameState" upgradeNotificationLastSeen="sameState" />,
);
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(<NotificationTrigger {...mockData} notificationStatus="active" />);
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(<NotificationTrigger {...mockData} notificationStatus="active" />);
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(
<NotificationTrigger {...mockData} upgradeNotificationLastSeen="before" upgradeNotificationCurrentState="after" />,
<NotificationTrigger
{...mockData}
upgradeNotificationCurrentState="sameState"
upgradeNotificationLastSeen="sameState"
/>,
);
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(
<NotificationTrigger
{...mockData}
upgradeNotificationLastSeen="before"
upgradeNotificationCurrentState="after"
/>,
);
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(
<NotificationTrigger
{...mockData}
upgradeNotificationLastSeen="before"
upgradeNotificationCurrentState="after"
/>,
);
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"');
});
});

View File

@@ -182,6 +182,7 @@ function Sequence({
{shouldDisplayNotificationTrigger ? (
<NotificationTrigger
courseId={courseId}
toggleNotificationTray={toggleNotificationTray}
isNotificationTrayVisible={isNotificationTrayVisible}
notificationStatus={notificationStatus}

View File

@@ -95,18 +95,20 @@ UpsellFBESoonCardContent.defaultProps = {
timezoneFormatArgs: {},
};
function ExpirationCountdown({ hoursToExpiration, setupgradeNotificationCurrentState, type }) {
function ExpirationCountdown({
courseId, hoursToExpiration, setupgradeNotificationCurrentState, type,
}) {
let expirationText;
if (hoursToExpiration >= 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 (
<div className="upsell-warning-light">
@@ -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 = (
<ExpirationCountdown
courseId={courseId}
hoursToExpiration={hoursToDiscountExpiration}
setupgradeNotificationCurrentState={setupgradeNotificationCurrentState}
type="offer"
@@ -312,6 +319,7 @@ function UpgradeNotification({
);
expirationBanner = (
<AccessExpirationDateBanner
courseId={courseId}
accessExpirationDate={accessExpirationDate}
timezoneFormatArgs={timezoneFormatArgs}
setupgradeNotificationCurrentState={setupgradeNotificationCurrentState}
@@ -328,6 +336,7 @@ function UpgradeNotification({
);
expirationBanner = (
<ExpirationCountdown
courseId={courseId}
hoursToExpiration={hoursToAccessExpiration}
setupgradeNotificationCurrentState={setupgradeNotificationCurrentState}
type="access"