Skip to content

Commit

Permalink
feat: courses page
Browse files Browse the repository at this point in the history
  • Loading branch information
AuraAlba committed Dec 7, 2023
1 parent 801bcc1 commit 4f8e137
Show file tree
Hide file tree
Showing 17 changed files with 655 additions and 7 deletions.
6 changes: 4 additions & 2 deletions src/features/Common/data/_test_/api.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,17 +19,19 @@ describe('getCoursesByInstitution', () => {
};

const institutionId = 1;
const page = 1;

getAuthenticatedHttpClient.mockReturnValue(httpClientMock);

getCoursesByInstitution(institutionId);
getCoursesByInstitution(institutionId, true, page);

expect(getAuthenticatedHttpClient).toHaveBeenCalledTimes(1);
expect(getAuthenticatedHttpClient).toHaveBeenCalledWith();

expect(httpClientMock.get).toHaveBeenCalledTimes(1);
expect(httpClientMock.get).toHaveBeenCalledWith(
'http://localhost:18000/pearson_course_operation/api/v2/courses/?limit=false&institution_id=1',
'http://localhost:18000/pearson_course_operation/api/v2/courses/?limit=true&institution_id=1',
{ params: { page } },
);
});
});
9 changes: 7 additions & 2 deletions src/features/Common/data/api.js
Original file line number Diff line number Diff line change
@@ -1,9 +1,14 @@
import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth';
import { getConfig } from '@edx/frontend-platform';

function getCoursesByInstitution(institutionId) {
function getCoursesByInstitution(institutionId, limit, page, filters) {
const params = {
page,
...filters,
};
return getAuthenticatedHttpClient().get(
`${getConfig().COURSE_OPERATIONS_API_V2_BASE_URL}/courses/?limit=false&institution_id=${institutionId}`,
`${getConfig().COURSE_OPERATIONS_API_V2_BASE_URL}/courses/?limit=${limit}&institution_id=${institutionId}`,
{ params },
);
}

Expand Down
102 changes: 102 additions & 0 deletions src/features/Courses/CoursesFilters/_test_/index.test.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
/* eslint-disable react/prop-types */
import React from 'react';
import { render, fireEvent, act } from '@testing-library/react';
import CoursesFilters from 'features/Courses/CoursesFilters';
import '@testing-library/jest-dom/extend-expect';

jest.mock('react-select', () => function reactSelect({ options, valueR, onChange }) {
function handleChange(event) {
const option = options.find(
(optionR) => optionR.value === event.currentTarget.value,
);
onChange(option);
}

return (
<select data-testid="select" value={valueR} onChange={handleChange}>
{options.map(({ label, value }) => (
<option key={value} value={value}>
{label}
</option>
))}
</select>
);
});

describe('InstructorsFilters Component', () => {
const mockSetFilters = jest.fn();

beforeEach(() => {
mockSetFilters.mockClear();
});

const dataCourses = [
{
masterCourseName: 'Demo Course 1',
numberOfClasses: 1,
missingClassesForInstructor: null,
numberOfStudents: 1,
numberOfPendingStudents: 1,
},
{
masterCourseName: 'Demo Course 2',
numberOfClasses: 1,
missingClassesForInstructor: 1,
numberOfStudents: 16,
numberOfPendingStudents: 0,
},
];

test('call service when apply filters', async () => {
const fetchData = jest.fn();
const resetPagination = jest.fn();
const { getByText, getByTestId } = render(
<CoursesFilters
fetchData={fetchData}
resetPagination={resetPagination}
setFilters={mockSetFilters}
dataCourses={dataCourses}
/>,
);

const courseSelect = getByTestId('select');
const buttonApplyFilters = getByText('Apply');

expect(courseSelect).toBeInTheDocument();
fireEvent.change(courseSelect, {
target: { value: 'Demo Course 1' },
});

expect(getByText('Demo Course 1')).toBeInTheDocument();
await act(async () => {
fireEvent.click(buttonApplyFilters);
});
expect(fetchData).toHaveBeenCalledTimes(1);
});

test('clear filters', async () => {
const fetchData = jest.fn();
const resetPagination = jest.fn();
const { getByText, getByTestId } = render(
<CoursesFilters
fetchData={fetchData}
resetPagination={resetPagination}
setFilters={mockSetFilters}
dataCourses={dataCourses}
/>,
);

const courseSelect = getByTestId('select');
const buttonClearFilters = getByText('Reset');

expect(courseSelect).toBeInTheDocument();
expect(courseSelect).toBeInTheDocument();
fireEvent.change(courseSelect, {
target: { value: 'Demo Course 1' },
});
await act(async () => {
fireEvent.click(buttonClearFilters);
});
expect(resetPagination).toHaveBeenCalledTimes(1);
});
});
79 changes: 79 additions & 0 deletions src/features/Courses/CoursesFilters/index.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
import React, { useState, useEffect } from 'react';

import { Col, Form } from '@edx/paragon';
import { Select, Button } from 'react-paragon-topaz';
import { logError } from '@edx/frontend-platform/logging';
import PropTypes from 'prop-types';

const CoursesFilters = ({
fetchData, resetPagination, dataCourses, setFilters,
}) => {
const [courseOptions, setCourseOptions] = useState([]);
const [courseSelected, setCourseSelected] = useState(null);

const handleCoursesFilter = async (e) => {
e.preventDefault();
const form = e.target;
const formData = new FormData(form);
const formJson = Object.fromEntries(formData.entries());
setFilters(formJson);
try {
fetchData(formJson);
} catch (error) {
logError(error);
}
};

const handleCleanFilters = () => {
fetchData();
resetPagination();
setCourseSelected(null);
setFilters({});
};

useEffect(() => {
if (dataCourses.length > 0) {
const options = dataCourses.map(course => ({
...course,
label: course.masterCourseName,
value: course.masterCourseName,
}));
setCourseOptions(options);
}
}, [dataCourses]);

return (
<div className="filter-container justify-content-center row">
<div className="col-11">
<h3>Find a course</h3>
<Form className="row justify-content-center" onSubmit={handleCoursesFilter}>
<Form.Row className="col-12">
<Form.Group as={Col}>
<Select
placeholder="Course"
name="course_name"
className="mr-2"
options={courseOptions}
onChange={option => setCourseSelected(option)}
value={courseSelected}
/>
</Form.Group>
<div className="d-flex col-3 justify-content-end mr-3 align-items-start">
<Button onClick={handleCleanFilters} variant="tertiary" text className="mr-2">Reset</Button>
<Button type="submit">Apply</Button>
</div>
</Form.Row>
</Form>
</div>
</div>
);
};

CoursesFilters.propTypes = {
fetchData: PropTypes.func.isRequired,
resetPagination: PropTypes.func.isRequired,
dataCourses: PropTypes.instanceOf(Array).isRequired,
setFilters: PropTypes.func.isRequired,
};

export default CoursesFilters;
57 changes: 57 additions & 0 deletions src/features/Courses/CoursesPage/_test_/index.test.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import React from 'react';
import axios from 'axios';
import CoursesPage from 'features/Courses/CoursesPage';
import {
render,
waitFor,
} from '@testing-library/react';
import '@testing-library/jest-dom/extend-expect';

jest.mock('axios');

jest.mock('@edx/frontend-platform/logging', () => ({
logError: jest.fn(),
}));

const mockResponse = {
data: {
results: [
{
masterCourseName: 'Demo Course 1',
numberOfClasses: 1,
missingClassesForInstructor: null,
numberOfStudents: 1,
numberOfPendingStudents: 1,
},
{
masterCourseName: 'Demo Course 2',
numberOfClasses: 1,
missingClassesForInstructor: 1,
numberOfStudents: 16,
numberOfPendingStudents: 0,
},
],
count: 2,
num_pages: 1,
current_page: 1,
},
};

describe('CoursesPage', () => {
it('renders courses data and pagination', async () => {
axios.get.mockResolvedValue(mockResponse);

const component = render(<CoursesPage />);

waitFor(() => {
expect(component.container).toHaveTextContent('Demo Course 1');
expect(component.container).toHaveTextContent('Demo Course 2');
expect(component.container).toHaveTextContent('Ready');
expect(component.container).toHaveTextContent('Missing (1)');
expect(component.container).toHaveTextContent('Pending (1)');
expect(component.container).toHaveTextContent('Complete');
expect(component.container).toHaveTextContent('1/2');
expect(component.container).toHaveTextContent('16/16');
});
});
});
71 changes: 71 additions & 0 deletions src/features/Courses/CoursesPage/_test_/reducer.test.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
import {
FETCH_COURSES_DATA_FAILURE,
FETCH_COURSES_DATA_REQUEST,
FETCH_COURSES_DATA_SUCCESS,
UPDATE_CURRENT_PAGE,
} from 'features/Courses/actionTypes';
import { RequestStatus } from 'features/constants';
import reducer from 'features/Courses/CoursesPage/reducer';

describe('Instructor page reducers', () => {
const initialState = {
data: [],
status: RequestStatus.SUCCESS,
error: null,
currentPage: 1,
numPages: 0,
};

test('should handle FETCH_COURSES_DATA_REQUEST', () => {
const state = {
...initialState,
status: RequestStatus.LOADING,
};
const action = {
type: FETCH_COURSES_DATA_REQUEST,
};
expect(reducer(state, action)).toEqual(state);
});

test('should handle FETCH_COURSES_DATA_SUCCESSS', () => {
const state = {
...initialState,
status: RequestStatus.SUCCESS,
count: 0,
};
const action = {
type: FETCH_COURSES_DATA_SUCCESS,
payload: {
results: [],
count: 0,
numPages: 0,
},
};
expect(reducer(state, action)).toEqual(state);
});

test('should handle FETCH_COURSES_DATA_FAILURE', () => {
const state = {
...initialState,
status: RequestStatus.ERROR,
error: '',
};
const action = {
type: FETCH_COURSES_DATA_FAILURE,
payload: '',
};
expect(reducer(state, action)).toEqual(state);
});

test('should handle UPDATE_CURRENT_PAGE', () => {
const state = {
...initialState,
currentPage: 1,
};
const action = {
type: UPDATE_CURRENT_PAGE,
payload: 1,
};
expect(reducer(state, action)).toEqual(state);
});
});
Loading

0 comments on commit 4f8e137

Please sign in to comment.