diff --git a/src/Notifications/data/__factories__/index.js b/src/Notifications/data/__factories__/index.js new file mode 100644 index 0000000..cdf7f0b --- /dev/null +++ b/src/Notifications/data/__factories__/index.js @@ -0,0 +1 @@ +import './notifications.factory'; diff --git a/src/Notifications/data/__factories__/notifications.factory.js b/src/Notifications/data/__factories__/notifications.factory.js new file mode 100644 index 0000000..f919167 --- /dev/null +++ b/src/Notifications/data/__factories__/notifications.factory.js @@ -0,0 +1,22 @@ +import { Factory } from 'rosie'; + +Factory.define('notificationsCount') + .attr('count', 45) + .attr('countByAppName', { + reminders: 10, + discussions: 20, + grades: 10, + authoring: 5, + }) + .attr('showNotificationTray', true); + +Factory.define('notification') + .sequence('id') + .attr('type', 'post') + .sequence('content', ['id'], (idx, notificationId) => `
User ${idx} posts Hello and welcome to SC0x + ${notificationId}!
`) + .attr('course_name', 'Supply Chain Analytics') + .sequence('content_url', (idx) => `https://example.com/${idx}`) + .attr('last_read', null) + .attr('last_seen', null) + .sequence('created_at', ['createdDate'], (idx, date) => date); diff --git a/src/Notifications/data/api.test.js b/src/Notifications/data/api.test.js new file mode 100644 index 0000000..58f8963 --- /dev/null +++ b/src/Notifications/data/api.test.js @@ -0,0 +1,167 @@ +import MockAdapter from 'axios-mock-adapter'; +import { Factory } from 'rosie'; + +import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth'; +import { initializeMockApp } from '@edx/frontend-platform/testing'; + +import { initializeStore } from '../../store'; +import executeThunk from '../../test-utils'; +import { + getNotificationsApiUrl, getNotificationsCountApiUrl, markAllNotificationsAsReadpiUrl, markNotificationsSeenApiUrl, + getNotificationCounts, getNotifications, markNotificationSeen, markAllNotificationRead, markNotificationRead, +} from './api'; +import { + fetchAppsNotificationCount, + fetchNotificationList, + markAllNotificationsAsRead, + markNotificationsAsRead, + markNotificationsAsSeen, +} from './thunks'; + +import './__factories__'; + +const notificationCountsApiUrl = getNotificationsCountApiUrl(); +const notificationsApiUrl = getNotificationsApiUrl(); +const markedAllNotificationsAsSeenApiUrl = markNotificationsSeenApiUrl('discussions'); +const markedAllNotificationsAsReadApiUrl = markAllNotificationsAsReadpiUrl('discussions'); +const markedNotificationAsReadApiUrl = markAllNotificationsAsReadpiUrl('discussions', 1); + +let axiosMock = null; +let store; + +describe('Notifications API', () => { + beforeEach(async () => { + initializeMockApp({ + authenticatedUser: { + userId: '123abc', + username: 'testuser', + administrator: false, + roles: [], + }, + }); + axiosMock = new MockAdapter(getAuthenticatedHttpClient()); + Factory.resetAll(); + store = initializeStore(); + }); + + afterEach(() => { + axiosMock.reset(); + }); + + it('successfully get notification counts for different tabs.', async () => { + axiosMock.onGet(notificationCountsApiUrl).reply(200, (Factory.build('notificationsCount'))); + + const { count, countByAppName } = await getNotificationCounts(); + + expect(count).toEqual(45); + expect(countByAppName.reminders).toEqual(10); + expect(countByAppName.discussions).toEqual(20); + expect(countByAppName.grades).toEqual(10); + expect(countByAppName.authoring).toEqual(5); + }); + + it('failed to get notification counts.', async () => { + axiosMock.onGet(notificationCountsApiUrl).reply(404); + await executeThunk(fetchAppsNotificationCount(), store.dispatch, store.getState); + + expect(store.getState().notifications.notificationStatus).toEqual('failed'); + }); + + it('denied to get notification counts.', async () => { + axiosMock.onGet(notificationCountsApiUrl).reply(403, {}); + await executeThunk(fetchAppsNotificationCount(), store.dispatch); + + expect(store.getState().notifications.notificationStatus).toEqual('denied'); + }); + + it('successfully get notifications.', async () => { + axiosMock.onGet(notificationsApiUrl).reply( + 200, + (Factory.buildList('notification', 2, null, { createdDate: new Date().toISOString() })), + ); + + const { notifications } = await getNotifications('discussions', 1, 10); + + expect(notifications).toHaveLength(2); + }); + + it('failed to get notifications.', async () => { + axiosMock.onGet(notificationsApiUrl).reply(404); + await executeThunk(fetchNotificationList({ page: 1, pageSize: 10 }), store.dispatch, store.getState); + + expect(store.getState().notifications.notificationStatus).toEqual('failed'); + }); + + it('denied to get notifications.', async () => { + axiosMock.onGet(notificationsApiUrl).reply(403, {}); + await executeThunk(fetchNotificationList({ page: 1, pageSize: 10 }), store.dispatch); + + expect(store.getState().notifications.notificationStatus).toEqual('denied'); + }); + + it('successfully marked all notifications as seen for selected app.', async () => { + axiosMock.onPut(markedAllNotificationsAsSeenApiUrl).reply(200, { message: 'Notifications marked seen.' }); + + const { message } = await markNotificationSeen('discussions'); + + expect(message).toEqual('Notifications marked seen.'); + }); + + it('failed to mark all notifications as seen for selected app.', async () => { + axiosMock.onPut(markedAllNotificationsAsSeenApiUrl).reply(404); + await executeThunk(markNotificationsAsSeen('discussions'), store.dispatch, store.getState); + + expect(store.getState().notifications.notificationStatus).toEqual('failed'); + }); + + it('denied to mark all notifications as seen for selected app.', async () => { + axiosMock.onPut(markedAllNotificationsAsSeenApiUrl).reply(403, {}); + await executeThunk(markNotificationsAsSeen('discussions'), store.dispatch); + + expect(store.getState().notifications.notificationStatus).toEqual('denied'); + }); + + it('successfully marked all notifications as read for selected app.', async () => { + axiosMock.onPut(markedAllNotificationsAsReadApiUrl).reply(200, { message: 'Notifications marked read.' }); + + const { message } = await markAllNotificationRead('discussions'); + + expect(message).toEqual('Notifications marked read.'); + }); + + it('failed to mark all notifications as read for selected app.', async () => { + axiosMock.onPut(markedAllNotificationsAsReadApiUrl).reply(404); + await executeThunk(markAllNotificationsAsRead('discussions'), store.dispatch, store.getState); + + expect(store.getState().notifications.notificationStatus).toEqual('failed'); + }); + + it('denied to mark all notifications as read for selected app.', async () => { + axiosMock.onPut(markedAllNotificationsAsReadApiUrl).reply(403, {}); + await executeThunk(markAllNotificationsAsRead('discussions'), store.dispatch); + + expect(store.getState().notifications.notificationStatus).toEqual('denied'); + }); + + it('successfully marked notification as read.', async () => { + axiosMock.onPut(markedNotificationAsReadApiUrl).reply(200, { message: 'Notification marked read.' }); + + const { data } = await markNotificationRead('discussions', 1); + + expect(data.message).toEqual('Notification marked read.'); + }); + + it('failed to mark notification as read .', async () => { + axiosMock.onPut(markedNotificationAsReadApiUrl).reply(404); + await executeThunk(markNotificationsAsRead('discussions', 1), store.dispatch, store.getState); + + expect(store.getState().notifications.notificationStatus).toEqual('failed'); + }); + + it('denied to mark notification as read.', async () => { + axiosMock.onPut(markedNotificationAsReadApiUrl).reply(403, {}); + await executeThunk(markNotificationsAsRead('discussions', 1), store.dispatch); + + expect(store.getState().notifications.notificationStatus).toEqual('denied'); + }); +}); diff --git a/src/Notifications/data/redux.test.js b/src/Notifications/data/redux.test.js new file mode 100644 index 0000000..4810458 --- /dev/null +++ b/src/Notifications/data/redux.test.js @@ -0,0 +1,140 @@ +import MockAdapter from 'axios-mock-adapter'; +import { Factory } from 'rosie'; + +import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth'; +import { initializeMockApp } from '@edx/frontend-platform/testing'; + +import { initializeStore } from '../../store'; +import executeThunk from '../../test-utils'; +import { + getNotificationsApiUrl, getNotificationsCountApiUrl, markAllNotificationsAsReadpiUrl, markNotificationsSeenApiUrl, +} from './api'; +import { + fetchAppsNotificationCount, fetchNotificationList, markNotificationsAsRead, markAllNotificationsAsRead, + resetNotificationState, markNotificationsAsSeen, +} from './thunks'; + +import './__factories__'; + +const notificationCountsApiUrl = getNotificationsCountApiUrl(); +const notificationsApiUrl = getNotificationsApiUrl(); +const markedNotificationAsReadApiUrl = markAllNotificationsAsReadpiUrl('discussions', 1); +const markedAllNotificationsAsReadApiUrl = markAllNotificationsAsReadpiUrl('discussions'); +const markedAllNotificationsAsSeenApiUrl = markNotificationsSeenApiUrl('discussions'); + +let axiosMock; +let store; + +describe('Notification Redux', () => { + beforeEach(async () => { + initializeMockApp({ + authenticatedUser: { + userId: '123abc', + username: 'testuser', + administrator: false, + roles: [], + }, + }); + axiosMock = new MockAdapter(getAuthenticatedHttpClient()); + Factory.resetAll(); + store = initializeStore(); + + axiosMock.onGet(notificationCountsApiUrl).reply(200, (Factory.build('notificationsCount'))); + axiosMock.onGet(notificationsApiUrl).reply(200, (Factory.buildList('notification', 2, null))); + await executeThunk(fetchAppsNotificationCount(), store.dispatch, store.getState); + await executeThunk(fetchNotificationList({ page: 1, pageSize: 10 }), store.dispatch, store.getState); + }); + + afterEach(() => { + axiosMock.reset(); + }); + + it('successfully loaded initial notification states in the redux.', async () => { + executeThunk(resetNotificationState(), store.dispatch, store.getState); + + const { notifications } = store.getState(); + + expect(notifications.notificationStatus).toEqual('idle'); + expect(notifications.appName).toEqual('discussions'); + expect(notifications.appsId).toHaveLength(0); + expect(notifications.apps).toEqual({}); + expect(notifications.notifications).toEqual({}); + expect(notifications.tabsCount).toEqual({}); + expect(notifications.showNotificationTray).toEqual(false); + expect(notifications.pagination.count).toEqual(10); + 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 () => { + const state = store.getState(); + + expect(Object.keys(state.notifications.notifications)).toHaveLength(2); + }); + + it('successfully loaded notification counts in the redux.', async () => { + const { notifications: { tabsCount } } = store.getState(); + + expect(tabsCount.count).toEqual(25); + expect(tabsCount.reminders).toEqual(10); + expect(tabsCount.discussions).toEqual(0); + expect(tabsCount.grades).toEqual(10); + expect(tabsCount.authoring).toEqual(5); + }); + + it('successfully loaded showNotificationTray status in the redux based on api.', async () => { + const state = store.getState(); + + expect(state.notifications.showNotificationTray).toEqual(true); + }); + + it('successfully store the count, numPages, currentPage, and nextPage data in redux.', async () => { + const { notifications: { pagination } } = store.getState(); + + expect(pagination.count).toEqual(10); + expect(pagination.currentPage).toEqual(1); + expect(pagination.numPages).toEqual(2); + }); + + it('successfully updated the selected app name in redux.', async () => { + const state = store.getState(); + + expect(state.notifications.appName).toEqual('discussions'); + }); + + it('successfully store notification ids in the selected app in apps.', async () => { + const state = store.getState(); + + expect(state.notifications.apps.discussions).toHaveLength(2); + }); + + it('successfully marked all notifications as seen for selected app.', async () => { + axiosMock.onPut(markedAllNotificationsAsSeenApiUrl).reply(200); + await executeThunk(markNotificationsAsSeen('discussions'), store.dispatch, store.getState); + + expect(store.getState().notifications.notificationStatus).toEqual('successful'); + }); + + it('successfully marked all notifications as read for selected app in the redux.', async () => { + axiosMock.onPut(markedAllNotificationsAsReadApiUrl).reply(200); + await executeThunk(markAllNotificationsAsRead('discussions'), store.dispatch, store.getState); + + const { notifications } = store.getState(); + const firstNotification = Object.values(notifications.notifications)[0]; + + expect(notifications.notificationStatus).toEqual('successful'); + expect(firstNotification.lastRead).not.toBeNull(); + }); + + it('successfully marked notification as read in the redux.', async () => { + axiosMock.onPut(markedNotificationAsReadApiUrl).reply(200); + await executeThunk(markNotificationsAsRead('discussions', 1), store.dispatch, store.getState); + + const { notifications } = store.getState(); + const firstNotification = Object.values(notifications.notifications)[0]; + + expect(notifications.notificationStatus).toEqual('successful'); + expect(firstNotification.lastRead).not.toBeNull(); + }); +}); diff --git a/src/Notifications/data/selector.test.jsx b/src/Notifications/data/selector.test.jsx new file mode 100644 index 0000000..90f51f8 --- /dev/null +++ b/src/Notifications/data/selector.test.jsx @@ -0,0 +1,123 @@ +import MockAdapter from 'axios-mock-adapter'; +import { Factory } from 'rosie'; + +import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth'; +import { initializeMockApp } from '@edx/frontend-platform/testing'; + +import { initializeStore } from '../../store'; +import executeThunk from '../../test-utils'; +import { getNotificationsApiUrl, getNotificationsCountApiUrl } from './api'; +import { + selectNotifications, + selectNotificationsByIds, + selectNotificationStatus, + selectNotificationTabs, + selectNotificationTabsCount, + selectPaginationData, + selectSelectedAppName, + selectSelectedAppNotificationIds, + selectShowNotificationTray, +} from './selectors'; +import { fetchAppsNotificationCount, fetchNotificationList } from './thunks'; + +import './__factories__'; + +const notificationCountsApiUrl = getNotificationsCountApiUrl(); +const notificationsApiUrl = getNotificationsApiUrl(); + +let axiosMock; +let store; + +describe('Notification Selectors', () => { + beforeEach(async () => { + initializeMockApp({ + authenticatedUser: { + userId: '123abc', + username: 'testuser', + administrator: false, + roles: [], + }, + }); + axiosMock = new MockAdapter(getAuthenticatedHttpClient()); + Factory.resetAll(); + store = initializeStore(); + + axiosMock.onGet(notificationCountsApiUrl).reply(200, (Factory.build('notificationsCount'))); + axiosMock.onGet(notificationsApiUrl).reply(200, (Factory.buildList('notification', 2, null))); + await executeThunk(fetchAppsNotificationCount(), store.dispatch, store.getState); + await executeThunk(fetchNotificationList({ page: 1, pageSize: 10 }), store.dispatch, store.getState); + }); + + afterEach(() => { + axiosMock.reset(); + }); + + it('should return notification status.', async () => { + const state = store.getState(); + const status = selectNotificationStatus()(state); + + expect(status).toEqual('successful'); + }); + + it('should return notification tabs count.', async () => { + const state = store.getState(); + const tabsCount = selectNotificationTabsCount()(state); + + expect(tabsCount.count).toEqual(25); + expect(tabsCount.reminders).toEqual(10); + expect(tabsCount.discussions).toEqual(0); + expect(tabsCount.grades).toEqual(10); + expect(tabsCount.authoring).toEqual(5); + }); + + it('should return notification tabs.', async () => { + const state = store.getState(); + const tabs = selectNotificationTabs()(state); + + expect(tabs).toHaveLength(4); + }); + + it('should return selected app notification ids.', async () => { + const state = store.getState(); + const notificationIds = selectSelectedAppNotificationIds('discussions')(state); + + expect(notificationIds).toHaveLength(2); + }); + + it('should return show notification tray status.', async () => { + const state = store.getState(); + const showNotificationTrayStatus = selectShowNotificationTray()(state); + + expect(showNotificationTrayStatus).toEqual(true); + }); + + it('should return notifications.', async () => { + const state = store.getState(); + const notifications = selectNotifications()(state); + + expect(Object.keys(notifications)).toHaveLength(2); + }); + + it('should return notifications from Ids.', async () => { + const state = store.getState(); + const notifications = selectNotificationsByIds('discussions')(state); + + expect(notifications).toHaveLength(2); + }); + + it('should return selected app name.', async () => { + const state = store.getState(); + const appName = selectSelectedAppName()(state); + + expect(appName).toEqual('discussions'); + }); + + it('should return pagination data.', async () => { + const state = store.getState(); + const paginationData = selectPaginationData()(state); + + expect(paginationData.count).toEqual(10); + expect(paginationData.currentPage).toEqual(1); + expect(paginationData.numPages).toEqual(2); + }); +}); diff --git a/src/test-utils.js b/src/test-utils.js new file mode 100644 index 0000000..21b8a55 --- /dev/null +++ b/src/test-utils.js @@ -0,0 +1,6 @@ +const executeThunk = async (thunk, dispatch, getState) => { + await thunk(dispatch, getState); + await new Promise(setImmediate); +}; + +export default executeThunk;