From c3541a3d79a0872a452de88a85cc708ff84884fb Mon Sep 17 00:00:00 2001 From: SundasNoreen Date: Thu, 15 Jun 2023 13:30:38 +0500 Subject: [PATCH 1/5] test: added notification redux test cases --- src/Notifications/data/__factories__/index.js | 1 + .../__factories__/notifications.factory.js | 22 +++ src/Notifications/data/api.test.js | 167 ++++++++++++++++++ src/Notifications/data/redux.test.js | 140 +++++++++++++++ src/Notifications/data/selector.test.jsx | 123 +++++++++++++ src/test-utils.js | 6 + 6 files changed, 459 insertions(+) create mode 100644 src/Notifications/data/__factories__/index.js create mode 100644 src/Notifications/data/__factories__/notifications.factory.js create mode 100644 src/Notifications/data/api.test.js create mode 100644 src/Notifications/data/redux.test.js create mode 100644 src/Notifications/data/selector.test.jsx create mode 100644 src/test-utils.js 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; From 6efa31092d116615aca867022ce04fab0b017e44 Mon Sep 17 00:00:00 2001 From: SundasNoreen Date: Thu, 15 Jun 2023 13:57:23 +0500 Subject: [PATCH 2/5] test: added redux, selector and api cases --- package-lock.json | 57 +++++++++++++++++++----- package.json | 6 ++- src/Notifications/data/api.js | 44 +++++++++--------- src/Notifications/data/redux.test.js | 33 +++++++------- src/Notifications/data/selector.test.jsx | 5 ++- src/Notifications/data/thunks.js | 4 +- 6 files changed, 97 insertions(+), 52 deletions(-) diff --git a/package-lock.json b/package-lock.json index a85b517..930d299 100644 --- a/package-lock.json +++ b/package-lock.json @@ -16,6 +16,7 @@ "@fortawesome/free-solid-svg-icons": "6.3.0", "@fortawesome/react-fontawesome": "^0.2.0", "@reduxjs/toolkit": "1.9.5", + "axios-mock-adapter": "1.21.4", "babel-polyfill": "6.26.0", "classnames": "2.3.2", "lodash": "4.17.21", @@ -23,6 +24,7 @@ "react-responsive": "8.2.0", "react-router-dom": "5.3.4", "react-transition-group": "4.4.5", + "rosie": "2.1.0", "timeago.js": "4.0.2" }, "devDependencies": { @@ -7709,8 +7711,7 @@ "node_modules/asynckit": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", - "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", - "dev": true + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==" }, "node_modules/at-least-node": { "version": "1.0.0", @@ -7799,7 +7800,6 @@ "version": "0.27.2", "resolved": "https://registry.npmjs.org/axios/-/axios-0.27.2.tgz", "integrity": "sha512-t+yRIyySRTp/wua5xEr+z1q60QmLq8ABsS5O9Me1AsE5dfKqgnCFzwiCZZ/cGNd1lq4/7akDWMxdhVlucjmnOQ==", - "dev": true, "dependencies": { "follow-redirects": "^1.14.9", "form-data": "^4.0.0" @@ -7819,6 +7819,40 @@ "url": "https://github.com/ArthurFiorette/axios-cache-interceptor?sponsor=1" } }, + "node_modules/axios-mock-adapter": { + "version": "1.21.4", + "resolved": "https://registry.npmjs.org/axios-mock-adapter/-/axios-mock-adapter-1.21.4.tgz", + "integrity": "sha512-ztnENm28ONAKeRXC/6SUW6pcsaXbThKq93MRDRAA47LYTzrGSDoO/DCr1NHz7jApEl95DrBoGPvZ0r9xtSbjqw==", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "is-buffer": "^2.0.5" + }, + "peerDependencies": { + "axios": ">= 0.17.0" + } + }, + "node_modules/axios-mock-adapter/node_modules/is-buffer": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-2.0.5.tgz", + "integrity": "sha512-i2R6zNFDwgEHJyQUtJEk0XFi1i0dPFn/oqjK3/vPCcDeJvW5NQ83V8QbicfF1SupOaB0h8ntgBC2YiE7dfyctQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "engines": { + "node": ">=4" + } + }, "node_modules/axobject-query": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-3.1.1.tgz", @@ -9166,7 +9200,6 @@ "version": "1.0.8", "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", - "dev": true, "dependencies": { "delayed-stream": "~1.0.0" }, @@ -9896,7 +9929,6 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", - "dev": true, "engines": { "node": ">=0.4.0" } @@ -11774,8 +11806,7 @@ "node_modules/fast-deep-equal": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", - "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", - "dev": true + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==" }, "node_modules/fast-defer": { "version": "1.1.7", @@ -12052,7 +12083,6 @@ "version": "1.15.2", "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.2.tgz", "integrity": "sha512-VQLG33o04KaQ8uYi2tVNbdrWp1QWxNNea+nmIB4EVM28v0hmP17z7aG1+wAkNzVq4KeXTq3221ye5qTJP91JwA==", - "dev": true, "funding": [ { "type": "individual", @@ -12283,7 +12313,6 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz", "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==", - "dev": true, "dependencies": { "asynckit": "^0.4.0", "combined-stream": "^1.0.8", @@ -19203,7 +19232,6 @@ "version": "1.52.0", "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", - "dev": true, "engines": { "node": ">= 0.6" } @@ -19212,7 +19240,6 @@ "version": "2.1.35", "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", - "dev": true, "dependencies": { "mime-db": "1.52.0" }, @@ -22628,6 +22655,14 @@ "rimraf": "bin.js" } }, + "node_modules/rosie": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/rosie/-/rosie-2.1.0.tgz", + "integrity": "sha512-Dbzdc+prLXZuB/suRptDnBUY29SdGvND3bLg6cll8n7PNqzuyCxSlRfrkn8PqjS9n4QVsiM7RCvxCkKAkTQRjA==", + "engines": { + "node": ">=10" + } + }, "node_modules/rst-selector-parser": { "version": "2.2.3", "resolved": "https://registry.npmjs.org/rst-selector-parser/-/rst-selector-parser-2.2.3.tgz", diff --git a/package.json b/package.json index cf5ec01..8533d00 100644 --- a/package.json +++ b/package.json @@ -62,14 +62,16 @@ "@fortawesome/free-solid-svg-icons": "6.3.0", "@fortawesome/react-fontawesome": "^0.2.0", "@reduxjs/toolkit": "1.9.5", + "axios-mock-adapter": "1.21.4", "babel-polyfill": "6.26.0", "classnames": "2.3.2", "lodash": "4.17.21", "react-redux": "7.2.9", "react-responsive": "8.2.0", + "react-router-dom": "5.3.4", "react-transition-group": "4.4.5", - "timeago.js": "4.0.2", - "react-router-dom": "5.3.4" + "rosie": "2.1.0", + "timeago.js": "4.0.2" }, "peerDependencies": { "@edx/frontend-platform": "^4.0.0", diff --git a/src/Notifications/data/api.js b/src/Notifications/data/api.js index 8e7c117..adb7438 100644 --- a/src/Notifications/data/api.js +++ b/src/Notifications/data/api.js @@ -1,8 +1,15 @@ -import { camelCaseObject } from '@edx/frontend-platform'; -import notificationsList from './notifications.json'; +import { camelCaseObject, getConfig, snakeCaseObject } from '@edx/frontend-platform'; +import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth'; + +export const getNotificationsCountApiUrl = () => `${getConfig().LMS_BASE_URL}/api/notifications/count/`; +export const getNotificationsApiUrl = () => `${getConfig().LMS_BASE_URL}/api/notifications/`; +export const markNotificationsSeenApiUrl = (appName) => `${getConfig().LMS_BASE_URL}/api/notifications/mark-notifications-unseen/${appName}/`; +export const markAllNotificationsAsReadpiUrl = (appName, id) => `${getConfig().LMS_BASE_URL}/api/notifications/mark-notifications-read/${appName}/${id}`; export async function getNotifications(appName, page, pageSize) { - const { data } = notificationsList; + const params = snakeCaseObject({ page, pageSize }); + const { data } = await getAuthenticatedHttpClient().get(getNotificationsApiUrl(), { params }); + const startIndex = (page - 1) * pageSize; const endIndex = startIndex + pageSize; @@ -11,30 +18,25 @@ export async function getNotifications(appName, page, pageSize) { } export async function getNotificationCounts() { - const data = { - count: 45, - count_by_app_name: { - reminders: 10, - discussions: 20, - grades: 10, - authoring: 5, - }, - show_notification_tray: false, - }; + const { data } = await getAuthenticatedHttpClient().get(getNotificationsCountApiUrl()); + return camelCaseObject(data); } -export async function markNotificationSeen() { - const data = []; +export async function markNotificationSeen(appName) { + const { data } = await getAuthenticatedHttpClient().put(`${markNotificationsSeenApiUrl(appName)}`); + return camelCaseObject(data); } -export async function markAllNotificationRead() { - const { data } = camelCaseObject(notificationsList); - return data; +export async function markAllNotificationRead(appName) { + const { data } = await getAuthenticatedHttpClient().put(`${markAllNotificationsAsReadpiUrl(appName)}`); + + return camelCaseObject(data); } -export async function markNotificationRead(notificationId) { - const { data } = camelCaseObject(notificationsList); - return { data, id: notificationId }; +export async function markNotificationRead(appName, notificationId) { + const { data } = await getAuthenticatedHttpClient().put(`${markAllNotificationsAsReadpiUrl(appName, notificationId)}`); + + return camelCaseObject({ data, id: notificationId }); } diff --git a/src/Notifications/data/redux.test.js b/src/Notifications/data/redux.test.js index 4810458..34b137a 100644 --- a/src/Notifications/data/redux.test.js +++ b/src/Notifications/data/redux.test.js @@ -40,7 +40,10 @@ describe('Notification Redux', () => { store = initializeStore(); axiosMock.onGet(notificationCountsApiUrl).reply(200, (Factory.build('notificationsCount'))); - axiosMock.onGet(notificationsApiUrl).reply(200, (Factory.buildList('notification', 2, null))); + 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); }); @@ -68,9 +71,9 @@ describe('Notification Redux', () => { }); it('successfully loaded notifications list in the redux.', async () => { - const state = store.getState(); + const { notifications: { notifications } } = store.getState(); - expect(Object.keys(state.notifications.notifications)).toHaveLength(2); + expect(Object.keys(notifications)).toHaveLength(2); }); it('successfully loaded notification counts in the redux.', async () => { @@ -84,9 +87,9 @@ describe('Notification Redux', () => { }); it('successfully loaded showNotificationTray status in the redux based on api.', async () => { - const state = store.getState(); + const { notifications: { showNotificationTray } } = store.getState(); - expect(state.notifications.showNotificationTray).toEqual(true); + expect(showNotificationTray).toEqual(true); }); it('successfully store the count, numPages, currentPage, and nextPage data in redux.', async () => { @@ -98,15 +101,15 @@ describe('Notification Redux', () => { }); it('successfully updated the selected app name in redux.', async () => { - const state = store.getState(); + const { notifications: { appName } } = store.getState(); - expect(state.notifications.appName).toEqual('discussions'); + expect(appName).toEqual('discussions'); }); it('successfully store notification ids in the selected app in apps.', async () => { - const state = store.getState(); + const { notifications: { apps } } = store.getState(); - expect(state.notifications.apps.discussions).toHaveLength(2); + expect(apps.discussions).toHaveLength(2); }); it('successfully marked all notifications as seen for selected app.', async () => { @@ -120,10 +123,10 @@ describe('Notification Redux', () => { axiosMock.onPut(markedAllNotificationsAsReadApiUrl).reply(200); await executeThunk(markAllNotificationsAsRead('discussions'), store.dispatch, store.getState); - const { notifications } = store.getState(); - const firstNotification = Object.values(notifications.notifications)[0]; + const { notifications: { notificationStatus, notifications } } = store.getState(); + const firstNotification = Object.values(notifications)[0]; - expect(notifications.notificationStatus).toEqual('successful'); + expect(notificationStatus).toEqual('successful'); expect(firstNotification.lastRead).not.toBeNull(); }); @@ -131,10 +134,10 @@ describe('Notification Redux', () => { 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]; + const { notifications: { notificationStatus, notifications } } = store.getState(); + const firstNotification = Object.values(notifications)[0]; - expect(notifications.notificationStatus).toEqual('successful'); + expect(notificationStatus).toEqual('successful'); expect(firstNotification.lastRead).not.toBeNull(); }); }); diff --git a/src/Notifications/data/selector.test.jsx b/src/Notifications/data/selector.test.jsx index 90f51f8..d09c368 100644 --- a/src/Notifications/data/selector.test.jsx +++ b/src/Notifications/data/selector.test.jsx @@ -43,7 +43,10 @@ describe('Notification Selectors', () => { store = initializeStore(); axiosMock.onGet(notificationCountsApiUrl).reply(200, (Factory.build('notificationsCount'))); - axiosMock.onGet(notificationsApiUrl).reply(200, (Factory.buildList('notification', 2, null))); + 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); }); diff --git a/src/Notifications/data/thunks.js b/src/Notifications/data/thunks.js index 1e702b2..df71063 100644 --- a/src/Notifications/data/thunks.js +++ b/src/Notifications/data/thunks.js @@ -97,11 +97,11 @@ export const markAllNotificationsAsRead = (appName) => ( } ); -export const markNotificationsAsRead = (notificationId) => ( +export const markNotificationsAsRead = (appName, notificationId) => ( async (dispatch) => { try { dispatch(markNotificationsAsReadRequest({ notificationId })); - const data = await markNotificationRead(notificationId); + const data = await markNotificationRead(appName, notificationId); dispatch(markNotificationsAsReadSuccess(data)); } catch (error) { if (getHttpErrorStatus(error) === 403) { From 4483a734bca958975416577c594da14691a770d5 Mon Sep 17 00:00:00 2001 From: SundasNoreen Date: Mon, 19 Jun 2023 12:24:57 +0500 Subject: [PATCH 3/5] refactor: fixed issues of review --- src/Notifications/data/api.js | 22 ++-- src/Notifications/data/api.test.js | 143 ++++++++++------------- src/Notifications/data/redux.test.js | 45 ++----- src/Notifications/data/selector.test.jsx | 18 +-- src/Notifications/data/thunks.js | 15 +-- 5 files changed, 101 insertions(+), 142 deletions(-) diff --git a/src/Notifications/data/api.js b/src/Notifications/data/api.js index adb7438..2aa2c05 100644 --- a/src/Notifications/data/api.js +++ b/src/Notifications/data/api.js @@ -1,10 +1,10 @@ -import { camelCaseObject, getConfig, snakeCaseObject } from '@edx/frontend-platform'; +import { getConfig, snakeCaseObject } from '@edx/frontend-platform'; import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth'; export const getNotificationsCountApiUrl = () => `${getConfig().LMS_BASE_URL}/api/notifications/count/`; export const getNotificationsApiUrl = () => `${getConfig().LMS_BASE_URL}/api/notifications/`; export const markNotificationsSeenApiUrl = (appName) => `${getConfig().LMS_BASE_URL}/api/notifications/mark-notifications-unseen/${appName}/`; -export const markAllNotificationsAsReadpiUrl = (appName, id) => `${getConfig().LMS_BASE_URL}/api/notifications/mark-notifications-read/${appName}/${id}`; +export const markAllNotificationsAsReadpiUrl = () => `${getConfig().LMS_BASE_URL}/api/notifications/read/`; export async function getNotifications(appName, page, pageSize) { const params = snakeCaseObject({ page, pageSize }); @@ -14,29 +14,31 @@ export async function getNotifications(appName, page, pageSize) { const endIndex = startIndex + pageSize; const notifications = data.slice(startIndex, endIndex); - return { notifications: camelCaseObject(notifications), numPages: 2, currentPage: page }; + return { notifications, numPages: 2, currentPage: page }; } export async function getNotificationCounts() { const { data } = await getAuthenticatedHttpClient().get(getNotificationsCountApiUrl()); - return camelCaseObject(data); + return data; } export async function markNotificationSeen(appName) { const { data } = await getAuthenticatedHttpClient().put(`${markNotificationsSeenApiUrl(appName)}`); - return camelCaseObject(data); + return data; } export async function markAllNotificationRead(appName) { - const { data } = await getAuthenticatedHttpClient().put(`${markAllNotificationsAsReadpiUrl(appName)}`); + const params = snakeCaseObject({ appName }); + const { data } = await getAuthenticatedHttpClient().put(markAllNotificationsAsReadpiUrl(), { params }); - return camelCaseObject(data); + return data; } -export async function markNotificationRead(appName, notificationId) { - const { data } = await getAuthenticatedHttpClient().put(`${markAllNotificationsAsReadpiUrl(appName, notificationId)}`); +export async function markNotificationRead(notificationId) { + const params = snakeCaseObject({ notificationId }); + const { data } = await getAuthenticatedHttpClient().put(markAllNotificationsAsReadpiUrl(), { params }); - return camelCaseObject({ data, id: notificationId }); + return { data, id: notificationId }; } diff --git a/src/Notifications/data/api.test.js b/src/Notifications/data/api.test.js index 58f8963..488aeab 100644 --- a/src/Notifications/data/api.test.js +++ b/src/Notifications/data/api.test.js @@ -4,30 +4,19 @@ 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); +const markedAllNotificationsAsReadApiUrl = markAllNotificationsAsReadpiUrl(); let axiosMock = null; -let store; describe('Notifications API', () => { beforeEach(async () => { @@ -41,14 +30,13 @@ describe('Notifications API', () => { }); axiosMock = new MockAdapter(getAuthenticatedHttpClient()); Factory.resetAll(); - store = initializeStore(); }); afterEach(() => { axiosMock.reset(); }); - it('successfully get notification counts for different tabs.', async () => { + it('Successfully get notification counts for different tabs.', async () => { axiosMock.onGet(notificationCountsApiUrl).reply(200, (Factory.build('notificationsCount'))); const { count, countByAppName } = await getNotificationCounts(); @@ -60,21 +48,20 @@ describe('Notifications API', () => { 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.each([ + { statusCode: 404, message: 'Failed to get notification counts.' }, + { statusCode: 403, message: 'Denied to get notification counts.' }, + ])('%s for notification counts API.', async ({ statusCode, message }) => { + axiosMock.onGet(notificationCountsApiUrl).reply(statusCode, { message }); + try { + await getNotificationCounts(); + } catch (error) { + expect(error.response.status).toEqual(statusCode); + expect(error.response.data.message).toEqual(message); + } }); - 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 () => { + it('Successfully get notifications.', async () => { axiosMock.onGet(notificationsApiUrl).reply( 200, (Factory.buildList('notification', 2, null, { createdDate: new Date().toISOString() })), @@ -85,21 +72,20 @@ describe('Notifications API', () => { 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.each([ + { statusCode: 404, message: 'Failed to get notifications.' }, + { statusCode: 403, message: 'Denied to get notifications.' }, + ])('%s for notification API.', async ({ statusCode, message }) => { + axiosMock.onGet(notificationsApiUrl).reply(statusCode, { message }); + try { + await getNotifications({ page: 1, pageSize: 10 }); + } catch (error) { + expect(error.response.status).toEqual(statusCode); + expect(error.response.data.message).toEqual(message); + } }); - 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 () => { + 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'); @@ -107,21 +93,20 @@ describe('Notifications API', () => { 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.each([ + { statusCode: 404, message: 'Failed to mark all notifications as seen for selected app.' }, + { statusCode: 403, message: 'Denied to mark all notifications as seen for selected app.' }, + ])('%s for notification mark as seen API.', async ({ statusCode, message }) => { + axiosMock.onPut(markedAllNotificationsAsSeenApiUrl).reply(statusCode, { message }); + try { + await markNotificationSeen('discussions'); + } catch (error) { + expect(error.response.status).toEqual(statusCode); + expect(error.response.data.message).toEqual(message); + } }); - 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 () => { + 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'); @@ -129,39 +114,37 @@ describe('Notifications API', () => { 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.each([ + { 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.' }, + ])('%s for notification mark all as read API.', async ({ statusCode, message }) => { + axiosMock.onPut(markedAllNotificationsAsReadApiUrl).reply(statusCode, { message }); + try { + await markAllNotificationRead('discussions'); + } catch (error) { + expect(error.response.status).toEqual(statusCode); + expect(error.response.data.message).toEqual(message); + } }); - it('denied to mark all notifications as read for selected app.', async () => { - axiosMock.onPut(markedAllNotificationsAsReadApiUrl).reply(403, {}); - await executeThunk(markAllNotificationsAsRead('discussions'), store.dispatch); + it('Successfully marked notification as read.', async () => { + axiosMock.onPut(markedAllNotificationsAsReadApiUrl).reply(200, { message: 'Notification marked read.' }); - 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); + const { data } = await markNotificationRead(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'); + it.each([ + { statusCode: 404, message: 'Failed to mark notification as read.' }, + { statusCode: 403, message: 'Denied to mark notification as read.' }, + ])('%s for notification mark as read API.', async ({ statusCode, message }) => { + axiosMock.onPut(markedAllNotificationsAsReadApiUrl).reply(statusCode, { message }); + try { + await markAllNotificationRead(1); + } catch (error) { + expect(error.response.status).toEqual(statusCode); + expect(error.response.data.message).toEqual(message); + } }); }); diff --git a/src/Notifications/data/redux.test.js b/src/Notifications/data/redux.test.js index 34b137a..998ce8f 100644 --- a/src/Notifications/data/redux.test.js +++ b/src/Notifications/data/redux.test.js @@ -18,8 +18,7 @@ import './__factories__'; const notificationCountsApiUrl = getNotificationsCountApiUrl(); const notificationsApiUrl = getNotificationsApiUrl(); -const markedNotificationAsReadApiUrl = markAllNotificationsAsReadpiUrl('discussions', 1); -const markedAllNotificationsAsReadApiUrl = markAllNotificationsAsReadpiUrl('discussions'); +const markedAllNotificationsAsReadApiUrl = markAllNotificationsAsReadpiUrl(); const markedAllNotificationsAsSeenApiUrl = markNotificationsSeenApiUrl('discussions'); let axiosMock; @@ -52,7 +51,7 @@ describe('Notification Redux', () => { axiosMock.reset(); }); - it('successfully loaded initial notification states in the redux.', async () => { + it('Successfully loaded initial notification states in the redux.', async () => { executeThunk(resetNotificationState(), store.dispatch, store.getState); const { notifications } = store.getState(); @@ -70,13 +69,13 @@ describe('Notification Redux', () => { 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(); expect(Object.keys(notifications)).toHaveLength(2); }); - it('successfully loaded notification counts in the redux.', async () => { + it('Successfully loaded notification counts in the redux.', async () => { const { notifications: { tabsCount } } = store.getState(); expect(tabsCount.count).toEqual(25); @@ -86,40 +85,14 @@ describe('Notification Redux', () => { expect(tabsCount.authoring).toEqual(5); }); - it('successfully loaded showNotificationTray status in the redux based on api.', async () => { - const { notifications: { showNotificationTray } } = store.getState(); - - expect(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 { notifications: { appName } } = store.getState(); - - expect(appName).toEqual('discussions'); - }); - - it('successfully store notification ids in the selected app in apps.', async () => { - const { notifications: { apps } } = store.getState(); - - expect(apps.discussions).toHaveLength(2); - }); - - 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); 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 () => { + 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); @@ -130,9 +103,9 @@ describe('Notification Redux', () => { 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); + it('Successfully marked notification as read in the redux.', async () => { + axiosMock.onPut(markedAllNotificationsAsReadApiUrl).reply(200); + await executeThunk(markNotificationsAsRead(1), store.dispatch, store.getState); const { notifications: { notificationStatus, notifications } } = store.getState(); const firstNotification = Object.values(notifications)[0]; diff --git a/src/Notifications/data/selector.test.jsx b/src/Notifications/data/selector.test.jsx index d09c368..bc73a3f 100644 --- a/src/Notifications/data/selector.test.jsx +++ b/src/Notifications/data/selector.test.jsx @@ -55,14 +55,14 @@ describe('Notification Selectors', () => { axiosMock.reset(); }); - it('should return notification status.', async () => { + 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 () => { + it('Should return notification tabs count.', async () => { const state = store.getState(); const tabsCount = selectNotificationTabsCount()(state); @@ -73,49 +73,49 @@ describe('Notification Selectors', () => { expect(tabsCount.authoring).toEqual(5); }); - it('should return notification tabs.', async () => { + 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 () => { + 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 () => { + 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 () => { + 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 () => { + 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 () => { + 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 () => { + it('Should return pagination data.', async () => { const state = store.getState(); const paginationData = selectPaginationData()(state); diff --git a/src/Notifications/data/thunks.js b/src/Notifications/data/thunks.js index df71063..3ffbb82 100644 --- a/src/Notifications/data/thunks.js +++ b/src/Notifications/data/thunks.js @@ -1,3 +1,4 @@ +import { camelCaseObject } from '@edx/frontend-platform'; import { fetchNotificationSuccess, fetchNotificationRequest, @@ -47,7 +48,7 @@ export const fetchNotificationList = ({ appName, page, pageSize }) => ( try { dispatch(fetchNotificationRequest({ appName })); const data = await getNotifications(appName, page, pageSize); - const normalisedData = normalizeNotifications((data)); + const normalisedData = normalizeNotifications((camelCaseObject(data))); dispatch(fetchNotificationSuccess({ ...normalisedData, numPages: data.numPages, currentPage: data.currentPage })); } catch (error) { if (getHttpErrorStatus(error) === 403) { @@ -64,7 +65,7 @@ export const fetchAppsNotificationCount = () => ( try { dispatch(fetchNotificationsCountRequest()); const data = await getNotificationCounts(); - const normalisedData = normalizeNotificationCounts((data)); + const normalisedData = normalizeNotificationCounts((camelCaseObject(data))); dispatch(fetchNotificationsCountSuccess({ ...normalisedData, countByAppName: data.countByAppName, @@ -86,7 +87,7 @@ export const markAllNotificationsAsRead = (appName) => ( try { dispatch(markAllNotificationsAsReadRequest({ appName })); const data = await markAllNotificationRead(appName); - dispatch(markAllNotificationsAsReadSuccess(data)); + dispatch(markAllNotificationsAsReadSuccess(camelCaseObject(data))); } catch (error) { if (getHttpErrorStatus(error) === 403) { dispatch(markAllNotificationsAsReadDenied()); @@ -97,12 +98,12 @@ export const markAllNotificationsAsRead = (appName) => ( } ); -export const markNotificationsAsRead = (appName, notificationId) => ( +export const markNotificationsAsRead = (notificationId) => ( async (dispatch) => { try { dispatch(markNotificationsAsReadRequest({ notificationId })); - const data = await markNotificationRead(appName, notificationId); - dispatch(markNotificationsAsReadSuccess(data)); + const data = await markNotificationRead(notificationId); + dispatch(markNotificationsAsReadSuccess(camelCaseObject(data))); } catch (error) { if (getHttpErrorStatus(error) === 403) { dispatch(markNotificationsAsReadDenied()); @@ -118,7 +119,7 @@ export const markNotificationsAsSeen = (appName) => ( try { dispatch(markNotificationsAsSeenRequest({ appName })); const data = await markNotificationSeen(appName); - dispatch(markNotificationsAsSeenSuccess(data)); + dispatch(markNotificationsAsSeenSuccess(camelCaseObject(data))); } catch (error) { if (getHttpErrorStatus(error) === 403) { dispatch(markNotificationsAsSeenDenied()); From 8175ba897a6c5c0204661683b15ccc8d67bab5c3 Mon Sep 17 00:00:00 2001 From: SundasNoreen Date: Mon, 19 Jun 2023 16:04:55 +0500 Subject: [PATCH 4/5] test: added failed and denied test cases of redux --- src/Notifications/data/redux.test.js | 60 ++++++++++++++++++++++++++++ 1 file changed, 60 insertions(+) diff --git a/src/Notifications/data/redux.test.js b/src/Notifications/data/redux.test.js index 998ce8f..bada666 100644 --- a/src/Notifications/data/redux.test.js +++ b/src/Notifications/data/redux.test.js @@ -75,6 +75,18 @@ describe('Notification Redux', () => { expect(Object.keys(notifications)).toHaveLength(2); }); + it.each([ + { statusCode: 404, status: 'failed' }, + { statusCode: 403, status: 'denied' }, + ])('%s to load notifications list in the redux.', async ({ statusCode, status }) => { + axiosMock.onGet(notificationsApiUrl).reply(statusCode); + await executeThunk(fetchNotificationList({ page: 1, pageSize: 10 }), store.dispatch, store.getState); + + const { notifications: { notificationStatus } } = store.getState(); + + expect(notificationStatus).toEqual(status); + }); + it('Successfully loaded notification counts in the redux.', async () => { const { notifications: { tabsCount } } = store.getState(); @@ -85,6 +97,18 @@ describe('Notification Redux', () => { expect(tabsCount.authoring).toEqual(5); }); + it.each([ + { statusCode: 404, status: 'failed' }, + { statusCode: 403, status: 'denied' }, + ])('%s to load notification counts in the redux.', async ({ statusCode, status }) => { + axiosMock.onGet(notificationCountsApiUrl).reply(statusCode); + await executeThunk(fetchAppsNotificationCount(), store.dispatch, store.getState); + + const { notifications: { notificationStatus } } = store.getState(); + + expect(notificationStatus).toEqual(status); + }); + it('Successfully marked all notifications as seen for selected app.', async () => { axiosMock.onPut(markedAllNotificationsAsSeenApiUrl).reply(200); await executeThunk(markNotificationsAsSeen('discussions'), store.dispatch, store.getState); @@ -92,6 +116,18 @@ describe('Notification Redux', () => { expect(store.getState().notifications.notificationStatus).toEqual('successful'); }); + it.each([ + { statusCode: 404, status: 'failed' }, + { statusCode: 403, status: 'denied' }, + ])('%s to mark all notifications as seen for selected app.', async ({ statusCode, status }) => { + axiosMock.onPut(markedAllNotificationsAsSeenApiUrl).reply(statusCode); + await executeThunk(markNotificationsAsSeen('discussions'), store.dispatch, store.getState); + + const { notifications: { notificationStatus } } = store.getState(); + + expect(notificationStatus).toEqual(status); + }); + 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); @@ -103,6 +139,18 @@ describe('Notification Redux', () => { expect(firstNotification.lastRead).not.toBeNull(); }); + it.each([ + { statusCode: 404, status: 'failed' }, + { statusCode: 403, status: 'denied' }, + ])('%s to mark all notifications as read for selected app in the redux.', async ({ statusCode, status }) => { + axiosMock.onPut(markedAllNotificationsAsReadApiUrl).reply(statusCode); + await executeThunk(markAllNotificationsAsRead('discussions'), store.dispatch, store.getState); + + const { notifications: { notificationStatus } } = store.getState(); + + expect(notificationStatus).toEqual(status); + }); + it('Successfully marked notification as read in the redux.', async () => { axiosMock.onPut(markedAllNotificationsAsReadApiUrl).reply(200); await executeThunk(markNotificationsAsRead(1), store.dispatch, store.getState); @@ -113,4 +161,16 @@ describe('Notification Redux', () => { expect(notificationStatus).toEqual('successful'); expect(firstNotification.lastRead).not.toBeNull(); }); + + it.each([ + { statusCode: 404, status: 'failed' }, + { statusCode: 403, status: 'denied' }, + ])('%s to marked notification as read in the redux.', async ({ statusCode, status }) => { + axiosMock.onPut(markedAllNotificationsAsReadApiUrl).reply(statusCode); + await executeThunk(markNotificationsAsRead(1), store.dispatch, store.getState); + + const { notifications: { notificationStatus } } = store.getState(); + + expect(notificationStatus).toEqual(status); + }); }); From a52ddfd9bdd224a483e6504edf0906fda1c680d6 Mon Sep 17 00:00:00 2001 From: SundasNoreen Date: Mon, 19 Jun 2023 16:58:32 +0500 Subject: [PATCH 5/5] refactor: changed api url --- src/Notifications/data/api.js | 6 +++--- src/Notifications/data/api.test.js | 4 ++-- src/Notifications/data/redux.test.js | 16 ++-------------- 3 files changed, 7 insertions(+), 19 deletions(-) diff --git a/src/Notifications/data/api.js b/src/Notifications/data/api.js index 2aa2c05..020ef80 100644 --- a/src/Notifications/data/api.js +++ b/src/Notifications/data/api.js @@ -4,7 +4,7 @@ import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth'; export const getNotificationsCountApiUrl = () => `${getConfig().LMS_BASE_URL}/api/notifications/count/`; export const getNotificationsApiUrl = () => `${getConfig().LMS_BASE_URL}/api/notifications/`; export const markNotificationsSeenApiUrl = (appName) => `${getConfig().LMS_BASE_URL}/api/notifications/mark-notifications-unseen/${appName}/`; -export const markAllNotificationsAsReadpiUrl = () => `${getConfig().LMS_BASE_URL}/api/notifications/read/`; +export const markNotificationAsReadApiUrl = () => `${getConfig().LMS_BASE_URL}/api/notifications/read/`; export async function getNotifications(appName, page, pageSize) { const params = snakeCaseObject({ page, pageSize }); @@ -31,14 +31,14 @@ export async function markNotificationSeen(appName) { export async function markAllNotificationRead(appName) { const params = snakeCaseObject({ appName }); - const { data } = await getAuthenticatedHttpClient().put(markAllNotificationsAsReadpiUrl(), { params }); + const { data } = await getAuthenticatedHttpClient().put(markNotificationAsReadApiUrl(), { params }); return data; } export async function markNotificationRead(notificationId) { const params = snakeCaseObject({ notificationId }); - const { data } = await getAuthenticatedHttpClient().put(markAllNotificationsAsReadpiUrl(), { params }); + const { data } = await getAuthenticatedHttpClient().put(markNotificationAsReadApiUrl(), { params }); return { data, id: notificationId }; } diff --git a/src/Notifications/data/api.test.js b/src/Notifications/data/api.test.js index 488aeab..9c0d82a 100644 --- a/src/Notifications/data/api.test.js +++ b/src/Notifications/data/api.test.js @@ -5,7 +5,7 @@ import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth'; import { initializeMockApp } from '@edx/frontend-platform/testing'; import { - getNotificationsApiUrl, getNotificationsCountApiUrl, markAllNotificationsAsReadpiUrl, markNotificationsSeenApiUrl, + getNotificationsApiUrl, getNotificationsCountApiUrl, markNotificationAsReadApiUrl, markNotificationsSeenApiUrl, getNotificationCounts, getNotifications, markNotificationSeen, markAllNotificationRead, markNotificationRead, } from './api'; @@ -14,7 +14,7 @@ import './__factories__'; const notificationCountsApiUrl = getNotificationsCountApiUrl(); const notificationsApiUrl = getNotificationsApiUrl(); const markedAllNotificationsAsSeenApiUrl = markNotificationsSeenApiUrl('discussions'); -const markedAllNotificationsAsReadApiUrl = markAllNotificationsAsReadpiUrl(); +const markedAllNotificationsAsReadApiUrl = markNotificationAsReadApiUrl(); let axiosMock = null; diff --git a/src/Notifications/data/redux.test.js b/src/Notifications/data/redux.test.js index bada666..f17e6bf 100644 --- a/src/Notifications/data/redux.test.js +++ b/src/Notifications/data/redux.test.js @@ -7,7 +7,7 @@ import { initializeMockApp } from '@edx/frontend-platform/testing'; import { initializeStore } from '../../store'; import executeThunk from '../../test-utils'; import { - getNotificationsApiUrl, getNotificationsCountApiUrl, markAllNotificationsAsReadpiUrl, markNotificationsSeenApiUrl, + getNotificationsApiUrl, getNotificationsCountApiUrl, markNotificationAsReadApiUrl, markNotificationsSeenApiUrl, } from './api'; import { fetchAppsNotificationCount, fetchNotificationList, markNotificationsAsRead, markAllNotificationsAsRead, @@ -18,7 +18,7 @@ import './__factories__'; const notificationCountsApiUrl = getNotificationsCountApiUrl(); const notificationsApiUrl = getNotificationsApiUrl(); -const markedAllNotificationsAsReadApiUrl = markAllNotificationsAsReadpiUrl(); +const markedAllNotificationsAsReadApiUrl = markNotificationAsReadApiUrl(); const markedAllNotificationsAsSeenApiUrl = markNotificationsSeenApiUrl('discussions'); let axiosMock; @@ -139,18 +139,6 @@ describe('Notification Redux', () => { expect(firstNotification.lastRead).not.toBeNull(); }); - it.each([ - { statusCode: 404, status: 'failed' }, - { statusCode: 403, status: 'denied' }, - ])('%s to mark all notifications as read for selected app in the redux.', async ({ statusCode, status }) => { - axiosMock.onPut(markedAllNotificationsAsReadApiUrl).reply(statusCode); - await executeThunk(markAllNotificationsAsRead('discussions'), store.dispatch, store.getState); - - const { notifications: { notificationStatus } } = store.getState(); - - expect(notificationStatus).toEqual(status); - }); - it('Successfully marked notification as read in the redux.', async () => { axiosMock.onPut(markedAllNotificationsAsReadApiUrl).reply(200); await executeThunk(markNotificationsAsRead(1), store.dispatch, store.getState);