diff --git a/.eslintignore b/.eslintignore index 9a23d71e..f36e16b8 100644 --- a/.eslintignore +++ b/.eslintignore @@ -3,3 +3,4 @@ dist/ node_modules/ jest.config.js .eslintrc.js +test-utils.jsx diff --git a/.eslintrc.js b/.eslintrc.js index 7c44dd07..50d24fdc 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -1,4 +1,3 @@ -// eslint-disable-next-line import/no-extraneous-dependencies const { createConfig } = require('@edx/frontend-build'); module.exports = createConfig('eslint', { diff --git a/package-lock.json b/package-lock.json index 23369d92..806a55f5 100644 --- a/package-lock.json +++ b/package-lock.json @@ -18,7 +18,7 @@ "@fortawesome/react-fontawesome": "^0.2.0", "@reduxjs/toolkit": "^1.9.5", "core-js": "3.31.0", - "moment": "^2.29.4", + "date-fns": "^3.3.1", "prop-types": "15.8.1", "react": "16.14.0", "react-dom": "16.14.0", @@ -9295,6 +9295,15 @@ "node": ">=10" } }, + "node_modules/date-fns": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-3.3.1.tgz", + "integrity": "sha512-y8e109LYGgoQDveiEBD3DYXKba1jWf5BA8YU1FL5Tvm0BTdEfy54WLCwnuYWZNnzzvALy/QQ4Hov+Q9RVRv+Zw==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/kossnocorp" + } + }, "node_modules/debug": { "version": "4.3.4", "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", @@ -17887,14 +17896,6 @@ "resolved": "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz", "integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==" }, - "node_modules/moment": { - "version": "2.29.4", - "resolved": "https://registry.npmjs.org/moment/-/moment-2.29.4.tgz", - "integrity": "sha512-5LC9SOxjSc2HF6vO2CyuTDNivEdoz2IvyJJGj6X8DJ0eFyfszE0QiEd+iXmBvUP3WHxSjFH/vIsA0EN00cgr8w==", - "engines": { - "node": "*" - } - }, "node_modules/mrmime": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/mrmime/-/mrmime-1.0.1.tgz", diff --git a/package.json b/package.json index a8f06228..101b134c 100644 --- a/package.json +++ b/package.json @@ -42,7 +42,7 @@ "@fortawesome/react-fontawesome": "^0.2.0", "@reduxjs/toolkit": "^1.9.5", "core-js": "3.31.0", - "moment": "^2.29.4", + "date-fns": "^3.3.1", "prop-types": "15.8.1", "react": "16.14.0", "react-dom": "16.14.0", diff --git a/src/assets/colors.scss b/src/assets/colors.scss index 758f4730..3faa622a 100644 --- a/src/assets/colors.scss +++ b/src/assets/colors.scss @@ -5,6 +5,7 @@ $color-black: #020917; $black-80: #333; $color-white: #fefefe; $color-gray: #60646d; +$gray-70: #666666; $gray-60: #808080; $gray-20: #dfe1e1; $color-pink: #ffecf0; diff --git a/src/features/Common/data/_test_/api.test.js b/src/features/Common/data/_test_/api.test.js index 39baa76c..b9849b49 100644 --- a/src/features/Common/data/_test_/api.test.js +++ b/src/features/Common/data/_test_/api.test.js @@ -1,5 +1,9 @@ import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth'; -import { getCoursesByInstitution, getLicensesByInstitution } from 'features/Common/data/api'; +import { + getCoursesByInstitution, + getLicensesByInstitution, + getClassesByInstitution, +} from 'features/Common/data/api'; jest.mock('@edx/frontend-platform/auth', () => ({ getAuthenticatedHttpClient: jest.fn(), @@ -13,6 +17,7 @@ jest.mock('@edx/frontend-platform', () => ({ })); describe('Common api services', () => { + const COURSE_OPERATIONS_API_V2 = 'http://localhost:18000/pearson_course_operation/api/v2'; test('should call getCoursesByInstitution with the correct parameters', () => { const httpClientMock = { get: jest.fn(), @@ -30,7 +35,7 @@ describe('Common api services', () => { expect(httpClientMock.get).toHaveBeenCalledTimes(1); expect(httpClientMock.get).toHaveBeenCalledWith( - 'http://localhost:18000/pearson_course_operation/api/v2/courses/?limit=true&institution_id=1', + `${COURSE_OPERATIONS_API_V2}/courses/?limit=true&institution_id=1`, { params: { page } }, ); }); @@ -51,7 +56,28 @@ describe('Common api services', () => { expect(httpClientMock.get).toHaveBeenCalledTimes(1); expect(httpClientMock.get).toHaveBeenCalledWith( - 'http://localhost:18000/pearson_course_operation/api/v2/license-pool/?limit=true&institution_id=1', + `${COURSE_OPERATIONS_API_V2}/license-pool/?limit=true&institution_id=1`, + ); + }); + + test('should call getClassesByInstitution with the correct parameters', () => { + const httpClientMock = { + get: jest.fn(), + }; + + const institutionId = 1; + const courseName = 'ccx1'; + + getAuthenticatedHttpClient.mockReturnValue(httpClientMock); + + getClassesByInstitution(institutionId, courseName); + + expect(getAuthenticatedHttpClient).toHaveBeenCalledTimes(3); + expect(getAuthenticatedHttpClient).toHaveBeenCalledWith(); + + expect(httpClientMock.get).toHaveBeenCalledTimes(1); + expect(httpClientMock.get).toHaveBeenCalledWith( + `${COURSE_OPERATIONS_API_V2}/classes/?limit=false&institution_id=1&course_name=ccx1&instructors=`, ); }); }); diff --git a/src/features/Common/data/api.js b/src/features/Common/data/api.js index ab26bcf4..c8f5e1b5 100644 --- a/src/features/Common/data/api.js +++ b/src/features/Common/data/api.js @@ -18,7 +18,17 @@ function getLicensesByInstitution(institutionId, limit) { ); } +function getClassesByInstitution(institutionId, courseName, limit = false, instructorsList = '') { + const encodedCourseName = encodeURIComponent(courseName); + + return getAuthenticatedHttpClient().get( + `${getConfig().COURSE_OPERATIONS_API_V2_BASE_URL}/classes` + + `/?limit=${limit}&institution_id=${institutionId}&course_name=${encodedCourseName}&instructors=${instructorsList}`, + ); +} + export { getCoursesByInstitution, getLicensesByInstitution, + getClassesByInstitution, }; diff --git a/src/features/Dashboard/DashboardPage/_test_/index.test.jsx b/src/features/Dashboard/DashboardPage/_test_/index.test.jsx index 225d5e2e..feac9ce9 100644 --- a/src/features/Dashboard/DashboardPage/_test_/index.test.jsx +++ b/src/features/Dashboard/DashboardPage/_test_/index.test.jsx @@ -1,28 +1,50 @@ import React from 'react'; -import { render } from '@testing-library/react'; import DashboardPage from 'features/Dashboard/DashboardPage'; import '@testing-library/jest-dom/extend-expect'; -import { Provider } from 'react-redux'; -import { initializeStore } from 'store'; - -let store; - -jest.mock('axios'); +import { renderWithProviders } from 'test-utils'; jest.mock('@edx/frontend-platform/logging', () => ({ logError: jest.fn(), })); describe('DashboardPage component', () => { - beforeEach(() => { - store = initializeStore(); - }); + const mockStore = { + dashboard: { + tableLicense: { + data: [ + { + licenseName: 'License Name 1', + purchasedSeats: 20, + numberOfStudents: 6, + numberOfPendingStudents: 11, + }, + ], + }, + classes: { + data: [ + { + classId: 'ccx-v1:demo+demo1+2020+ccx1', + className: 'ccx 1', + masterCourseName: 'Demo Course 1', + instructors: [], + numberOfStudents: 0, + numberOfPendingStudents: 0, + maxStudents: 20, + startDate: '2024-01-23T21:50:51Z', + endDate: null, + }, + ], + }, + }, + }; + + const component = renderWithProviders( + , + { preloadedState: mockStore }, + ); + test('renders components', () => { - const { getByText } = render( - - - , - ); + const { getByText } = component; expect(getByText('This week')).toBeInTheDocument(); expect(getByText('Next week')).toBeInTheDocument(); @@ -30,5 +52,9 @@ describe('DashboardPage component', () => { expect(getByText('New students registered')).toBeInTheDocument(); expect(getByText('Classes scheduled')).toBeInTheDocument(); expect(getByText('License inventory')).toBeInTheDocument(); + expect(getByText('Instructor assignment')).toBeInTheDocument(); + expect(getByText('ccx 1')).toBeInTheDocument(); + expect(getByText('Demo Course 1')).toBeInTheDocument(); + expect(getByText('License Name 1')).toBeInTheDocument(); }); }); diff --git a/src/features/Dashboard/DashboardPage/index.jsx b/src/features/Dashboard/DashboardPage/index.jsx index 5067089d..a7af99b4 100644 --- a/src/features/Dashboard/DashboardPage/index.jsx +++ b/src/features/Dashboard/DashboardPage/index.jsx @@ -2,10 +2,11 @@ import React, { useEffect, useState } from 'react'; import { useSelector, useDispatch } from 'react-redux'; import { useHistory } from 'react-router-dom'; -import { Container } from '@edx/paragon'; +import { Container, Col, Row } from '@edx/paragon'; import StudentsMetrics from 'features/Students/StudentsMetrics'; import LicensesTable from 'features/Licenses/LicensesTable'; import { Button } from 'react-paragon-topaz'; +import InstructorAssignSection from 'features/Dashboard/InstructorAssignSection'; import { fetchLicensesData } from 'features/Dashboard/data'; import { updateActiveTab } from 'features/Main/data/slice'; @@ -50,13 +51,22 @@ const DashboardPage = () => { {stateInstitution.length === 1 ? `Welcome to ${stateInstitution[0].name}` : 'Select an institution'} -
-
-

License inventory

- -
- -
+ + +
+
+

License inventory

+ +
+ +
+ + +
+ +
+ +
); }; diff --git a/src/features/Dashboard/DashboardPage/index.scss b/src/features/Dashboard/DashboardPage/index.scss index 38cf436e..6f9ccdab 100644 --- a/src/features/Dashboard/DashboardPage/index.scss +++ b/src/features/Dashboard/DashboardPage/index.scss @@ -4,3 +4,13 @@ background-color: $color-white; padding: 2rem 0; } + +.instructor-assign-section { + background-color: $color-white; +} + +.license-section, +.instructor-assign-section { + box-shadow: 0px 3px 12px 0px rgba(0, 0, 0, 0.16); + border-radius: 0.375rem; +} diff --git a/src/features/Dashboard/InstructorAssignSection/ClassCard.jsx b/src/features/Dashboard/InstructorAssignSection/ClassCard.jsx new file mode 100644 index 00000000..6748da8b --- /dev/null +++ b/src/features/Dashboard/InstructorAssignSection/ClassCard.jsx @@ -0,0 +1,33 @@ +import React from 'react'; +import { format } from 'date-fns'; +import PropTypes from 'prop-types'; + +import { Button } from 'react-paragon-topaz'; + +import 'features/Dashboard/InstructorAssignSection/index.scss'; + +const ClassCard = ({ data }) => { + const fullDate = format(new Date(data.startDate), 'PP'); + + return ( +
+

{data?.className}

+

{data?.masterCourseName}

+

{fullDate}

+ +
+ ); +}; + +ClassCard.propTypes = { + data: PropTypes.arrayOf(PropTypes.shape([])), +}; + +ClassCard.defaultProps = { + data: [], +}; + +export default ClassCard; diff --git a/src/features/Dashboard/InstructorAssignSection/_test_/index.test.jsx b/src/features/Dashboard/InstructorAssignSection/_test_/index.test.jsx new file mode 100644 index 00000000..c98517b1 --- /dev/null +++ b/src/features/Dashboard/InstructorAssignSection/_test_/index.test.jsx @@ -0,0 +1,44 @@ +import React from 'react'; +import InstructorAssignSection from 'features/Dashboard/InstructorAssignSection'; +import '@testing-library/jest-dom/extend-expect'; +import { renderWithProviders } from 'test-utils'; + +jest.mock('@edx/frontend-platform/logging', () => ({ + logError: jest.fn(), +})); + +describe('Instructor Assign component', () => { + const mockStore = { + dashboard: { + classes: { + data: [ + { + classId: 'ccx-v1:demo+demo1+2020+ccx1', + className: 'ccx 1', + masterCourseName: 'Demo Course 1', + instructors: [], + numberOfStudents: 0, + numberOfPendingStudents: 0, + maxStudents: 20, + startDate: '2024-01-23T21:50:51Z', + endDate: null, + }, + ], + }, + }, + }; + const component = renderWithProviders( + , + { preloadedState: mockStore }, + ); + + test('renders components', () => { + const { getByText } = component; + + expect(getByText('Instructor assignment')).toBeInTheDocument(); + expect(getByText('ccx 1')).toBeInTheDocument(); + expect(getByText('Demo Course 1')).toBeInTheDocument(); + expect(getByText('Jan 23, 2024')).toBeInTheDocument(); + expect(getByText('Assign instructor')).toBeInTheDocument(); + }); +}); diff --git a/src/features/Dashboard/InstructorAssignSection/index.jsx b/src/features/Dashboard/InstructorAssignSection/index.jsx new file mode 100644 index 00000000..338072ac --- /dev/null +++ b/src/features/Dashboard/InstructorAssignSection/index.jsx @@ -0,0 +1,50 @@ +import React, { useEffect, useState } from 'react'; +import { useSelector, useDispatch } from 'react-redux'; + +import { Row, Col } from '@edx/paragon'; +import ClassCard from 'features/Dashboard/InstructorAssignSection/ClassCard'; +import { Button } from 'react-paragon-topaz'; + +import { fetchClassesData } from 'features/Dashboard/data'; + +import 'features/Dashboard/InstructorAssignSection/index.scss'; + +const InstructorAssignSection = () => { + const dispatch = useDispatch(); + const stateInstitution = useSelector((state) => state.main.institution.data); + const classesData = useSelector((state) => state.dashboard.classes.data); + const [classCards, setClassCards] = useState([]); + let idInstitution = ''; + const numberOfClasses = 2; + // eslint-disable-next-line no-unused-expressions + stateInstitution.length > 0 ? idInstitution = stateInstitution[0].id : idInstitution = ''; + + useEffect(() => { + dispatch(fetchClassesData(idInstitution)); + }, [idInstitution]); // eslint-disable-line react-hooks/exhaustive-deps + + useEffect(() => { + // Display only the first 'NumberOfClasses' on the homepage. + if (classesData.length > numberOfClasses) { + setClassCards(classesData.slice(0, numberOfClasses)); + } else { + setClassCards(classesData); + } + }, [classesData]); + + return ( + + +

Instructor assignment

+ {classCards.map(classInfo => )} + {classesData.length > numberOfClasses && ( +
+ +
+ )} + +
+ ); +}; + +export default InstructorAssignSection; diff --git a/src/features/Dashboard/InstructorAssignSection/index.scss b/src/features/Dashboard/InstructorAssignSection/index.scss new file mode 100644 index 00000000..776320a2 --- /dev/null +++ b/src/features/Dashboard/InstructorAssignSection/index.scss @@ -0,0 +1,49 @@ +@import "assets/colors.scss"; + +.title-instr-assign { + padding: 1rem; + border-bottom: 1px solid $gray-20; + margin: 0; +} + +.instructor-assign-section .view-all-btn.btn-primary:not(:disabled):not(.disabled) { + text-decoration: underline; + + &:hover, + &:active, + &:focus, + &:focus-visible { + color: $primary; + background: transparent; + border: none; + outline: none; + text-decoration: underline; + } +} + +.class-card-container { + padding: 1rem; + border-bottom: 1px solid $gray-20; + + .course-name { + margin-bottom: 0.3rem; + font-size: 0.75rem; + text-transform: uppercase; + font-weight: 700; + color: $gray-70; + } + + .date { + font-size: 1rem; + line-height: 21px; + color: $gray-70; + } + + .fa-calendar-day { + margin-right: 8px; + } + + .fa-chalkboard-user { + margin-right: 5px; + } +} diff --git a/src/features/Dashboard/data/_test_/redux.test.jsx b/src/features/Dashboard/data/_test_/redux.test.jsx index 615d5894..ef6cd563 100644 --- a/src/features/Dashboard/data/_test_/redux.test.jsx +++ b/src/features/Dashboard/data/_test_/redux.test.jsx @@ -1,7 +1,7 @@ import MockAdapter from 'axios-mock-adapter'; import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth'; import { initializeMockApp } from '@edx/frontend-platform/testing'; -import { fetchLicensesData } from 'features/Dashboard/data'; +import { fetchLicensesData, fetchClassesData } from 'features/Dashboard/data'; import { executeThunk } from 'test-utils'; import { initializeStore } from 'store'; @@ -28,7 +28,8 @@ describe('Dashboard redux tests', () => { }); test('successful fetch licenses data', async () => { - const licensesApiUrl = `${process.env.COURSE_OPERATIONS_API_V2_BASE_URL}/license-pool/?limit=false&institution_id=1`; + const licensesApiUrl = `${process.env.COURSE_OPERATIONS_API_V2_BASE_URL}/license-pool/` + + '?limit=false&institution_id=1'; const mockResponse = [ { licenseName: 'License Name 1', @@ -59,7 +60,8 @@ describe('Dashboard redux tests', () => { }); test('failed fetch licenses data', async () => { - const licensesApiUrl = `${process.env.COURSE_OPERATIONS_API_V2_BASE_URL}/license-pool/?limit=false&institution_id=1`; + const licensesApiUrl = `${process.env.COURSE_OPERATIONS_API_V2_BASE_URL}/license-pool/` + + '?limit=false&institution_id=1'; axiosMock.onGet(licensesApiUrl) .reply(500); @@ -74,4 +76,53 @@ describe('Dashboard redux tests', () => { expect(store.getState().dashboard.tableLicense.status) .toEqual('error'); }); + + test('successful fetch classes data', async () => { + const classesApiUrl = `${process.env.COURSE_OPERATIONS_API_V2_BASE_URL}/classes/` + + '?limit=false&institution_id=1&course_name=&instructors=null'; + const mockResponse = [ + { + classId: 'ccx-v1:demo+demo1+2020+ccx1', + className: 'ccx 1', + masterCourseName: 'Demo Course 1', + instructors: [], + numberOfStudents: 0, + numberOfPendingStudents: 0, + maxStudents: 20, + startDate: '2024-01-23T21:50:51Z', + endDate: null, + }, + ]; + axiosMock.onGet(classesApiUrl) + .reply(200, mockResponse); + + expect(store.getState().dashboard.classes.status) + .toEqual('loading'); + + await executeThunk(fetchClassesData(1), store.dispatch, store.getState); + + expect(store.getState().dashboard.classes.data) + .toEqual(mockResponse); + + expect(store.getState().dashboard.classes.status) + .toEqual('success'); + }); + + test('failed fetch licenses data', async () => { + const classesApiUrl = `${process.env.COURSE_OPERATIONS_API_V2_BASE_URL}/classes/` + + '?limit=false&institution_id=1&course_name=&instructors=null'; + axiosMock.onGet(classesApiUrl) + .reply(500); + + expect(store.getState().dashboard.classes.status) + .toEqual('loading'); + + await executeThunk(fetchClassesData(1), store.dispatch, store.getState); + + expect(store.getState().dashboard.classes.data) + .toEqual([]); + + expect(store.getState().dashboard.classes.status) + .toEqual('error'); + }); }); diff --git a/src/features/Dashboard/data/index.js b/src/features/Dashboard/data/index.js index 96f810e7..910c1431 100644 --- a/src/features/Dashboard/data/index.js +++ b/src/features/Dashboard/data/index.js @@ -1,2 +1,2 @@ export { reducer } from 'features/Dashboard/data/slice'; -export { fetchLicensesData } from 'features/Dashboard/data/thunks'; +export { fetchLicensesData, fetchClassesData } from 'features/Dashboard/data/thunks'; diff --git a/src/features/Dashboard/data/slice.js b/src/features/Dashboard/data/slice.js index 165c8e4e..31adc748 100644 --- a/src/features/Dashboard/data/slice.js +++ b/src/features/Dashboard/data/slice.js @@ -6,6 +6,9 @@ const initialState = { tableLicense: { ...initialStateService, }, + classes: { + ...initialStateService, + }, }; export const dashboardSlice = createSlice({ @@ -22,6 +25,16 @@ export const dashboardSlice = createSlice({ fetchLicensesDataFailed: (state) => { state.tableLicense.status = RequestStatus.ERROR; }, + fetchClassesDataRequest: (state) => { + state.classes.status = RequestStatus.LOADING; + }, + fetchClassesDataSuccess: (state, { payload }) => { + state.classes.status = RequestStatus.SUCCESS; + state.classes.data = payload; + }, + fetchClassesDataFailed: (state) => { + state.classes.status = RequestStatus.ERROR; + }, }, }); @@ -29,6 +42,9 @@ export const { fetchLicensesDataRequest, fetchLicensesDataSuccess, fetchLicensesDataFailed, + fetchClassesDataRequest, + fetchClassesDataSuccess, + fetchClassesDataFailed, } = dashboardSlice.actions; export const { reducer } = dashboardSlice; diff --git a/src/features/Dashboard/data/thunks.js b/src/features/Dashboard/data/thunks.js index 910479a8..9988ee98 100644 --- a/src/features/Dashboard/data/thunks.js +++ b/src/features/Dashboard/data/thunks.js @@ -1,11 +1,14 @@ import { logError } from '@edx/frontend-platform/logging'; import { camelCaseObject } from '@edx/frontend-platform'; -import { getLicensesByInstitution } from 'features/Common/data/api'; +import { getLicensesByInstitution, getClassesByInstitution } from 'features/Common/data/api'; import { fetchLicensesDataRequest, fetchLicensesDataSuccess, fetchLicensesDataFailed, + fetchClassesDataRequest, + fetchClassesDataSuccess, + fetchClassesDataFailed, } from 'features/Dashboard/data/slice'; function fetchLicensesData(id) { @@ -21,6 +24,20 @@ function fetchLicensesData(id) { }; } +function fetchClassesData(id) { + return async (dispatch) => { + dispatch(fetchClassesDataRequest()); + try { + const response = camelCaseObject(await getClassesByInstitution(id, '', false, 'null')); + dispatch(fetchClassesDataSuccess(response.data)); + } catch (error) { + dispatch(fetchClassesDataFailed()); + logError(error); + } + }; +} + export { fetchLicensesData, + fetchClassesData, }; diff --git a/src/features/Instructors/InstructorsTable/columns.jsx b/src/features/Instructors/InstructorsTable/columns.jsx index 669ddc1a..1e7592e2 100644 --- a/src/features/Instructors/InstructorsTable/columns.jsx +++ b/src/features/Instructors/InstructorsTable/columns.jsx @@ -1,5 +1,5 @@ /* eslint-disable react/prop-types, no-nested-ternary */ -import moment from 'moment'; +import { differenceInHours, differenceInDays, differenceInWeeks } from 'date-fns'; const columns = [ { @@ -10,11 +10,11 @@ const columns = [ Header: 'Last seen', accessor: 'lastAccess', Cell: ({ row }) => { - const currentDate = moment(Date.now()); - const lastDate = moment(new Date(row.values.lastAccess)); - const diffHours = currentDate.diff(lastDate, 'hours'); - const diffDays = currentDate.diff(lastDate, 'days'); - const diffWeeks = currentDate.diff(lastDate, 'weeks'); + const currentDate = Date.now(); + const lastDate = new Date(row.values.lastAccess); + const diffHours = differenceInHours(currentDate, lastDate); + const diffDays = differenceInDays(currentDate, lastDate); + const diffWeeks = differenceInWeeks(currentDate, lastDate); return ( {diffHours < 24 ? 'Today' diff --git a/src/features/Students/data/_test_/api.test.js b/src/features/Students/data/_test_/api.test.js index 4d600b26..3e09f249 100644 --- a/src/features/Students/data/_test_/api.test.js +++ b/src/features/Students/data/_test_/api.test.js @@ -1,5 +1,5 @@ import { - getStudentbyInstitutionAdmin, handleEnrollments, getClassesByInstitution, + getStudentbyInstitutionAdmin, handleEnrollments, } from 'features/Students/data/api'; import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth'; @@ -59,26 +59,3 @@ describe('handleEnrollments', () => { ); }); }); - -describe('getClassesByInstitution', () => { - test('should call getClassesByInstitution with the correct parameters', () => { - const httpClientMock = { - get: jest.fn(), - }; - - const institutionId = 1; - const courseName = 'ccx1'; - - getAuthenticatedHttpClient.mockReturnValue(httpClientMock); - - getClassesByInstitution(institutionId, courseName); - - expect(getAuthenticatedHttpClient).toHaveBeenCalledTimes(3); - expect(getAuthenticatedHttpClient).toHaveBeenCalledWith(); - - expect(httpClientMock.get).toHaveBeenCalledTimes(1); - expect(httpClientMock.get).toHaveBeenCalledWith( - 'http://localhost:18000/pearson_course_operation/api/v2/classes/?limit=false&institution_id=1&course_name=ccx1', - ); - }); -}); diff --git a/src/features/Students/data/_test_/redux.test.js b/src/features/Students/data/_test_/redux.test.js index 37a7db6f..ceb71659 100644 --- a/src/features/Students/data/_test_/redux.test.js +++ b/src/features/Students/data/_test_/redux.test.js @@ -130,7 +130,7 @@ describe('Students redux tests', () => { }); test('successful fetch classes data', async () => { - const studentsApiUrl = `${process.env.COURSE_OPERATIONS_API_V2_BASE_URL}/classes/?limit=false&institution_id=1&course_name=Demo`; + const studentsApiUrl = `${process.env.COURSE_OPERATIONS_API_V2_BASE_URL}/classes/?limit=false&institution_id=1&course_name=Demo&instructors=`; const mockResponse = [ { classId: 'ccx-v1:demo+demo1+2020+ccx@2', @@ -154,14 +154,14 @@ describe('Students redux tests', () => { }); test('failed fetch classes data', async () => { - const studentsApiUrl = `${process.env.COURSE_OPERATIONS_API_V2_BASE_URL}/classes/?limit=false&institution_id=1`; + const studentsApiUrl = `${process.env.COURSE_OPERATIONS_API_V2_BASE_URL}/classes/?limit=false&institution_id=1&course_name=Demo&instructors=`; axiosMock.onGet(studentsApiUrl) .reply(500); expect(store.getState().students.classes.status) .toEqual('loading'); - await executeThunk(fetchClassesData(1), store.dispatch, store.getState); + await executeThunk(fetchClassesData(1, 'Demo'), store.dispatch, store.getState); expect(store.getState().students.classes.data) .toEqual([]); diff --git a/src/features/Students/data/api.js b/src/features/Students/data/api.js index 2985b06c..c8543e9e 100644 --- a/src/features/Students/data/api.js +++ b/src/features/Students/data/api.js @@ -23,15 +23,6 @@ function handleEnrollments(data, courseId) { ); } -function getClassesByInstitution(institutionId, courseName) { - const encodedCourseName = encodeURIComponent(courseName); - - return getAuthenticatedHttpClient().get( - `${getConfig().COURSE_OPERATIONS_API_V2_BASE_URL}/classes` - + `/?limit=false&institution_id=${institutionId}&course_name=${encodedCourseName}`, - ); -} - function getMetricsStudents() { const metricsData = { data: { @@ -45,6 +36,5 @@ function getMetricsStudents() { export { getStudentbyInstitutionAdmin, handleEnrollments, - getClassesByInstitution, getMetricsStudents, }; diff --git a/src/features/Students/data/thunks.js b/src/features/Students/data/thunks.js index 160579c5..5613bde3 100644 --- a/src/features/Students/data/thunks.js +++ b/src/features/Students/data/thunks.js @@ -14,8 +14,8 @@ import { fetchMetricsDataSuccess, fetchMetricsDataFailed, } from 'features/Students/data/slice'; -import { getStudentbyInstitutionAdmin, getClassesByInstitution, getMetricsStudents } from 'features/Students/data/api'; -import { getCoursesByInstitution } from 'features/Common/data/api'; +import { getStudentbyInstitutionAdmin, getMetricsStudents } from 'features/Students/data/api'; +import { getCoursesByInstitution, getClassesByInstitution } from 'features/Common/data/api'; function fetchStudentsData(currentPage, filtersData) { return async (dispatch) => { @@ -50,7 +50,7 @@ function fetchClassesData(id, courseName) { dispatch(fetchClassesDataRequest()); try { - const response = camelCaseObject(await getClassesByInstitution(id, courseName)); + const response = camelCaseObject(await getClassesByInstitution(id, courseName, false)); dispatch(fetchClassesDataSuccess(response.data)); } catch (error) { dispatch(fetchClassesDataFailed()); diff --git a/src/test-utils.js b/src/test-utils.js deleted file mode 100644 index 705b41ec..00000000 --- a/src/test-utils.js +++ /dev/null @@ -1,3 +0,0 @@ -export const executeThunk = async (thunk, dispatch, getState) => { - await thunk(dispatch, getState); -}; diff --git a/src/test-utils.jsx b/src/test-utils.jsx new file mode 100644 index 00000000..79082c13 --- /dev/null +++ b/src/test-utils.jsx @@ -0,0 +1,22 @@ +import React from 'react'; +import { render } from '@testing-library/react'; +import { Provider } from 'react-redux'; + +import { initializeStore } from 'store'; + +export const executeThunk = async (thunk, dispatch, getState) => { + await thunk(dispatch, getState); +}; + +export function renderWithProviders( + ui, + { + preloadedState = {}, + // Automatically create a store instance if no store was passed in + store = initializeStore(preloadedState), + ...renderOptions + } = {}, +) { + const Wrapper = ({ children }) => {children}; + return { store, ...render(ui, { wrapper: Wrapper, ...renderOptions }) }; +}