Compare commits
17 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
83f3241e26 | ||
|
|
936c8714b7 | ||
|
|
6b18f28933 | ||
|
|
0ce451cfd2 | ||
|
|
42e831a693 | ||
|
|
9af7eaf587 | ||
|
|
3fbbde1044 | ||
|
|
c3f0282be4 | ||
|
|
ceb9c13542 | ||
|
|
7229bff3ff | ||
|
|
0695d4f400 | ||
|
|
de8783c708 | ||
|
|
e2540bc3a0 | ||
|
|
ffb5a765e2 | ||
|
|
952e543217 | ||
|
|
0e6a272f2b | ||
|
|
45a1da9f5e |
7837
package-lock.json
generated
7837
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
27
package.json
27
package.json
@@ -35,34 +35,35 @@
|
|||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@edx/brand": "npm:@edx/brand-openedx@1.2.0",
|
"@edx/brand": "npm:@edx/brand-openedx@1.2.0",
|
||||||
"@edx/browserslist-config": "^1.1.1",
|
"@edx/browserslist-config": "^1.1.1",
|
||||||
"@edx/frontend-build": "12.8.54",
|
"@edx/frontend-build": "12.8.61",
|
||||||
"@edx/frontend-platform": "4.5.1",
|
"@edx/frontend-platform": "4.6.0",
|
||||||
"@edx/reactifex": "^2.1.1",
|
"@edx/reactifex": "^2.1.1",
|
||||||
"@testing-library/dom": "9.3.1",
|
"@testing-library/dom": "9.3.1",
|
||||||
"@testing-library/jest-dom": "5.16.5",
|
"@testing-library/jest-dom": "5.16.5",
|
||||||
"@testing-library/react": "10.4.9",
|
"@testing-library/react": "10.4.9",
|
||||||
|
"@wojtekmaj/enzyme-adapter-react-17": "0.8.0",
|
||||||
"enzyme": "3.11.0",
|
"enzyme": "3.11.0",
|
||||||
"enzyme-adapter-react-16": "1.15.7",
|
|
||||||
"husky": "8.0.3",
|
"husky": "8.0.3",
|
||||||
"jest": "29.5.0",
|
"jest": "29.6.1",
|
||||||
"jest-chain": "1.1.6",
|
"jest-chain": "1.1.6",
|
||||||
"prop-types": "15.8.1",
|
"prop-types": "15.8.1",
|
||||||
"react": "16.14.0",
|
"react": "17.0.2",
|
||||||
"react-dom": "16.14.0",
|
"react-dom": "17.0.2",
|
||||||
"react-redux": "7.2.9",
|
"react-redux": "7.2.9",
|
||||||
"react-test-renderer": "16.14.0",
|
"react-router-dom": "5.3.4",
|
||||||
|
"react-test-renderer": "17.0.2",
|
||||||
"redux": "4.2.1",
|
"redux": "4.2.1",
|
||||||
"redux-saga": "1.2.3"
|
"redux-saga": "1.2.3"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@edx/paragon": "20.44.0",
|
"@edx/paragon": "20.45.0",
|
||||||
"@fortawesome/fontawesome-svg-core": "6.3.0",
|
"@fortawesome/fontawesome-svg-core": "6.4.0",
|
||||||
"@fortawesome/free-brands-svg-icons": "6.3.0",
|
"@fortawesome/free-brands-svg-icons": "6.4.0",
|
||||||
"@fortawesome/free-regular-svg-icons": "6.3.0",
|
"@fortawesome/free-regular-svg-icons": "6.4.0",
|
||||||
"@fortawesome/free-solid-svg-icons": "6.3.0",
|
"@fortawesome/free-solid-svg-icons": "6.4.0",
|
||||||
"@fortawesome/react-fontawesome": "^0.2.0",
|
"@fortawesome/react-fontawesome": "^0.2.0",
|
||||||
"@reduxjs/toolkit": "1.9.5",
|
"@reduxjs/toolkit": "1.9.5",
|
||||||
"axios-mock-adapter": "1.21.4",
|
"axios-mock-adapter": "1.21.5",
|
||||||
"babel-polyfill": "6.26.0",
|
"babel-polyfill": "6.26.0",
|
||||||
"classnames": "2.3.2",
|
"classnames": "2.3.2",
|
||||||
"lodash": "4.17.21",
|
"lodash": "4.17.21",
|
||||||
|
|||||||
@@ -29,27 +29,35 @@ const NotificationRowItem = ({
|
|||||||
className="d-flex mb-2 align-items-center text-decoration-none"
|
className="d-flex mb-2 align-items-center text-decoration-none"
|
||||||
to={contentUrl}
|
to={contentUrl}
|
||||||
onClick={handleMarkAsRead}
|
onClick={handleMarkAsRead}
|
||||||
|
data-testid={`notification-${id}`}
|
||||||
>
|
>
|
||||||
<Icon src={iconComponent} className={`${iconClass} mr-4 notification-icon`} />
|
<Icon
|
||||||
<div className="d-flex w-100">
|
src={iconComponent}
|
||||||
|
className={`${iconClass} mr-4 notification-icon`}
|
||||||
|
data-testid={`notification-icon-${id}`}
|
||||||
|
/>
|
||||||
|
<div className="d-flex w-100" data-testid="notification-contents">
|
||||||
<div className="d-flex align-items-center w-100">
|
<div className="d-flex align-items-center w-100">
|
||||||
<div className="py-10px w-100 px-0 cursor-pointer">
|
<div className="py-10px w-100 px-0 cursor-pointer">
|
||||||
<span
|
<span
|
||||||
className="line-height-24 text-gray-700 mb-2 notification-item-content overflow-hidden content"
|
className="line-height-24 text-gray-700 mb-2 notification-item-content overflow-hidden content"
|
||||||
// eslint-disable-next-line react/no-danger
|
// eslint-disable-next-line react/no-danger
|
||||||
dangerouslySetInnerHTML={{ __html: content }}
|
dangerouslySetInnerHTML={{ __html: content }}
|
||||||
|
data-testid={`notification-content-${id}`}
|
||||||
/>
|
/>
|
||||||
<div className="py-0 d-flex">
|
<div className="py-0 d-flex">
|
||||||
<span className="font-size-12 text-gray-500 line-height-20">
|
<span className="font-size-12 text-gray-500 line-height-20">
|
||||||
<span>{courseName}</span>
|
<span data-testid={`notification-course-${id}`}>{courseName}
|
||||||
|
</span>
|
||||||
<span className="text-light-700 px-1.5">{intl.formatMessage(messages.fullStop)}</span>
|
<span className="text-light-700 px-1.5">{intl.formatMessage(messages.fullStop)}</span>
|
||||||
<span>{timeago.format(createdAt, 'time-locale')}</span>
|
<span data-testid={`notification-created-date-${id}`}> {timeago.format(createdAt, 'time-locale')}
|
||||||
|
</span>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{!lastRead && (
|
{!lastRead && (
|
||||||
<div className="d-flex py-1.5 px-1.5 ml-2 cursor-pointer">
|
<div className="d-flex py-1.5 px-1.5 ml-2 cursor-pointer">
|
||||||
<span className="bg-brand-500 rounded unread" />
|
<span className="bg-brand-500 rounded unread" data-testid={`unread-notification-${id}`} />
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,21 +1,24 @@
|
|||||||
import React, { useCallback, useMemo } from 'react';
|
import React, { useCallback, useMemo } from 'react';
|
||||||
import { Button } from '@edx/paragon';
|
import { Button, Spinner } from '@edx/paragon';
|
||||||
import { useDispatch, useSelector } from 'react-redux';
|
import { useDispatch, useSelector } from 'react-redux';
|
||||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||||
import isEmpty from 'lodash/isEmpty';
|
import isEmpty from 'lodash/isEmpty';
|
||||||
import messages from './messages';
|
import messages from './messages';
|
||||||
import NotificationRowItem from './NotificationRowItem';
|
import NotificationRowItem from './NotificationRowItem';
|
||||||
import { markAllNotificationsAsRead } from './data/thunks';
|
import { markAllNotificationsAsRead } from './data/thunks';
|
||||||
import { selectNotificationsByIds, selectPaginationData, selectSelectedAppName } from './data/selectors';
|
import {
|
||||||
|
selectNotificationsByIds, selectPaginationData, selectSelectedAppName, selectNotificationStatus,
|
||||||
|
} from './data/selectors';
|
||||||
import { splitNotificationsByTime } from './utils';
|
import { splitNotificationsByTime } from './utils';
|
||||||
import { updatePaginationRequest } from './data/slice';
|
import { updatePaginationRequest, RequestStatus } from './data/slice';
|
||||||
|
|
||||||
const NotificationSections = () => {
|
const NotificationSections = () => {
|
||||||
const intl = useIntl();
|
const intl = useIntl();
|
||||||
const dispatch = useDispatch();
|
const dispatch = useDispatch();
|
||||||
const selectedAppName = useSelector(selectSelectedAppName());
|
const selectedAppName = useSelector(selectSelectedAppName());
|
||||||
|
const notificationRequestStatus = useSelector(selectNotificationStatus());
|
||||||
const notifications = useSelector(selectNotificationsByIds(selectedAppName));
|
const notifications = useSelector(selectNotificationsByIds(selectedAppName));
|
||||||
const { currentPage, numPages } = useSelector(selectPaginationData());
|
const { hasMorePages } = useSelector(selectPaginationData());
|
||||||
const { today = [], earlier = [] } = useMemo(
|
const { today = [], earlier = [] } = useMemo(
|
||||||
() => splitNotificationsByTime(notifications),
|
() => splitNotificationsByTime(notifications),
|
||||||
[notifications],
|
[notifications],
|
||||||
@@ -44,6 +47,7 @@ const NotificationSections = () => {
|
|||||||
variant="link"
|
variant="link"
|
||||||
className="text-info-500 font-size-14 line-height-10 text-decoration-none p-0 border-0"
|
className="text-info-500 font-size-14 line-height-10 text-decoration-none p-0 border-0"
|
||||||
onClick={handleMarkAllAsRead}
|
onClick={handleMarkAllAsRead}
|
||||||
|
data-testid="mark-all-read"
|
||||||
>
|
>
|
||||||
{intl.formatMessage(messages.notificationMarkAsRead)}
|
{intl.formatMessage(messages.notificationMarkAsRead)}
|
||||||
</Button>
|
</Button>
|
||||||
@@ -56,7 +60,7 @@ const NotificationSections = () => {
|
|||||||
type={notification.type}
|
type={notification.type}
|
||||||
contentUrl={notification.contentUrl}
|
contentUrl={notification.contentUrl}
|
||||||
content={notification.content}
|
content={notification.content}
|
||||||
courseName={notification.courseName}
|
courseName={notification.contentContext?.courseName || ''}
|
||||||
createdAt={notification.createdAt}
|
createdAt={notification.createdAt}
|
||||||
lastRead={notification.lastRead}
|
lastRead={notification.lastRead}
|
||||||
/>
|
/>
|
||||||
@@ -66,13 +70,24 @@ const NotificationSections = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="mt-4 px-4">
|
<div className="mt-4 px-4" data-testid="notification-tray-section">
|
||||||
{renderNotificationSection('today', today)}
|
{renderNotificationSection('today', today)}
|
||||||
{renderNotificationSection('earlier', earlier)}
|
{renderNotificationSection('earlier', earlier)}
|
||||||
{currentPage < numPages && (
|
{hasMorePages && notificationRequestStatus === RequestStatus.IN_PROGRESS ? (
|
||||||
<Button variant="primary" className="w-100 bg-primary-500" onClick={updatePagination}>
|
<div className="d-flex justify-content-center p-4">
|
||||||
{intl.formatMessage(messages.loadMoreNotifications)}
|
<Spinner animation="border" variant="primary" size="lg" />
|
||||||
</Button>
|
</div>
|
||||||
|
) : (hasMorePages && notificationRequestStatus === RequestStatus.SUCCESSFUL
|
||||||
|
&& (
|
||||||
|
<Button
|
||||||
|
variant="primary"
|
||||||
|
className="w-100 bg-primary-500"
|
||||||
|
onClick={updatePagination}
|
||||||
|
data-testid="load-more-notifications"
|
||||||
|
>
|
||||||
|
{intl.formatMessage(messages.loadMoreNotifications)}
|
||||||
|
</Button>
|
||||||
|
)
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ const NotificationTabs = () => {
|
|||||||
const { currentPage } = useSelector(selectPaginationData());
|
const { currentPage } = useSelector(selectPaginationData());
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
dispatch(fetchNotificationList({ appName: selectedAppName, page: currentPage, pageSize: 10 }));
|
dispatch(fetchNotificationList({ appName: selectedAppName, page: currentPage }));
|
||||||
if (selectedAppName) { dispatch(markNotificationsAsSeen(selectedAppName)); }
|
if (selectedAppName) { dispatch(markNotificationsAsSeen(selectedAppName)); }
|
||||||
}, [currentPage, selectedAppName]);
|
}, [currentPage, selectedAppName]);
|
||||||
|
|
||||||
@@ -32,6 +32,7 @@ const NotificationTabs = () => {
|
|||||||
title={appName}
|
title={appName}
|
||||||
notification={notificationUnseenCounts[appName]}
|
notification={notificationUnseenCounts[appName]}
|
||||||
tabClassName="pt-0 pb-10px px-2.5 d-flex border-top-0 mb-0 align-items-center line-height-24 text-capitalize"
|
tabClassName="pt-0 pb-10px px-2.5 d-flex border-top-0 mb-0 align-items-center line-height-24 text-capitalize"
|
||||||
|
data-testid={`notification-tab-${appName}`}
|
||||||
>
|
>
|
||||||
{appName === selectedAppName && (<NotificationSections />)}
|
{appName === selectedAppName && (<NotificationSections />)}
|
||||||
</Tab>
|
</Tab>
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ Factory.define('notificationsCount')
|
|||||||
.attr('count', 45)
|
.attr('count', 45)
|
||||||
.attr('countByAppName', {
|
.attr('countByAppName', {
|
||||||
reminders: 10,
|
reminders: 10,
|
||||||
discussions: 20,
|
discussion: 20,
|
||||||
grades: 10,
|
grades: 10,
|
||||||
authoring: 5,
|
authoring: 5,
|
||||||
})
|
})
|
||||||
@@ -13,10 +13,19 @@ Factory.define('notificationsCount')
|
|||||||
Factory.define('notification')
|
Factory.define('notification')
|
||||||
.sequence('id')
|
.sequence('id')
|
||||||
.attr('type', 'post')
|
.attr('type', 'post')
|
||||||
.sequence('content', ['id'], (idx, notificationId) => `<p><b>User ${idx}</b> posts <b>Hello and welcome to SC0x
|
.sequence('content', ['id'], (idx, notificationId) => `<p><strong>User ${idx}</strong> posts <strong>Hello and welcome to SC0x
|
||||||
${notificationId}!</b></p>`)
|
${notificationId}!</strong></p>`)
|
||||||
.attr('course_name', 'Supply Chain Analytics')
|
.attr('course_name', 'Supply Chain Analytics')
|
||||||
.sequence('content_url', (idx) => `https://example.com/${idx}`)
|
.sequence('content_url', (idx) => `https://example.com/${idx}`)
|
||||||
.attr('last_read', null)
|
.attr('last_read', null)
|
||||||
.attr('last_seen', null)
|
.attr('last_seen', null)
|
||||||
.sequence('created_at', ['createdDate'], (idx, date) => date);
|
.sequence('created_at', ['createdDate'], (idx, date) => date);
|
||||||
|
|
||||||
|
Factory.define('notificationsList')
|
||||||
|
.attr('next', null)
|
||||||
|
.attr('previous', null)
|
||||||
|
.attr('count', null, 2)
|
||||||
|
.attr('num_pages', null, 1)
|
||||||
|
.attr('current_page', null, 1)
|
||||||
|
.attr('start', null, 0)
|
||||||
|
.attr('results', ['results'], (results) => results || Factory.buildList('notification', 2, null, { createdDate: new Date().toISOString() }));
|
||||||
|
|||||||
@@ -2,19 +2,14 @@ import { getConfig, snakeCaseObject } from '@edx/frontend-platform';
|
|||||||
import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth';
|
import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth';
|
||||||
|
|
||||||
export const getNotificationsCountApiUrl = () => `${getConfig().LMS_BASE_URL}/api/notifications/count/`;
|
export const getNotificationsCountApiUrl = () => `${getConfig().LMS_BASE_URL}/api/notifications/count/`;
|
||||||
export const getNotificationsApiUrl = () => `${getConfig().LMS_BASE_URL}/api/notifications/`;
|
export const getNotificationsListApiUrl = () => `${getConfig().LMS_BASE_URL}/api/notifications/`;
|
||||||
export const markNotificationsSeenApiUrl = (appName) => `${getConfig().LMS_BASE_URL}/api/notifications/mark-notifications-unseen/${appName}/`;
|
export const markNotificationsSeenApiUrl = (appName) => `${getConfig().LMS_BASE_URL}/api/notifications/mark-seen/${appName}/`;
|
||||||
export const markNotificationAsReadApiUrl = () => `${getConfig().LMS_BASE_URL}/api/notifications/read/`;
|
export const markNotificationAsReadApiUrl = () => `${getConfig().LMS_BASE_URL}/api/notifications/read/`;
|
||||||
|
|
||||||
export async function getNotifications(appName, page, pageSize) {
|
export async function getNotificationsList(appName, page) {
|
||||||
const params = snakeCaseObject({ page, pageSize });
|
const params = snakeCaseObject({ appName, page });
|
||||||
const { data } = await getAuthenticatedHttpClient().get(getNotificationsApiUrl(), { params });
|
const { data } = await getAuthenticatedHttpClient().get(getNotificationsListApiUrl(), { params });
|
||||||
|
return data;
|
||||||
const startIndex = (page - 1) * pageSize;
|
|
||||||
const endIndex = startIndex + pageSize;
|
|
||||||
|
|
||||||
const notifications = data.slice(startIndex, endIndex);
|
|
||||||
return { notifications, numPages: 2, currentPage: page };
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getNotificationCounts() {
|
export async function getNotificationCounts() {
|
||||||
@@ -31,14 +26,14 @@ export async function markNotificationSeen(appName) {
|
|||||||
|
|
||||||
export async function markAllNotificationRead(appName) {
|
export async function markAllNotificationRead(appName) {
|
||||||
const params = snakeCaseObject({ appName });
|
const params = snakeCaseObject({ appName });
|
||||||
const { data } = await getAuthenticatedHttpClient().put(markNotificationAsReadApiUrl(), { params });
|
const { data } = await getAuthenticatedHttpClient().patch(markNotificationAsReadApiUrl(), params);
|
||||||
|
|
||||||
return data;
|
return data;
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function markNotificationRead(notificationId) {
|
export async function markNotificationRead(notificationId) {
|
||||||
const params = snakeCaseObject({ notificationId });
|
const params = snakeCaseObject({ notificationId });
|
||||||
const { data } = await getAuthenticatedHttpClient().put(markNotificationAsReadApiUrl(), { params });
|
const { data } = await getAuthenticatedHttpClient().patch(markNotificationAsReadApiUrl(), params);
|
||||||
|
|
||||||
return { data, id: notificationId };
|
return { data, id: notificationId };
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,15 +5,15 @@ import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth';
|
|||||||
import { initializeMockApp } from '@edx/frontend-platform/testing';
|
import { initializeMockApp } from '@edx/frontend-platform/testing';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
getNotificationsApiUrl, getNotificationsCountApiUrl, markNotificationAsReadApiUrl, markNotificationsSeenApiUrl,
|
getNotificationsListApiUrl, getNotificationsCountApiUrl, markNotificationAsReadApiUrl, markNotificationsSeenApiUrl,
|
||||||
getNotificationCounts, getNotifications, markNotificationSeen, markAllNotificationRead, markNotificationRead,
|
getNotificationCounts, getNotificationsList, markNotificationSeen, markAllNotificationRead, markNotificationRead,
|
||||||
} from './api';
|
} from './api';
|
||||||
|
|
||||||
import './__factories__';
|
import './__factories__';
|
||||||
|
|
||||||
const notificationCountsApiUrl = getNotificationsCountApiUrl();
|
const notificationCountsApiUrl = getNotificationsCountApiUrl();
|
||||||
const notificationsApiUrl = getNotificationsApiUrl();
|
const notificationsApiUrl = getNotificationsListApiUrl();
|
||||||
const markedAllNotificationsAsSeenApiUrl = markNotificationsSeenApiUrl('discussions');
|
const markedAllNotificationsAsSeenApiUrl = markNotificationsSeenApiUrl('discussion');
|
||||||
const markedAllNotificationsAsReadApiUrl = markNotificationAsReadApiUrl();
|
const markedAllNotificationsAsReadApiUrl = markNotificationAsReadApiUrl();
|
||||||
|
|
||||||
let axiosMock = null;
|
let axiosMock = null;
|
||||||
@@ -43,7 +43,7 @@ describe('Notifications API', () => {
|
|||||||
|
|
||||||
expect(count).toEqual(45);
|
expect(count).toEqual(45);
|
||||||
expect(countByAppName.reminders).toEqual(10);
|
expect(countByAppName.reminders).toEqual(10);
|
||||||
expect(countByAppName.discussions).toEqual(20);
|
expect(countByAppName.discussion).toEqual(20);
|
||||||
expect(countByAppName.grades).toEqual(10);
|
expect(countByAppName.grades).toEqual(10);
|
||||||
expect(countByAppName.authoring).toEqual(5);
|
expect(countByAppName.authoring).toEqual(5);
|
||||||
});
|
});
|
||||||
@@ -62,14 +62,11 @@ describe('Notifications API', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('Successfully get notifications.', async () => {
|
it('Successfully get notifications.', async () => {
|
||||||
axiosMock.onGet(notificationsApiUrl).reply(
|
axiosMock.onGet(notificationsApiUrl).reply(200, (Factory.build('notificationsList')));
|
||||||
200,
|
|
||||||
(Factory.buildList('notification', 2, null, { createdDate: new Date().toISOString() })),
|
|
||||||
);
|
|
||||||
|
|
||||||
const { notifications } = await getNotifications('discussions', 1, 10);
|
const notifications = await getNotificationsList('discussion', 1);
|
||||||
|
|
||||||
expect(notifications).toHaveLength(2);
|
expect(notifications.results).toHaveLength(2);
|
||||||
});
|
});
|
||||||
|
|
||||||
it.each([
|
it.each([
|
||||||
@@ -78,7 +75,7 @@ describe('Notifications API', () => {
|
|||||||
])('%s for notification API.', async ({ statusCode, message }) => {
|
])('%s for notification API.', async ({ statusCode, message }) => {
|
||||||
axiosMock.onGet(notificationsApiUrl).reply(statusCode, { message });
|
axiosMock.onGet(notificationsApiUrl).reply(statusCode, { message });
|
||||||
try {
|
try {
|
||||||
await getNotifications({ page: 1, pageSize: 10 });
|
await getNotificationsList('discussion', 1);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
expect(error.response.status).toEqual(statusCode);
|
expect(error.response.status).toEqual(statusCode);
|
||||||
expect(error.response.data.message).toEqual(message);
|
expect(error.response.data.message).toEqual(message);
|
||||||
@@ -88,7 +85,7 @@ describe('Notifications API', () => {
|
|||||||
it('Successfully marked all notifications as seen for selected app.', async () => {
|
it('Successfully marked all notifications as seen for selected app.', async () => {
|
||||||
axiosMock.onPut(markedAllNotificationsAsSeenApiUrl).reply(200, { message: 'Notifications marked seen.' });
|
axiosMock.onPut(markedAllNotificationsAsSeenApiUrl).reply(200, { message: 'Notifications marked seen.' });
|
||||||
|
|
||||||
const { message } = await markNotificationSeen('discussions');
|
const { message } = await markNotificationSeen('discussion');
|
||||||
|
|
||||||
expect(message).toEqual('Notifications marked seen.');
|
expect(message).toEqual('Notifications marked seen.');
|
||||||
});
|
});
|
||||||
@@ -99,7 +96,7 @@ describe('Notifications API', () => {
|
|||||||
])('%s for notification mark as seen API.', async ({ statusCode, message }) => {
|
])('%s for notification mark as seen API.', async ({ statusCode, message }) => {
|
||||||
axiosMock.onPut(markedAllNotificationsAsSeenApiUrl).reply(statusCode, { message });
|
axiosMock.onPut(markedAllNotificationsAsSeenApiUrl).reply(statusCode, { message });
|
||||||
try {
|
try {
|
||||||
await markNotificationSeen('discussions');
|
await markNotificationSeen('discussion');
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
expect(error.response.status).toEqual(statusCode);
|
expect(error.response.status).toEqual(statusCode);
|
||||||
expect(error.response.data.message).toEqual(message);
|
expect(error.response.data.message).toEqual(message);
|
||||||
@@ -107,9 +104,9 @@ describe('Notifications API', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('Successfully marked all notifications as read for selected app.', async () => {
|
it('Successfully marked all notifications as read for selected app.', async () => {
|
||||||
axiosMock.onPut(markedAllNotificationsAsReadApiUrl).reply(200, { message: 'Notifications marked read.' });
|
axiosMock.onPatch(markedAllNotificationsAsReadApiUrl).reply(200, { message: 'Notifications marked read.' });
|
||||||
|
|
||||||
const { message } = await markAllNotificationRead('discussions');
|
const { message } = await markAllNotificationRead('discussion');
|
||||||
|
|
||||||
expect(message).toEqual('Notifications marked read.');
|
expect(message).toEqual('Notifications marked read.');
|
||||||
});
|
});
|
||||||
@@ -118,9 +115,9 @@ describe('Notifications API', () => {
|
|||||||
{ statusCode: 404, message: 'Failed to mark all notifications as read for selected app.' },
|
{ statusCode: 404, message: 'Failed to mark all notifications as read for selected app.' },
|
||||||
{ statusCode: 403, message: 'Denied to mark all notifications as read for selected app.' },
|
{ statusCode: 403, message: 'Denied to mark all notifications as read for selected app.' },
|
||||||
])('%s for notification mark all as read API.', async ({ statusCode, message }) => {
|
])('%s for notification mark all as read API.', async ({ statusCode, message }) => {
|
||||||
axiosMock.onPut(markedAllNotificationsAsReadApiUrl).reply(statusCode, { message });
|
axiosMock.onPatch(markedAllNotificationsAsReadApiUrl).reply(statusCode, { message });
|
||||||
try {
|
try {
|
||||||
await markAllNotificationRead('discussions');
|
await markAllNotificationRead('discussion');
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
expect(error.response.status).toEqual(statusCode);
|
expect(error.response.status).toEqual(statusCode);
|
||||||
expect(error.response.data.message).toEqual(message);
|
expect(error.response.data.message).toEqual(message);
|
||||||
@@ -128,7 +125,7 @@ describe('Notifications API', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('Successfully marked notification as read.', async () => {
|
it('Successfully marked notification as read.', async () => {
|
||||||
axiosMock.onPut(markedAllNotificationsAsReadApiUrl).reply(200, { message: 'Notification marked read.' });
|
axiosMock.onPatch(markedAllNotificationsAsReadApiUrl).reply(200, { message: 'Notification marked read.' });
|
||||||
|
|
||||||
const { data } = await markNotificationRead(1);
|
const { data } = await markNotificationRead(1);
|
||||||
|
|
||||||
@@ -139,7 +136,7 @@ describe('Notifications API', () => {
|
|||||||
{ statusCode: 404, message: 'Failed to mark notification as read.' },
|
{ statusCode: 404, message: 'Failed to mark notification as read.' },
|
||||||
{ statusCode: 403, message: 'Denied to mark notification as read.' },
|
{ statusCode: 403, message: 'Denied to mark notification as read.' },
|
||||||
])('%s for notification mark as read API.', async ({ statusCode, message }) => {
|
])('%s for notification mark as read API.', async ({ statusCode, message }) => {
|
||||||
axiosMock.onPut(markedAllNotificationsAsReadApiUrl).reply(statusCode, { message });
|
axiosMock.onPatch(markedAllNotificationsAsReadApiUrl).reply(statusCode, { message });
|
||||||
try {
|
try {
|
||||||
await markAllNotificationRead(1);
|
await markAllNotificationRead(1);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|||||||
@@ -6,8 +6,9 @@ import { initializeMockApp } from '@edx/frontend-platform/testing';
|
|||||||
|
|
||||||
import { initializeStore } from '../../store';
|
import { initializeStore } from '../../store';
|
||||||
import executeThunk from '../../test-utils';
|
import executeThunk from '../../test-utils';
|
||||||
|
import mockNotificationsResponse from '../test-utils';
|
||||||
import {
|
import {
|
||||||
getNotificationsApiUrl, getNotificationsCountApiUrl, markNotificationAsReadApiUrl, markNotificationsSeenApiUrl,
|
getNotificationsListApiUrl, getNotificationsCountApiUrl, markNotificationAsReadApiUrl, markNotificationsSeenApiUrl,
|
||||||
} from './api';
|
} from './api';
|
||||||
import {
|
import {
|
||||||
fetchAppsNotificationCount, fetchNotificationList, markNotificationsAsRead, markAllNotificationsAsRead,
|
fetchAppsNotificationCount, fetchNotificationList, markNotificationsAsRead, markAllNotificationsAsRead,
|
||||||
@@ -17,9 +18,9 @@ import {
|
|||||||
import './__factories__';
|
import './__factories__';
|
||||||
|
|
||||||
const notificationCountsApiUrl = getNotificationsCountApiUrl();
|
const notificationCountsApiUrl = getNotificationsCountApiUrl();
|
||||||
const notificationsApiUrl = getNotificationsApiUrl();
|
const notificationsListApiUrl = getNotificationsListApiUrl();
|
||||||
const markedAllNotificationsAsReadApiUrl = markNotificationAsReadApiUrl();
|
const markedAllNotificationsAsReadApiUrl = markNotificationAsReadApiUrl();
|
||||||
const markedAllNotificationsAsSeenApiUrl = markNotificationsSeenApiUrl('discussions');
|
const markedAllNotificationsAsSeenApiUrl = markNotificationsSeenApiUrl('discussion');
|
||||||
|
|
||||||
let axiosMock;
|
let axiosMock;
|
||||||
let store;
|
let store;
|
||||||
@@ -38,13 +39,7 @@ describe('Notification Redux', () => {
|
|||||||
Factory.resetAll();
|
Factory.resetAll();
|
||||||
store = initializeStore();
|
store = initializeStore();
|
||||||
|
|
||||||
axiosMock.onGet(notificationCountsApiUrl).reply(200, (Factory.build('notificationsCount')));
|
({ store, axiosMock } = await mockNotificationsResponse());
|
||||||
axiosMock.onGet(notificationsApiUrl).reply(
|
|
||||||
200,
|
|
||||||
(Factory.buildList('notification', 2, null, { createdDate: new Date().toISOString() })),
|
|
||||||
);
|
|
||||||
await executeThunk(fetchAppsNotificationCount(), store.dispatch, store.getState);
|
|
||||||
await executeThunk(fetchNotificationList({ page: 1, pageSize: 10 }), store.dispatch, store.getState);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
@@ -57,30 +52,26 @@ describe('Notification Redux', () => {
|
|||||||
const { notifications } = store.getState();
|
const { notifications } = store.getState();
|
||||||
|
|
||||||
expect(notifications.notificationStatus).toEqual('idle');
|
expect(notifications.notificationStatus).toEqual('idle');
|
||||||
expect(notifications.appName).toEqual('discussions');
|
expect(notifications.appName).toEqual('discussion');
|
||||||
expect(notifications.appsId).toHaveLength(0);
|
expect(notifications.appsId).toHaveLength(0);
|
||||||
expect(notifications.apps).toEqual({});
|
expect(notifications.apps).toEqual({});
|
||||||
expect(notifications.notifications).toEqual({});
|
expect(notifications.notifications).toEqual({});
|
||||||
expect(notifications.tabsCount).toEqual({});
|
expect(notifications.tabsCount).toEqual({});
|
||||||
expect(notifications.showNotificationsTray).toEqual(false);
|
expect(notifications.showNotificationsTray).toEqual(false);
|
||||||
expect(notifications.pagination.count).toEqual(10);
|
expect(notifications.pagination).toEqual({});
|
||||||
expect(notifications.pagination.numPages).toEqual(1);
|
|
||||||
expect(notifications.pagination.currentPage).toEqual(1);
|
|
||||||
expect(notifications.pagination.nextPage).toBeNull();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('Successfully loaded notifications list in the redux.', async () => {
|
it('Successfully loaded notifications list in the redux.', async () => {
|
||||||
const { notifications: { notifications } } = store.getState();
|
const { notifications: { notifications } } = store.getState();
|
||||||
|
expect(Object.keys(notifications)).toHaveLength(10);
|
||||||
expect(Object.keys(notifications)).toHaveLength(2);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it.each([
|
it.each([
|
||||||
{ statusCode: 404, status: 'failed' },
|
{ statusCode: 404, status: 'failed' },
|
||||||
{ statusCode: 403, status: 'denied' },
|
{ statusCode: 403, status: 'denied' },
|
||||||
])('%s to load notifications list in the redux.', async ({ statusCode, status }) => {
|
])('%s to load notifications list in the redux.', async ({ statusCode, status }) => {
|
||||||
axiosMock.onGet(notificationsApiUrl).reply(statusCode);
|
axiosMock.onGet(notificationsListApiUrl).reply(statusCode);
|
||||||
await executeThunk(fetchNotificationList({ page: 1, pageSize: 10 }), store.dispatch, store.getState);
|
await executeThunk(fetchNotificationList({ page: 1 }), store.dispatch, store.getState);
|
||||||
|
|
||||||
const { notifications: { notificationStatus } } = store.getState();
|
const { notifications: { notificationStatus } } = store.getState();
|
||||||
|
|
||||||
@@ -92,7 +83,7 @@ describe('Notification Redux', () => {
|
|||||||
|
|
||||||
expect(tabsCount.count).toEqual(25);
|
expect(tabsCount.count).toEqual(25);
|
||||||
expect(tabsCount.reminders).toEqual(10);
|
expect(tabsCount.reminders).toEqual(10);
|
||||||
expect(tabsCount.discussions).toEqual(0);
|
expect(tabsCount.discussion).toEqual(0);
|
||||||
expect(tabsCount.grades).toEqual(10);
|
expect(tabsCount.grades).toEqual(10);
|
||||||
expect(tabsCount.authoring).toEqual(5);
|
expect(tabsCount.authoring).toEqual(5);
|
||||||
});
|
});
|
||||||
@@ -111,7 +102,7 @@ describe('Notification Redux', () => {
|
|||||||
|
|
||||||
it('Successfully marked all notifications as seen for selected app.', async () => {
|
it('Successfully marked all notifications as seen for selected app.', async () => {
|
||||||
axiosMock.onPut(markedAllNotificationsAsSeenApiUrl).reply(200);
|
axiosMock.onPut(markedAllNotificationsAsSeenApiUrl).reply(200);
|
||||||
await executeThunk(markNotificationsAsSeen('discussions'), store.dispatch, store.getState);
|
await executeThunk(markNotificationsAsSeen('discussion'), store.dispatch, store.getState);
|
||||||
|
|
||||||
expect(store.getState().notifications.notificationStatus).toEqual('successful');
|
expect(store.getState().notifications.notificationStatus).toEqual('successful');
|
||||||
});
|
});
|
||||||
@@ -121,7 +112,7 @@ describe('Notification Redux', () => {
|
|||||||
{ statusCode: 403, status: 'denied' },
|
{ statusCode: 403, status: 'denied' },
|
||||||
])('%s to mark all notifications as seen for selected app.', async ({ statusCode, status }) => {
|
])('%s to mark all notifications as seen for selected app.', async ({ statusCode, status }) => {
|
||||||
axiosMock.onPut(markedAllNotificationsAsSeenApiUrl).reply(statusCode);
|
axiosMock.onPut(markedAllNotificationsAsSeenApiUrl).reply(statusCode);
|
||||||
await executeThunk(markNotificationsAsSeen('discussions'), store.dispatch, store.getState);
|
await executeThunk(markNotificationsAsSeen('discussion'), store.dispatch, store.getState);
|
||||||
|
|
||||||
const { notifications: { notificationStatus } } = store.getState();
|
const { notifications: { notificationStatus } } = store.getState();
|
||||||
|
|
||||||
@@ -129,8 +120,8 @@ describe('Notification Redux', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('Successfully marked all notifications as read for selected app in the redux.', async () => {
|
it('Successfully marked all notifications as read for selected app in the redux.', async () => {
|
||||||
axiosMock.onPut(markedAllNotificationsAsReadApiUrl).reply(200);
|
axiosMock.onPatch(markedAllNotificationsAsReadApiUrl).reply(200);
|
||||||
await executeThunk(markAllNotificationsAsRead('discussions'), store.dispatch, store.getState);
|
await executeThunk(markAllNotificationsAsRead('discussion'), store.dispatch, store.getState);
|
||||||
|
|
||||||
const { notifications: { notificationStatus, notifications } } = store.getState();
|
const { notifications: { notificationStatus, notifications } } = store.getState();
|
||||||
const firstNotification = Object.values(notifications)[0];
|
const firstNotification = Object.values(notifications)[0];
|
||||||
@@ -140,7 +131,7 @@ describe('Notification Redux', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('Successfully marked notification as read in the redux.', async () => {
|
it('Successfully marked notification as read in the redux.', async () => {
|
||||||
axiosMock.onPut(markedAllNotificationsAsReadApiUrl).reply(200);
|
axiosMock.onPatch(markedAllNotificationsAsReadApiUrl).reply(200);
|
||||||
await executeThunk(markNotificationsAsRead(1), store.dispatch, store.getState);
|
await executeThunk(markNotificationsAsRead(1), store.dispatch, store.getState);
|
||||||
|
|
||||||
const { notifications: { notificationStatus, notifications } } = store.getState();
|
const { notifications: { notificationStatus, notifications } } = store.getState();
|
||||||
@@ -154,7 +145,7 @@ describe('Notification Redux', () => {
|
|||||||
{ statusCode: 404, status: 'failed' },
|
{ statusCode: 404, status: 'failed' },
|
||||||
{ statusCode: 403, status: 'denied' },
|
{ statusCode: 403, status: 'denied' },
|
||||||
])('%s to marked notification as read in the redux.', async ({ statusCode, status }) => {
|
])('%s to marked notification as read in the redux.', async ({ statusCode, status }) => {
|
||||||
axiosMock.onPut(markedAllNotificationsAsReadApiUrl).reply(statusCode);
|
axiosMock.onPatch(markedAllNotificationsAsReadApiUrl).reply(statusCode);
|
||||||
await executeThunk(markNotificationsAsRead(1), store.dispatch, store.getState);
|
await executeThunk(markNotificationsAsRead(1), store.dispatch, store.getState);
|
||||||
|
|
||||||
const { notifications: { notificationStatus } } = store.getState();
|
const { notifications: { notificationStatus } } = store.getState();
|
||||||
|
|||||||
@@ -5,8 +5,7 @@ import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth';
|
|||||||
import { initializeMockApp } from '@edx/frontend-platform/testing';
|
import { initializeMockApp } from '@edx/frontend-platform/testing';
|
||||||
|
|
||||||
import { initializeStore } from '../../store';
|
import { initializeStore } from '../../store';
|
||||||
import executeThunk from '../../test-utils';
|
import mockNotificationsResponse from '../test-utils';
|
||||||
import { getNotificationsApiUrl, getNotificationsCountApiUrl } from './api';
|
|
||||||
import {
|
import {
|
||||||
selectNotifications,
|
selectNotifications,
|
||||||
selectNotificationsByIds,
|
selectNotificationsByIds,
|
||||||
@@ -18,13 +17,9 @@ import {
|
|||||||
selectSelectedAppNotificationIds,
|
selectSelectedAppNotificationIds,
|
||||||
selectShowNotificationTray,
|
selectShowNotificationTray,
|
||||||
} from './selectors';
|
} from './selectors';
|
||||||
import { fetchAppsNotificationCount, fetchNotificationList } from './thunks';
|
|
||||||
|
|
||||||
import './__factories__';
|
import './__factories__';
|
||||||
|
|
||||||
const notificationCountsApiUrl = getNotificationsCountApiUrl();
|
|
||||||
const notificationsApiUrl = getNotificationsApiUrl();
|
|
||||||
|
|
||||||
let axiosMock;
|
let axiosMock;
|
||||||
let store;
|
let store;
|
||||||
|
|
||||||
@@ -42,13 +37,7 @@ describe('Notification Selectors', () => {
|
|||||||
Factory.resetAll();
|
Factory.resetAll();
|
||||||
store = initializeStore();
|
store = initializeStore();
|
||||||
|
|
||||||
axiosMock.onGet(notificationCountsApiUrl).reply(200, (Factory.build('notificationsCount')));
|
({ store, axiosMock } = await mockNotificationsResponse());
|
||||||
axiosMock.onGet(notificationsApiUrl).reply(
|
|
||||||
200,
|
|
||||||
(Factory.buildList('notification', 2, null, { createdDate: new Date().toISOString() })),
|
|
||||||
);
|
|
||||||
await executeThunk(fetchAppsNotificationCount(), store.dispatch, store.getState);
|
|
||||||
await executeThunk(fetchNotificationList({ page: 1, pageSize: 10 }), store.dispatch, store.getState);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
@@ -68,7 +57,7 @@ describe('Notification Selectors', () => {
|
|||||||
|
|
||||||
expect(tabsCount.count).toEqual(25);
|
expect(tabsCount.count).toEqual(25);
|
||||||
expect(tabsCount.reminders).toEqual(10);
|
expect(tabsCount.reminders).toEqual(10);
|
||||||
expect(tabsCount.discussions).toEqual(0);
|
expect(tabsCount.discussion).toEqual(0);
|
||||||
expect(tabsCount.grades).toEqual(10);
|
expect(tabsCount.grades).toEqual(10);
|
||||||
expect(tabsCount.authoring).toEqual(5);
|
expect(tabsCount.authoring).toEqual(5);
|
||||||
});
|
});
|
||||||
@@ -82,9 +71,9 @@ describe('Notification Selectors', () => {
|
|||||||
|
|
||||||
it('Should return selected app notification ids.', async () => {
|
it('Should return selected app notification ids.', async () => {
|
||||||
const state = store.getState();
|
const state = store.getState();
|
||||||
const notificationIds = selectSelectedAppNotificationIds('discussions')(state);
|
const notificationIds = selectSelectedAppNotificationIds('discussion')(state);
|
||||||
|
|
||||||
expect(notificationIds).toHaveLength(2);
|
expect(notificationIds).toHaveLength(10);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('Should return show notification tray status.', async () => {
|
it('Should return show notification tray status.', async () => {
|
||||||
@@ -98,29 +87,29 @@ describe('Notification Selectors', () => {
|
|||||||
const state = store.getState();
|
const state = store.getState();
|
||||||
const notifications = selectNotifications()(state);
|
const notifications = selectNotifications()(state);
|
||||||
|
|
||||||
expect(Object.keys(notifications)).toHaveLength(2);
|
expect(Object.keys(notifications)).toHaveLength(10);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('Should return notifications from Ids.', async () => {
|
it('Should return notifications from Ids.', async () => {
|
||||||
const state = store.getState();
|
const state = store.getState();
|
||||||
const notifications = selectNotificationsByIds('discussions')(state);
|
const notifications = selectNotificationsByIds('discussion')(state);
|
||||||
|
|
||||||
expect(notifications).toHaveLength(2);
|
expect(notifications).toHaveLength(10);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('Should return selected app name.', async () => {
|
it('Should return selected app name.', async () => {
|
||||||
const state = store.getState();
|
const state = store.getState();
|
||||||
const appName = selectSelectedAppName()(state);
|
const appName = selectSelectedAppName()(state);
|
||||||
|
|
||||||
expect(appName).toEqual('discussions');
|
expect(appName).toEqual('discussion');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('Should return pagination data.', async () => {
|
it('Should return pagination data.', async () => {
|
||||||
const state = store.getState();
|
const state = store.getState();
|
||||||
const paginationData = selectPaginationData()(state);
|
const paginationData = selectPaginationData()(state);
|
||||||
|
|
||||||
expect(paginationData.count).toEqual(10);
|
|
||||||
expect(paginationData.currentPage).toEqual(1);
|
expect(paginationData.currentPage).toEqual(1);
|
||||||
expect(paginationData.numPages).toEqual(2);
|
expect(paginationData.numPages).toEqual(2);
|
||||||
|
expect(paginationData.hasMorePages).toEqual(true);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -3,26 +3,21 @@ import { createSlice } from '@reduxjs/toolkit';
|
|||||||
|
|
||||||
export const RequestStatus = {
|
export const RequestStatus = {
|
||||||
IDLE: 'idle',
|
IDLE: 'idle',
|
||||||
LOADING: 'in-progress',
|
IN_PROGRESS: 'in-progress',
|
||||||
LOADED: 'successful',
|
SUCCESSFUL: 'successful',
|
||||||
FAILED: 'failed',
|
FAILED: 'failed',
|
||||||
DENIED: 'denied',
|
DENIED: 'denied',
|
||||||
};
|
};
|
||||||
|
|
||||||
const initialState = {
|
const initialState = {
|
||||||
notificationStatus: 'idle',
|
notificationStatus: RequestStatus.IDLE,
|
||||||
appName: 'discussions',
|
appName: 'discussion',
|
||||||
appsId: [],
|
appsId: [],
|
||||||
apps: {},
|
apps: {},
|
||||||
notifications: {},
|
notifications: {},
|
||||||
tabsCount: {},
|
tabsCount: {},
|
||||||
showNotificationsTray: false,
|
showNotificationsTray: false,
|
||||||
pagination: {
|
pagination: {},
|
||||||
count: 10,
|
|
||||||
numPages: 1,
|
|
||||||
currentPage: 1,
|
|
||||||
nextPage: null,
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
const slice = createSlice({
|
const slice = createSlice({
|
||||||
name: 'notifications',
|
name: 'notifications',
|
||||||
@@ -35,21 +30,19 @@ const slice = createSlice({
|
|||||||
state.notificationStatus = RequestStatus.FAILED;
|
state.notificationStatus = RequestStatus.FAILED;
|
||||||
},
|
},
|
||||||
fetchNotificationRequest: (state) => {
|
fetchNotificationRequest: (state) => {
|
||||||
state.notificationStatus = RequestStatus.LOADING;
|
state.notificationStatus = RequestStatus.IN_PROGRESS;
|
||||||
},
|
},
|
||||||
fetchNotificationSuccess: (state, { payload }) => {
|
fetchNotificationSuccess: (state, { payload }) => {
|
||||||
const {
|
const {
|
||||||
newNotificationIds, notificationsKeyValuePair, numPages, currentPage,
|
newNotificationIds, notificationsKeyValuePair, pagination,
|
||||||
} = payload;
|
} = payload;
|
||||||
const existingNotificationIds = state.apps[state.appName];
|
const existingNotificationIds = state.apps[state.appName];
|
||||||
|
|
||||||
state.apps[state.appName] = Array.from(new Set([...existingNotificationIds, ...newNotificationIds]));
|
state.apps[state.appName] = Array.from(new Set([...existingNotificationIds, ...newNotificationIds]));
|
||||||
state.notifications = { ...state.notifications, ...notificationsKeyValuePair };
|
state.notifications = { ...state.notifications, ...notificationsKeyValuePair };
|
||||||
state.tabsCount.count -= state.tabsCount[state.appName];
|
state.tabsCount.count -= state.tabsCount[state.appName];
|
||||||
state.tabsCount[state.appName] = 0;
|
state.tabsCount[state.appName] = 0;
|
||||||
state.notificationStatus = RequestStatus.LOADED;
|
state.notificationStatus = RequestStatus.SUCCESSFUL;
|
||||||
state.pagination.numPages = numPages;
|
state.pagination = pagination;
|
||||||
state.pagination.currentPage = currentPage;
|
|
||||||
},
|
},
|
||||||
fetchNotificationsCountDenied: (state) => {
|
fetchNotificationsCountDenied: (state) => {
|
||||||
state.notificationStatus = RequestStatus.DENIED;
|
state.notificationStatus = RequestStatus.DENIED;
|
||||||
@@ -58,7 +51,7 @@ const slice = createSlice({
|
|||||||
state.notificationStatus = RequestStatus.FAILED;
|
state.notificationStatus = RequestStatus.FAILED;
|
||||||
},
|
},
|
||||||
fetchNotificationsCountRequest: (state) => {
|
fetchNotificationsCountRequest: (state) => {
|
||||||
state.notificationStatus = RequestStatus.LOADING;
|
state.notificationStatus = RequestStatus.IN_PROGRESS;
|
||||||
},
|
},
|
||||||
fetchNotificationsCountSuccess: (state, { payload }) => {
|
fetchNotificationsCountSuccess: (state, { payload }) => {
|
||||||
const {
|
const {
|
||||||
@@ -68,13 +61,13 @@ const slice = createSlice({
|
|||||||
state.appsId = appIds;
|
state.appsId = appIds;
|
||||||
state.apps = apps;
|
state.apps = apps;
|
||||||
state.showNotificationsTray = showNotificationsTray;
|
state.showNotificationsTray = showNotificationsTray;
|
||||||
state.notificationStatus = RequestStatus.LOADED;
|
state.notificationStatus = RequestStatus.SUCCESSFUL;
|
||||||
},
|
},
|
||||||
markNotificationsAsSeenRequest: (state) => {
|
markNotificationsAsSeenRequest: (state) => {
|
||||||
state.notificationStatus = RequestStatus.LOADING;
|
state.notificationStatus = RequestStatus.IN_PROGRESS;
|
||||||
},
|
},
|
||||||
markNotificationsAsSeenSuccess: (state) => {
|
markNotificationsAsSeenSuccess: (state) => {
|
||||||
state.notificationStatus = RequestStatus.LOADED;
|
state.notificationStatus = RequestStatus.SUCCESSFUL;
|
||||||
},
|
},
|
||||||
markNotificationsAsSeenDenied: (state) => {
|
markNotificationsAsSeenDenied: (state) => {
|
||||||
state.notificationStatus = RequestStatus.DENIED;
|
state.notificationStatus = RequestStatus.DENIED;
|
||||||
@@ -83,7 +76,7 @@ const slice = createSlice({
|
|||||||
state.notificationStatus = RequestStatus.FAILED;
|
state.notificationStatus = RequestStatus.FAILED;
|
||||||
},
|
},
|
||||||
markAllNotificationsAsReadRequest: (state) => {
|
markAllNotificationsAsReadRequest: (state) => {
|
||||||
state.notificationStatus = RequestStatus.LOADING;
|
state.notificationStatus = RequestStatus.IN_PROGRESS;
|
||||||
},
|
},
|
||||||
markAllNotificationsAsReadSuccess: (state) => {
|
markAllNotificationsAsReadSuccess: (state) => {
|
||||||
const updatedNotifications = Object.fromEntries(
|
const updatedNotifications = Object.fromEntries(
|
||||||
@@ -92,7 +85,7 @@ const slice = createSlice({
|
|||||||
]),
|
]),
|
||||||
);
|
);
|
||||||
state.notifications = updatedNotifications;
|
state.notifications = updatedNotifications;
|
||||||
state.notificationStatus = RequestStatus.LOADED;
|
state.notificationStatus = RequestStatus.SUCCESSFUL;
|
||||||
},
|
},
|
||||||
markAllNotificationsAsReadDenied: (state) => {
|
markAllNotificationsAsReadDenied: (state) => {
|
||||||
state.notificationStatus = RequestStatus.DENIED;
|
state.notificationStatus = RequestStatus.DENIED;
|
||||||
@@ -101,12 +94,12 @@ const slice = createSlice({
|
|||||||
state.notificationStatus = RequestStatus.FAILED;
|
state.notificationStatus = RequestStatus.FAILED;
|
||||||
},
|
},
|
||||||
markNotificationsAsReadRequest: (state) => {
|
markNotificationsAsReadRequest: (state) => {
|
||||||
state.notificationStatus = RequestStatus.LOADING;
|
state.notificationStatus = RequestStatus.IN_PROGRESS;
|
||||||
},
|
},
|
||||||
markNotificationsAsReadSuccess: (state, { payload }) => {
|
markNotificationsAsReadSuccess: (state, { payload }) => {
|
||||||
const date = new Date().toISOString();
|
const date = new Date().toISOString();
|
||||||
state.notifications[payload.id] = { ...state.notifications[payload.id], lastRead: date };
|
state.notifications[payload.id] = { ...state.notifications[payload.id], lastRead: date };
|
||||||
state.notificationStatus = RequestStatus.LOADED;
|
state.notificationStatus = RequestStatus.SUCCESSFUL;
|
||||||
},
|
},
|
||||||
markNotificationsAsReadDenied: (state) => {
|
markNotificationsAsReadDenied: (state) => {
|
||||||
state.notificationStatus = RequestStatus.DENIED;
|
state.notificationStatus = RequestStatus.DENIED;
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ import {
|
|||||||
markNotificationsAsReadFailure,
|
markNotificationsAsReadFailure,
|
||||||
} from './slice';
|
} from './slice';
|
||||||
import {
|
import {
|
||||||
getNotifications, getNotificationCounts, markNotificationSeen, markAllNotificationRead, markNotificationRead,
|
getNotificationsList, getNotificationCounts, markNotificationSeen, markAllNotificationRead, markNotificationRead,
|
||||||
} from './api';
|
} from './api';
|
||||||
import { getHttpErrorStatus } from '../utils';
|
import { getHttpErrorStatus } from '../utils';
|
||||||
|
|
||||||
@@ -35,21 +35,26 @@ const normalizeNotificationCounts = ({ countByAppName, count, showNotificationsT
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
const normalizeNotifications = ({ notifications }) => {
|
const normalizeNotifications = (data) => {
|
||||||
const newNotificationIds = notifications.map(notification => notification.id.toString());
|
const newNotificationIds = data.results.map(notification => notification.id.toString());
|
||||||
const notificationsKeyValuePair = notifications.reduce((acc, obj) => { acc[obj.id] = obj; return acc; }, {});
|
const notificationsKeyValuePair = data.results.reduce((acc, obj) => { acc[obj.id] = obj; return acc; }, {});
|
||||||
|
const pagination = {
|
||||||
|
numPages: data.numPages,
|
||||||
|
currentPage: data.currentPage,
|
||||||
|
hasMorePages: !!data.next,
|
||||||
|
};
|
||||||
return {
|
return {
|
||||||
newNotificationIds, notificationsKeyValuePair,
|
newNotificationIds, notificationsKeyValuePair, pagination,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
export const fetchNotificationList = ({ appName, page, pageSize }) => (
|
export const fetchNotificationList = ({ appName, page }) => (
|
||||||
async (dispatch) => {
|
async (dispatch) => {
|
||||||
try {
|
try {
|
||||||
dispatch(fetchNotificationRequest({ appName }));
|
dispatch(fetchNotificationRequest({ appName }));
|
||||||
const data = await getNotifications(appName, page, pageSize);
|
const data = await getNotificationsList(appName, page);
|
||||||
const normalisedData = normalizeNotifications((camelCaseObject(data)));
|
const normalisedData = normalizeNotifications((camelCaseObject(data)));
|
||||||
dispatch(fetchNotificationSuccess({ ...normalisedData, numPages: data.numPages, currentPage: data.currentPage }));
|
dispatch(fetchNotificationSuccess({ ...normalisedData }));
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (getHttpErrorStatus(error) === 403) {
|
if (getHttpErrorStatus(error) === 403) {
|
||||||
dispatch(fetchNotificationDenied(appName));
|
dispatch(fetchNotificationDenied(appName));
|
||||||
|
|||||||
@@ -54,7 +54,7 @@ const Notifications = () => {
|
|||||||
overlay={(
|
overlay={(
|
||||||
<Popover
|
<Popover
|
||||||
id="notificationTray"
|
id="notificationTray"
|
||||||
data-testid="notificationTray"
|
data-testid="notification-tray"
|
||||||
className={classNames('overflow-auto rounded-0 border-0', {
|
className={classNames('overflow-auto rounded-0 border-0', {
|
||||||
'w-100': !isOnMediumScreen && !isOnLargeScreen,
|
'w-100': !isOnMediumScreen && !isOnLargeScreen,
|
||||||
'medium-screen': isOnMediumScreen,
|
'medium-screen': isOnMediumScreen,
|
||||||
@@ -64,7 +64,7 @@ const Notifications = () => {
|
|||||||
<div ref={popoverRef}>
|
<div ref={popoverRef}>
|
||||||
<Popover.Title as="h2" className="d-flex justify-content-between p-0 m-4 border-0 text-primary-500 font-size-18 line-height-24">
|
<Popover.Title as="h2" className="d-flex justify-content-between p-0 m-4 border-0 text-primary-500 font-size-18 line-height-24">
|
||||||
{intl.formatMessage(messages.notificationTitle)}
|
{intl.formatMessage(messages.notificationTitle)}
|
||||||
<Icon src={Settings} className="icon-size-20" />
|
<Icon src={Settings} className="icon-size-20" data-testid="setting-icon" />
|
||||||
</Popover.Title>
|
</Popover.Title>
|
||||||
<Popover.Content className="notification-content p-0">
|
<Popover.Content className="notification-content p-0">
|
||||||
<NotificationTabs />
|
<NotificationTabs />
|
||||||
@@ -83,12 +83,14 @@ const Notifications = () => {
|
|||||||
variant="light"
|
variant="light"
|
||||||
iconClassNames="text-primary-500"
|
iconClassNames="text-primary-500"
|
||||||
className="ml-4 mr-1 my-3 notification-button"
|
className="ml-4 mr-1 my-3 notification-button"
|
||||||
|
data-testid="notification-bell-icon"
|
||||||
/>
|
/>
|
||||||
{notificationCounts?.count > 0 && (
|
{notificationCounts?.count > 0 && (
|
||||||
<Badge
|
<Badge
|
||||||
pill
|
pill
|
||||||
variant="danger"
|
variant="danger"
|
||||||
className="font-weight-normal px-1 notification-badge"
|
className="font-weight-normal px-1 notification-badge"
|
||||||
|
data-testid="notification-count"
|
||||||
>
|
>
|
||||||
{notificationCounts.count}
|
{notificationCounts.count}
|
||||||
</Badge>
|
</Badge>
|
||||||
|
|||||||
109
src/Notifications/index.test.jsx
Normal file
109
src/Notifications/index.test.jsx
Normal file
@@ -0,0 +1,109 @@
|
|||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
import {
|
||||||
|
act, fireEvent, render, screen, waitFor,
|
||||||
|
} from '@testing-library/react';
|
||||||
|
import MockAdapter from 'axios-mock-adapter';
|
||||||
|
import { Context as ResponsiveContext } from 'react-responsive';
|
||||||
|
import { Factory } from 'rosie';
|
||||||
|
|
||||||
|
import { initializeMockApp } from '@edx/frontend-platform';
|
||||||
|
import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth';
|
||||||
|
import { IntlProvider } from '@edx/frontend-platform/i18n';
|
||||||
|
import { AppContext, AppProvider } from '@edx/frontend-platform/react';
|
||||||
|
|
||||||
|
import AuthenticatedUserDropdown from '../learning-header/AuthenticatedUserDropdown';
|
||||||
|
import { initializeStore } from '../store';
|
||||||
|
import executeThunk from '../test-utils';
|
||||||
|
import { getNotificationsCountApiUrl } from './data/api';
|
||||||
|
import { fetchAppsNotificationCount } from './data/thunks';
|
||||||
|
|
||||||
|
import './data/__factories__';
|
||||||
|
|
||||||
|
const notificationCountsApiUrl = getNotificationsCountApiUrl();
|
||||||
|
|
||||||
|
let axiosMock;
|
||||||
|
let store;
|
||||||
|
|
||||||
|
function renderComponent() {
|
||||||
|
render(
|
||||||
|
<ResponsiveContext.Provider>
|
||||||
|
<IntlProvider locale="en" messages={{}}>
|
||||||
|
<AppProvider store={store}>
|
||||||
|
<AppContext.Provider>
|
||||||
|
<AuthenticatedUserDropdown />
|
||||||
|
</AppContext.Provider>
|
||||||
|
</AppProvider>
|
||||||
|
</IntlProvider>
|
||||||
|
</ResponsiveContext.Provider>,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('Notification test cases.', () => {
|
||||||
|
beforeEach(async () => {
|
||||||
|
initializeMockApp({
|
||||||
|
authenticatedUser: {
|
||||||
|
userId: '123abc',
|
||||||
|
username: 'testuser',
|
||||||
|
administrator: false,
|
||||||
|
roles: [],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
axiosMock = new MockAdapter(getAuthenticatedHttpClient());
|
||||||
|
Factory.resetAll();
|
||||||
|
store = initializeStore();
|
||||||
|
});
|
||||||
|
|
||||||
|
async function setupMockNotificationCountResponse(count = 45, showNotificationsTray = true) {
|
||||||
|
axiosMock.onGet(notificationCountsApiUrl)
|
||||||
|
.reply(200, (Factory.build('notificationsCount', { count, showNotificationsTray })));
|
||||||
|
await executeThunk(fetchAppsNotificationCount(), store.dispatch, store.getState);
|
||||||
|
}
|
||||||
|
|
||||||
|
it('Successfully showed bell icon and unseen count on it if unseen count is greater then 0.', async () => {
|
||||||
|
await setupMockNotificationCountResponse();
|
||||||
|
renderComponent();
|
||||||
|
|
||||||
|
const bellIcon = screen.queryByTestId('notification-bell-icon');
|
||||||
|
const notificationCount = screen.queryByTestId('notification-count');
|
||||||
|
|
||||||
|
expect(bellIcon).toBeInTheDocument();
|
||||||
|
expect(notificationCount).toBeInTheDocument();
|
||||||
|
expect(screen.queryByText(45)).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Successfully showed bell icon and hide unseen count tag when unseen count is zero.', async () => {
|
||||||
|
await setupMockNotificationCountResponse(0);
|
||||||
|
renderComponent();
|
||||||
|
|
||||||
|
const bellIcon = screen.queryByTestId('notification-bell-icon');
|
||||||
|
const notificationCount = screen.queryByTestId('notification-count');
|
||||||
|
|
||||||
|
expect(bellIcon).toBeInTheDocument();
|
||||||
|
expect(notificationCount).not.toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Successfully hides bell icon when showNotificationsTray is false.', async () => {
|
||||||
|
await setupMockNotificationCountResponse(45, false);
|
||||||
|
renderComponent();
|
||||||
|
|
||||||
|
const bellIcon = screen.queryByTestId('notification-bell-icon');
|
||||||
|
|
||||||
|
expect(bellIcon).not.toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Successfully viewed setting icon and show/hide notification tray by clicking on the bell icon .', async () => {
|
||||||
|
await setupMockNotificationCountResponse();
|
||||||
|
renderComponent();
|
||||||
|
|
||||||
|
const bellIcon = screen.queryByTestId('notification-bell-icon');
|
||||||
|
|
||||||
|
await act(async () => { fireEvent.click(bellIcon); });
|
||||||
|
expect(screen.queryByTestId('notification-tray')).toBeInTheDocument();
|
||||||
|
expect(screen.queryByTestId('setting-icon')).toBeInTheDocument();
|
||||||
|
|
||||||
|
await act(async () => { fireEvent.click(bellIcon); });
|
||||||
|
await waitFor(() => expect(screen.queryByTestId('notification-tray')).not.toBeInTheDocument());
|
||||||
|
});
|
||||||
|
});
|
||||||
87
src/Notifications/notificationRowItem.test.jsx
Normal file
87
src/Notifications/notificationRowItem.test.jsx
Normal file
@@ -0,0 +1,87 @@
|
|||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
import {
|
||||||
|
act, fireEvent, render, screen,
|
||||||
|
} from '@testing-library/react';
|
||||||
|
import MockAdapter from 'axios-mock-adapter';
|
||||||
|
import { Context as ResponsiveContext } from 'react-responsive';
|
||||||
|
import { Factory } from 'rosie';
|
||||||
|
|
||||||
|
import { initializeMockApp } from '@edx/frontend-platform';
|
||||||
|
import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth';
|
||||||
|
import { IntlProvider } from '@edx/frontend-platform/i18n';
|
||||||
|
import { AppContext, AppProvider } from '@edx/frontend-platform/react';
|
||||||
|
|
||||||
|
import AuthenticatedUserDropdown from '../learning-header/AuthenticatedUserDropdown';
|
||||||
|
import { initializeStore } from '../store';
|
||||||
|
import { markNotificationAsReadApiUrl } from './data/api';
|
||||||
|
import mockNotificationsResponse from './test-utils';
|
||||||
|
|
||||||
|
import './data/__factories__';
|
||||||
|
|
||||||
|
const markedNotificationAsReadApiUrl = markNotificationAsReadApiUrl();
|
||||||
|
|
||||||
|
let axiosMock;
|
||||||
|
let store;
|
||||||
|
|
||||||
|
function renderComponent() {
|
||||||
|
render(
|
||||||
|
<ResponsiveContext.Provider>
|
||||||
|
<IntlProvider locale="en" messages={{}}>
|
||||||
|
<AppProvider store={store}>
|
||||||
|
<AppContext.Provider>
|
||||||
|
<AuthenticatedUserDropdown />
|
||||||
|
</AppContext.Provider>
|
||||||
|
</AppProvider>
|
||||||
|
</IntlProvider>
|
||||||
|
</ResponsiveContext.Provider>,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('Notification row item test cases.', () => {
|
||||||
|
beforeEach(async () => {
|
||||||
|
initializeMockApp({
|
||||||
|
authenticatedUser: {
|
||||||
|
userId: 3,
|
||||||
|
username: 'abc123',
|
||||||
|
administrator: true,
|
||||||
|
roles: [],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
axiosMock = new MockAdapter(getAuthenticatedHttpClient());
|
||||||
|
Factory.resetAll();
|
||||||
|
store = initializeStore();
|
||||||
|
|
||||||
|
({ store, axiosMock } = await mockNotificationsResponse());
|
||||||
|
});
|
||||||
|
|
||||||
|
it(
|
||||||
|
'Successfully viewed notification icon, notification context, unread , course name and notification time.',
|
||||||
|
async () => {
|
||||||
|
renderComponent();
|
||||||
|
|
||||||
|
const bellIcon = screen.queryByTestId('notification-bell-icon');
|
||||||
|
await act(async () => { fireEvent.click(bellIcon); });
|
||||||
|
|
||||||
|
expect(screen.queryByTestId('notification-icon-1')).toBeInTheDocument();
|
||||||
|
expect(screen.queryByTestId('notification-content-1')).toBeInTheDocument();
|
||||||
|
expect(screen.queryByTestId('notification-course-1')).toBeInTheDocument();
|
||||||
|
expect(screen.queryByTestId('notification-created-date-1')).toBeInTheDocument();
|
||||||
|
expect(screen.queryByTestId('unread-notification-1')).toBeInTheDocument();
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
it('Successfully marked notification as read.', async () => {
|
||||||
|
axiosMock.onPatch(markedNotificationAsReadApiUrl).reply(200, { message: 'Notification marked read.' });
|
||||||
|
renderComponent();
|
||||||
|
|
||||||
|
const bellIcon = screen.queryByTestId('notification-bell-icon');
|
||||||
|
await act(async () => { fireEvent.click(bellIcon); });
|
||||||
|
|
||||||
|
const notification = screen.queryByTestId('notification-1');
|
||||||
|
await act(async () => { fireEvent.click(notification); });
|
||||||
|
|
||||||
|
expect(screen.queryByTestId('unread-notification-1')).not.toBeInTheDocument();
|
||||||
|
});
|
||||||
|
});
|
||||||
106
src/Notifications/notificationSections.test.jsx
Normal file
106
src/Notifications/notificationSections.test.jsx
Normal file
@@ -0,0 +1,106 @@
|
|||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
import {
|
||||||
|
act, fireEvent, render, screen, within,
|
||||||
|
} from '@testing-library/react';
|
||||||
|
import MockAdapter from 'axios-mock-adapter';
|
||||||
|
import { Context as ResponsiveContext } from 'react-responsive';
|
||||||
|
import { Factory } from 'rosie';
|
||||||
|
|
||||||
|
import { initializeMockApp } from '@edx/frontend-platform';
|
||||||
|
import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth';
|
||||||
|
import { IntlProvider } from '@edx/frontend-platform/i18n';
|
||||||
|
import { AppContext, AppProvider } from '@edx/frontend-platform/react';
|
||||||
|
|
||||||
|
import AuthenticatedUserDropdown from '../learning-header/AuthenticatedUserDropdown';
|
||||||
|
import { initializeStore } from '../store';
|
||||||
|
import { markNotificationAsReadApiUrl, markNotificationsSeenApiUrl, getNotificationsListApiUrl } from './data/api';
|
||||||
|
import mockNotificationsResponse from './test-utils';
|
||||||
|
import { markNotificationsAsSeen, fetchNotificationList } from './data/thunks';
|
||||||
|
import executeThunk from '../test-utils';
|
||||||
|
import './data/__factories__';
|
||||||
|
|
||||||
|
const markedAllNotificationsAsReadApiUrl = markNotificationAsReadApiUrl();
|
||||||
|
|
||||||
|
let axiosMock;
|
||||||
|
let store;
|
||||||
|
|
||||||
|
function renderComponent() {
|
||||||
|
render(
|
||||||
|
<ResponsiveContext.Provider>
|
||||||
|
<IntlProvider locale="en" messages={{}}>
|
||||||
|
<AppProvider store={store}>
|
||||||
|
<AppContext.Provider>
|
||||||
|
<AuthenticatedUserDropdown />
|
||||||
|
</AppContext.Provider>
|
||||||
|
</AppProvider>
|
||||||
|
</IntlProvider>
|
||||||
|
</ResponsiveContext.Provider>,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('Notification sections test cases.', () => {
|
||||||
|
beforeEach(async () => {
|
||||||
|
initializeMockApp({
|
||||||
|
authenticatedUser: {
|
||||||
|
userId: 3,
|
||||||
|
username: 'abc123',
|
||||||
|
administrator: true,
|
||||||
|
roles: [],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
axiosMock = new MockAdapter(getAuthenticatedHttpClient());
|
||||||
|
Factory.resetAll();
|
||||||
|
store = initializeStore();
|
||||||
|
|
||||||
|
({ store, axiosMock } = await mockNotificationsResponse());
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Successfully viewed last 24 hours and earlier section along with mark all as read label.', async () => {
|
||||||
|
renderComponent();
|
||||||
|
|
||||||
|
const bellIcon = screen.queryByTestId('notification-bell-icon');
|
||||||
|
await act(async () => { fireEvent.click(bellIcon); });
|
||||||
|
const notificationTraySection = screen.queryByTestId('notification-tray-section');
|
||||||
|
|
||||||
|
expect(within(notificationTraySection).queryByText('Last 24 hours')).toBeInTheDocument();
|
||||||
|
expect(within(notificationTraySection).queryByText('Earlier')).toBeInTheDocument();
|
||||||
|
expect(within(notificationTraySection).queryByText('Mark all as read')).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Successfully marked all notifications as read, removing the unread status.', async () => {
|
||||||
|
axiosMock.onPatch(markedAllNotificationsAsReadApiUrl).reply(200, { message: 'Notifications marked read.' });
|
||||||
|
renderComponent();
|
||||||
|
|
||||||
|
const bellIcon = screen.queryByTestId('notification-bell-icon');
|
||||||
|
await act(async () => { fireEvent.click(bellIcon); });
|
||||||
|
const markAllReadButton = screen.queryByTestId('mark-all-read');
|
||||||
|
|
||||||
|
expect(screen.queryByTestId('unread-notification-1')).toBeInTheDocument();
|
||||||
|
await act(async () => { fireEvent.click(markAllReadButton); });
|
||||||
|
|
||||||
|
expect(screen.queryByTestId('unread-notification-1')).not.toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Successfully load more notifications by clicking on load more notification button.', async () => {
|
||||||
|
axiosMock.onPut(markNotificationsSeenApiUrl('discussion')).reply(200);
|
||||||
|
await executeThunk(markNotificationsAsSeen('discussions'), store.dispatch, store.getState);
|
||||||
|
renderComponent();
|
||||||
|
|
||||||
|
const bellIcon = screen.queryByTestId('notification-bell-icon');
|
||||||
|
await act(async () => { fireEvent.click(bellIcon); });
|
||||||
|
|
||||||
|
expect(screen.queryAllByTestId('notification-contents')).toHaveLength(10);
|
||||||
|
const loadMoreButton = screen.queryByTestId('load-more-notifications');
|
||||||
|
|
||||||
|
axiosMock.onGet(getNotificationsListApiUrl()).reply(
|
||||||
|
200,
|
||||||
|
(Factory.build('notificationsList', { num_pages: 2, current_page: 2 })),
|
||||||
|
);
|
||||||
|
await executeThunk(fetchNotificationList({ appName: 'discussion', page: 2 }), store.dispatch, store.getState);
|
||||||
|
|
||||||
|
await act(async () => { fireEvent.click(loadMoreButton); });
|
||||||
|
expect(screen.queryAllByTestId('notification-contents')).toHaveLength(12);
|
||||||
|
});
|
||||||
|
});
|
||||||
91
src/Notifications/notificationTabs.test.jsx
Normal file
91
src/Notifications/notificationTabs.test.jsx
Normal file
@@ -0,0 +1,91 @@
|
|||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
import {
|
||||||
|
act, fireEvent, render, screen, within,
|
||||||
|
} from '@testing-library/react';
|
||||||
|
import { Context as ResponsiveContext } from 'react-responsive';
|
||||||
|
import { Factory } from 'rosie';
|
||||||
|
|
||||||
|
import { initializeMockApp } from '@edx/frontend-platform';
|
||||||
|
import { IntlProvider } from '@edx/frontend-platform/i18n';
|
||||||
|
import { AppContext, AppProvider } from '@edx/frontend-platform/react';
|
||||||
|
|
||||||
|
import AuthenticatedUserDropdown from '../learning-header/AuthenticatedUserDropdown';
|
||||||
|
import { initializeStore } from '../store';
|
||||||
|
import mockNotificationsResponse from './test-utils';
|
||||||
|
|
||||||
|
import './data/__factories__';
|
||||||
|
|
||||||
|
let store;
|
||||||
|
|
||||||
|
function renderComponent() {
|
||||||
|
render(
|
||||||
|
<ResponsiveContext.Provider>
|
||||||
|
<IntlProvider locale="en" messages={{}}>
|
||||||
|
<AppProvider store={store}>
|
||||||
|
<AppContext.Provider>
|
||||||
|
<AuthenticatedUserDropdown />
|
||||||
|
</AppContext.Provider>
|
||||||
|
</AppProvider>
|
||||||
|
</IntlProvider>
|
||||||
|
</ResponsiveContext.Provider>,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('Notification Tabs test cases.', () => {
|
||||||
|
beforeEach(async () => {
|
||||||
|
initializeMockApp({
|
||||||
|
authenticatedUser: {
|
||||||
|
userId: '123abc',
|
||||||
|
username: 'testuser',
|
||||||
|
administrator: false,
|
||||||
|
roles: [],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
Factory.resetAll();
|
||||||
|
store = initializeStore();
|
||||||
|
|
||||||
|
({ store } = await mockNotificationsResponse());
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Notification tabs displayed with default discussion tab selected and no unseen counts.', async () => {
|
||||||
|
renderComponent();
|
||||||
|
|
||||||
|
const bellIcon = screen.queryByTestId('notification-bell-icon');
|
||||||
|
await act(async () => { fireEvent.click(bellIcon); });
|
||||||
|
|
||||||
|
const tabs = screen.queryAllByRole('tab');
|
||||||
|
const selectedTab = tabs.find(tab => tab.getAttribute('aria-selected') === 'true');
|
||||||
|
|
||||||
|
expect(tabs.length).toEqual(5);
|
||||||
|
expect(within(selectedTab).queryByText('discussion')).toBeInTheDocument();
|
||||||
|
expect(within(selectedTab).queryByRole('status')).not.toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Successfully showed unseen counts for unselected tabs.', async () => {
|
||||||
|
renderComponent();
|
||||||
|
const bellIcon = screen.queryByTestId('notification-bell-icon');
|
||||||
|
await act(async () => { fireEvent.click(bellIcon); });
|
||||||
|
|
||||||
|
const tabs = screen.getAllByRole('tab');
|
||||||
|
|
||||||
|
expect(within(tabs[0]).queryByRole('status')).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Successfully selected reminder tab.', async () => {
|
||||||
|
renderComponent();
|
||||||
|
|
||||||
|
const bellIcon = screen.queryByTestId('notification-bell-icon');
|
||||||
|
await act(async () => { fireEvent.click(bellIcon); });
|
||||||
|
const notificationTab = screen.getAllByRole('tab');
|
||||||
|
|
||||||
|
await act(async () => { fireEvent.click(notificationTab[0], { dataset: { rbEventKey: 'reminders' } }); });
|
||||||
|
|
||||||
|
const tabs = screen.queryAllByRole('tab');
|
||||||
|
const selectedTab = tabs.find(tab => tab.getAttribute('aria-selected') === 'true');
|
||||||
|
|
||||||
|
expect(within(selectedTab).queryByText('reminders')).toBeInTheDocument();
|
||||||
|
expect(within(selectedTab).queryByRole('status')).not.toBeInTheDocument();
|
||||||
|
});
|
||||||
|
});
|
||||||
32
src/Notifications/test-utils.js
Normal file
32
src/Notifications/test-utils.js
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
import MockAdapter from 'axios-mock-adapter';
|
||||||
|
import { Factory } from 'rosie';
|
||||||
|
|
||||||
|
import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth';
|
||||||
|
|
||||||
|
import { initializeStore } from '../store';
|
||||||
|
import executeThunk from '../test-utils';
|
||||||
|
import { getNotificationsListApiUrl, getNotificationsCountApiUrl } from './data/api';
|
||||||
|
import { fetchAppsNotificationCount, fetchNotificationList } from './data/thunks';
|
||||||
|
|
||||||
|
import './data/__factories__';
|
||||||
|
|
||||||
|
const notificationCountsApiUrl = getNotificationsCountApiUrl();
|
||||||
|
const notificationsApiUrl = getNotificationsListApiUrl();
|
||||||
|
|
||||||
|
export default async function mockNotificationsResponse() {
|
||||||
|
const store = initializeStore();
|
||||||
|
const axiosMock = new MockAdapter(getAuthenticatedHttpClient());
|
||||||
|
const notifications = (Factory.buildList('notification', 8, null, { createdDate: new Date().toISOString() }).concat(
|
||||||
|
Factory.buildList('notification', 2, null, { createdDate: '2023-06-01T00:46:11.979531Z' }),
|
||||||
|
));
|
||||||
|
axiosMock.onGet(notificationCountsApiUrl).reply(200, (Factory.build('notificationsCount')));
|
||||||
|
axiosMock.onGet(notificationsApiUrl).reply(200, (Factory.build('notificationsList', {
|
||||||
|
results: notifications,
|
||||||
|
num_pages: 2,
|
||||||
|
next: `${notificationsApiUrl}?app_name=discussion&page=2`,
|
||||||
|
})));
|
||||||
|
|
||||||
|
await executeThunk(fetchAppsNotificationCount(), store.dispatch, store.getState);
|
||||||
|
await executeThunk(fetchNotificationList({ appName: 'discussion', page: 1 }), store.dispatch, store.getState);
|
||||||
|
return { store, axiosMock };
|
||||||
|
}
|
||||||
@@ -30,10 +30,10 @@
|
|||||||
"header.menu.orderHistory.label": "Historial de órdenes",
|
"header.menu.orderHistory.label": "Historial de órdenes",
|
||||||
"header.navigation.skipNavLink": "Dirígete al contenido principal.",
|
"header.navigation.skipNavLink": "Dirígete al contenido principal.",
|
||||||
"header.menu.signOut.label": "Cerrar sesión",
|
"header.menu.signOut.label": "Cerrar sesión",
|
||||||
"notification.title": "Notifications",
|
"notification.title": "Notificaciones",
|
||||||
"notification.today.heading": "Last 24 hours",
|
"notification.today.heading": "Últimas 24 horas",
|
||||||
"notification.earlier.heading": "Earlier",
|
"notification.earlier.heading": "Más temprano",
|
||||||
"notification.mark.as.read": "Mark all as read",
|
"notification.mark.as.read": "Marcar todo como leído",
|
||||||
"notification.fullStop": "•",
|
"notification.fullStop": "•",
|
||||||
"notification.load.more.notifications": "Load more notifications"
|
"notification.load.more.notifications": "Cargar más notificaciones"
|
||||||
}
|
}
|
||||||
@@ -123,7 +123,7 @@ $white: #fff;
|
|||||||
}
|
}
|
||||||
|
|
||||||
.content {
|
.content {
|
||||||
b {
|
strong {
|
||||||
color: #00262B !important;
|
color: #00262B !important;
|
||||||
font-weight: 500 !important;
|
font-weight: 500 !important;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ const AuthenticatedUserDropdown = ({ intl, username }) => {
|
|||||||
dispatch(fetchAppsNotificationCount());
|
dispatch(fetchAppsNotificationCount());
|
||||||
}
|
}
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, [notificationStatus]);
|
}, []);
|
||||||
|
|
||||||
const dashboardMenuItem = (
|
const dashboardMenuItem = (
|
||||||
<Dropdown.Item href={`${getConfig().LMS_BASE_URL}/dashboard`}>
|
<Dropdown.Item href={`${getConfig().LMS_BASE_URL}/dashboard`}>
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
import Enzyme from 'enzyme';
|
import Enzyme from 'enzyme';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import Adapter from 'enzyme-adapter-react-16';
|
import Adapter from '@wojtekmaj/enzyme-adapter-react-17';
|
||||||
import '@testing-library/jest-dom';
|
import '@testing-library/jest-dom';
|
||||||
import '@testing-library/jest-dom/extend-expect';
|
import '@testing-library/jest-dom/extend-expect';
|
||||||
import 'babel-polyfill';
|
import 'babel-polyfill';
|
||||||
|
|||||||
Reference in New Issue
Block a user